diff --git a/.github/actions/toolchains/action.yml b/.github/actions/toolchains/action.yml index 6f35a7ac..8f50d713 100644 --- a/.github/actions/toolchains/action.yml +++ b/.github/actions/toolchains/action.yml @@ -20,8 +20,7 @@ runs: with: python-version: '3.12' # The python fixture check type-checks the generated package with mypy - # against Python 3.9, the oldest version it supports; mypy 2 can't - # target 3.9. + # against Python 3.10, the oldest version it supports. - if: inputs.lang == 'python' shell: bash run: python -m pip install 'mypy==1.20.2' @@ -48,7 +47,7 @@ runs: - if: inputs.lang == 'go' uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' - if: inputs.lang == 'dotnet' uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9c08aab..c8f7b695 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,20 +70,20 @@ jobs: targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - run: cargo build -p weaveffi-cli - # `weaveffi diff --check` is the CI gate we recommend to users, so it - # must hold for our own samples: a fresh tree diffs clean, and a tree - # with a stale file is reported instead of silently passing. - - name: weaveffi diff --check on every sample + # `weaveffi generate --check` is the CI gate we recommend to users, so + # it must hold for our own samples: a fresh tree checks clean, and a + # tree with a stale file is reported instead of silently passing. + - name: weaveffi generate --check on every sample run: | set -euo pipefail BIN=target/debug/weaveffi for sample in calculator codec kvstore; do "$BIN" generate "samples/$sample" -o "diffcheck/$sample" --quiet - "$BIN" diff "samples/$sample" -o "diffcheck/$sample" --check --quiet + "$BIN" generate "samples/$sample" -o "diffcheck/$sample" --check --quiet done echo "stale" >> diffcheck/calculator/c/calculator.h - if "$BIN" diff samples/calculator -o diffcheck/calculator --check --quiet; then - echo "diff --check did not detect a modified file" >&2 + if "$BIN" generate samples/calculator -o diffcheck/calculator --check --quiet; then + echo "generate --check did not detect a modified file" >&2 exit 1 fi - name: weaveffi.schema.json is up to date @@ -197,7 +197,7 @@ jobs: ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }} run: | set -euxo pipefail - cargo run -q -p weaveffi-cli -- build samples/kvstore --platforms android-arm64 --target kotlin + cargo run -q -p weaveffi-cli -- build samples/kvstore --platforms android-arm64 --target kotlin --strict readelf="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" "$readelf" -d target/weaveffi/android-arm64/libkvstore.so | grep 'SONAME.*libkvstore.so' "$readelf" -d target/weaveffi/android-arm64/libkvstore_jni.so | grep 'NEEDED.*libkvstore.so' diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index cf7d7b6a..f0750094 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -24,7 +24,6 @@ jobs: target: - fuzz_parse_yaml - fuzz_parse_json - - fuzz_parse_toml - fuzz_parse_type_ref - fuzz_validate - fuzz_value_buffer @@ -50,7 +49,7 @@ jobs: - name: Seed the corpus run: | - python -m pip install pyyaml tomli-w + python -m pip install pyyaml python crates/weaveffi-fuzz/seed_corpus.py ${{ matrix.target }} \ crates/weaveffi-fuzz/corpus/${{ matrix.target }} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index aa7e25ba..c1974446 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,33 +2,39 @@ The contributor guide to the architecture lives in [`docs/src/architecture.md`](docs/src/architecture.md), and the normative C -ABI is [`docs/src/reference/abi.md`](docs/src/reference/abi.md). Read them -before changing a generator, the IDL or IR, validation, the producer macro, -the runtime, or the orchestrator. +ABI (revision 5) is [`docs/src/reference/abi.md`](docs/src/reference/abi.md). +Read them before changing a generator, the IDL or IR, validation, the +producer macro, the runtime, or the orchestrator. The pipeline: ```text a Rust producer crate (the API read from its built library's metadata) -or an IDL (YAML/JSON/TOML) +or an IDL (YAML/JSON) → IR (Api) - → validate (every rule, including the C symbol table, under the - Identity from weaveffi.toml / cargo metadata) - → Model (C symbols and ABI signatures, computed once; the plan) - → Target::render (pure, per language) - → Orchestrator (changed-file writes, generation records, stale-file - removal) + → validate (every rule, including the C symbol and slot tables, under + the Identity from weaveffi.toml / cargo metadata) + → Model (v2: types split by position as Ty / ParamTy / RetTy; every + binding stores its AbiFn and its passing contracts, ArgPass, RetPass, + ResultPass, ItemPass, CallbackRetPass, and ErrorStrategy, resolved once) + → Target::render (pure, per language, through the shared emitters in + codegen/; never re-derives a family, slot, or error domain) + → Orchestrator (a Changeset: changed-file writes, generation records, + stale-file removal; `generate --check` and `--diff` report it) ``` The workspace crates: -- `weaveffi-model`: the IR, IDL parsing, the macro's Rust extraction, - library metadata frames, validation, package identity, the model, ABI - lowering, the marshalling plan, and contract tables. -- `weaveffi-cli`: the `weaveffi` command and the library behind it: the - backend framework, the orchestrator and generation records, packaging, and - the eleven language targets under `src/targets/`. -- `weaveffi-macros`: `#[weaveffi::module]` and `export_runtime!`. +- `weaveffi-model`: the IR, YAML and JSON parsing, library metadata frames, + validation, package identity, the model and its symbol table, C ABI + lowering, the passing contracts (`plan`), error-type naming, and contract + tables. It doesn't depend on `syn`. +- `weaveffi-cli`: the `weaveffi` command and the library behind it: the one + `Target` trait and its `REGISTRY`, the shared emitters (`codegen::codecs`, + `contract`, `errors`, `docs`), the orchestrator and generation records, + packaging, and the eleven language targets under `src/targets/`. +- `weaveffi-macros`: `#[weaveffi::module]`, the marker attributes, and + `export_runtime!`; its `src/extract.rs` lowers a module tree to the IR. - `weaveffi`: the producer facade crate and the C ABI runtime (`weaveffi::abi`) linked into every producer. - `weaveffi-fuzz`: unpublished fuzz harnesses. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71b2696b..b034aa55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,14 +53,17 @@ should also pass that target's fixture check and conformance lanes. ### Snapshot tests -Snapshot tests (`crates/weaveffi-cli/tests/snapshots.rs`) pin the exact -output of every generator for the fixtures in -`crates/weaveffi-cli/tests/fixtures/`, using +Snapshot tests (`crates/weaveffi-cli/tests/snapshots.rs`) render every +registered target for every fixture in `crates/weaveffi-cli/tests/fixtures/` +and pin the exact output for the `kitchen_sink` fixture, file by file, using [`cargo-insta`](https://insta.rs/). Fixed files (runtimes, package -manifests, and READMEs, listed per target in `FIXED_FILES`) are snapshotted -once, from `kitchen_sink`; a target's copy of the C header is asserted -byte-equal to the C target's own header instead of snapshotted; every other -file is snapshotted for every fixture. When output changes on purpose: +manifests, and READMEs, listed per target by `Target::fixed_files`) aren't +snapshotted, and a target's copy of the C header is asserted byte-equal to +the C target's own header instead. The other fixtures are covered by the +fixture compile checks, the determinism test, and targeted assertions in +each target's `tests.rs` (which should test only target-specific naming, +escaping, and configuration, not what the snapshot already pins). When +output changes on purpose: ```bash cargo install cargo-insta --locked @@ -80,7 +83,11 @@ type-checks it with the target language's toolchain (through `scripts/fixtures/.sh`). Run it for any target whose output you change; CI runs one job per target. A missing tool (including `mypy` 1.x for Python and the node-gyp headers from `npx node-gyp install` for Node.js) is -reported as a skip locally and fails the check when `CI=true`. +reported as a skip locally and fails the check when `CI=true`. The Python +and Ruby checks (and conformance lanes) need Python 3.10 and Ruby 3.2 or +newer: they use the `python3` and `ruby` on PATH when those are new enough, +and otherwise look for a `python3.N` on PATH or Homebrew's Ruby (see +`scripts/toolchains.sh`). ### Conformance @@ -121,11 +128,15 @@ Preview the book with `mdbook serve docs -p 3000 -n 127.0.0.1`. Read the [architecture guide](docs/src/architecture.md) first; its "Adding a generator" section is the checklist. In short: add -`crates/weaveffi-cli/src/targets//` implementing `LanguageBackend`, -register it with one line in the `cli_targets!` registry in -`crates/weaveffi-cli/src/config.rs`, add it to the snapshot tests, add -`scripts/fixtures/.sh` and `conformance//`, and document it under -`docs/src/generators/`. +`crates/weaveffi-cli/src/targets//` implementing `Target` (rendering +from the passing contracts stored on the model and through the shared +emitters in `codegen/`, never matching on a type's family), register +it with one entry in `REGISTRY` in `crates/weaveffi-cli/src/targets/mod.rs` +(which also adds it to `--target`, the config tables, and the snapshot +tests), add `scripts/fixtures/.sh` and `conformance//` and list +the target in the CI matrices, `scripts/check-fixtures.sh`, and +`conformance/run.sh` (a test checks that every list matches the registry), +and document it, with its tier, under `docs/src/generators/`. ## Fuzzing @@ -143,10 +154,9 @@ cargo install cargo-fuzz --locked ``` Seed a target's corpus from its committed seeds and the snapshot fixtures -(needs `pip install pyyaml tomli-w`), then run it for 60 seconds (swap the -target name for any of `fuzz_parse_yaml`, `fuzz_parse_json`, -`fuzz_parse_toml`, `fuzz_parse_type_ref`, `fuzz_validate`, -`fuzz_value_buffer`): +(needs `pip install pyyaml`), then run it for 60 seconds (swap the target +name for any of `fuzz_parse_yaml`, `fuzz_parse_json`, `fuzz_parse_type_ref`, +`fuzz_validate`, `fuzz_value_buffer`): ```bash python3 crates/weaveffi-fuzz/seed_corpus.py fuzz_parse_yaml \ @@ -302,8 +312,8 @@ fix/android-jni-crash - **CI** (`ci.yml`): formatting, clippy, rustdoc, and a build of `weaveffi-model` without its IDL features; the test suite with snapshot - checks on Linux, macOS, and Windows; `weaveffi diff --check` on every - sample, the JSON Schema drift check, and a `wasm32` build of every sample + checks on Linux, macOS, and Windows; `weaveffi generate --check` on + every sample, the JSON Schema drift check, and a `wasm32` build of every sample whose embedded metadata must match the native build's; the fixture compile check per target; the conformance harness per language on Linux and macOS; an Android job that links the Kotlin JNI shim with the NDK and runs diff --git a/Cargo.lock b/Cargo.lock index 485e61d2..977999c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "annotate-snippets" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a5b0a5ab8561c603f16c3b78c24a386b1f70e7fc1b1b1e8c15738828e4a176" +dependencies = [ + "anstyle", + "memchr", + "unicode-width 0.2.2", +] + [[package]] name = "anstream" version = "1.0.0" @@ -82,18 +93,18 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "assert_cmd" version = "2.2.2" @@ -139,6 +150,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -326,6 +343,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -466,6 +489,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -599,6 +645,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +[[package]] +name = "granit-parser" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da55feca5d1625466bae068f9fd4b87892caeae70c3fe671ddb0f50d41aebf37" +dependencies = [ + "arraydeque", + "smallvec", +] + [[package]] name = "half" version = "2.7.1" @@ -904,6 +960,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1223,12 +1291,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "same-file" version = "1.0.6" @@ -1263,6 +1325,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -1273,6 +1341,23 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253edfdea34032da21af84e16fcc94f4c7a6ba237f9f3bc2dbf43cea8535c44" +dependencies = [ + "annotate-snippets", + "base64", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -1335,19 +1420,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.14.2", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1371,6 +1443,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -1706,12 +1784,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -1826,7 +1898,6 @@ dependencies = [ name = "weaveffi-cli" version = "0.25.0" dependencies = [ - "anyhow", "assert_cmd", "camino", "clap", @@ -1842,8 +1913,8 @@ dependencies = [ "rayon", "schemars", "serde", + "serde-saphyr", "serde_json", - "serde_yaml", "sha2", "similar", "tar", @@ -1880,14 +1951,11 @@ version = "0.25.0" dependencies = [ "heck", "miette", - "proc-macro2", "schemars", "serde", + "serde-saphyr", "serde_json", - "serde_yaml", - "syn 2.0.119", "thiserror", - "toml 0.8.23", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index eb281756..31b28a18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ default-members = [ [workspace.package] version = "0.25.0" -rust-version = "1.85" +rust-version = "1.89" edition = "2021" license = "MIT OR Apache-2.0" authors = ["WeaveFFI Contributors"] @@ -30,7 +30,7 @@ weaveffi = { path = "crates/weaveffi", version = "0.25.0" } weaveffi-macros = { path = "crates/weaveffi-macros", version = "0.25.0" } weaveffi-model = { path = "crates/weaveffi-model", version = "0.25.0", default-features = false } serde = { version = "1.0", features = ["derive"] } -serde_yaml = "0.9" +serde-saphyr = { version = "2", default-features = false, features = ["serialize", "deserialize"] } serde_json = "1.0" toml = "0.8" thiserror = "1.0" diff --git a/README.md b/README.md index 03b5209f..086fd8e7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # WeaveFFI -[![CI](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml/badge.svg)](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) [![Schema](https://img.shields.io/badge/schema-0.11.0-orange)](./weaveffi.schema.json) [![C ABI](https://img.shields.io/badge/C%20ABI-4-orange)](docs/src/reference/abi.md) +[![CI](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml/badge.svg)](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) [![Schema](https://img.shields.io/badge/schema-0.12.0-orange)](./weaveffi.schema.json) [![C ABI](https://img.shields.io/badge/C%20ABI-5-orange)](docs/src/reference/abi.md) WeaveFFI generates idiomatic, type-safe bindings for 11 languages (C, C++, Swift, Kotlin, Node.js, WebAssembly, Python, .NET, Dart, Go, and Ruby) for any native library that exposes a C ABI. Write the library in Rust and annotate a -module with `#[weaveffi::module]`, or describe it in a YAML, JSON, or TOML IDL -and implement the generated C header in C, C++, Zig, or anything else. Either +module with `#[weaveffi::module]`, or describe it in a YAML or JSON IDL and +implement the generated C header in C, C++, Zig, or anything else. Either way, every language talks to the same stable C ABI, and every package is named after your library, not after WeaveFFI. @@ -17,7 +17,8 @@ What you get, in every language: `deinit`, RAII) with a garbage-collector backstop, and safe to close while another thread is mid-call. - **Typed errors.** An error domain becomes an exception hierarchy, a Swift - error enum, or a Go error type the consumer can match on. + error enum, or a Go error type the consumer can match on, and a domain + can gain codes without breaking deployed bindings. - **Async with cancellation.** `async` functions become `Promise`s, coroutines, `Task`s, `async throws`, or awaitables, and cancelling them (an `AbortSignal`, a `CancellationToken`, a `context.Context`, task @@ -45,8 +46,8 @@ cargo new --lib kvstore && cd kvstore cargo add weaveffi ``` -Add `crate-type = ["cdylib"]` under `[lib]` in `Cargo.toml`, then put this in -`src/lib.rs`: +Put this in `src/lib.rs` (no `crate-type` is needed: WeaveFFI builds the +`cdylib` itself): ```rust #[weaveffi::module] @@ -54,6 +55,7 @@ pub mod kv { use std::collections::BTreeMap; use std::sync::Mutex; + /// The store's errors. The macro generates `Display` from the docs. #[weaveffi::error] #[derive(Debug)] pub enum KvError { @@ -63,15 +65,6 @@ pub mod kv { StoreFull = 1003, } - impl std::fmt::Display for KvError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - KvError::KeyNotFound => "key not found", - KvError::StoreFull => "store has reached capacity", - }) - } - } - /// An in-memory key-value store. #[weaveffi::interface] pub struct Store { @@ -94,9 +87,14 @@ pub mod kv { Ok(entries.insert(key.to_string(), value).is_none()) } + /// The value under `key`. + pub fn get(&self, key: &str) -> Result, KvError> { + self.entries.lock().unwrap().get(key).cloned().ok_or(KvError::KeyNotFound) + } + /// The number of entries. - pub fn count(&self) -> i64 { - self.entries.lock().unwrap().len() as i64 + pub fn count(&self) -> usize { + self.entries.lock().unwrap().len() } } } @@ -107,7 +105,11 @@ weaveffi::export_runtime!(); The macro emits the `extern "C"` functions; you write no `unsafe` code. An interface type must be `Send + Sync`, because the object is shared across the -boundary as an `Arc`. This is a trimmed-down store; the full +boundary as an `Arc`. Error messages come from the variants' doc comments +(or `#[weaveffi(message = "...")]` templates that interpolate fields), and a +`Result` whose error type isn't a declared domain (`String`, +`std::io::Error`, `anyhow::Error`) throws an untyped error with its +`Display` text. This is a trimmed-down store; the full [`samples/kvstore`](samples/kvstore/src/lib.rs) adds records, enums, callbacks, iterators, async, and the rest (see [Samples](docs/src/samples.md)). @@ -115,8 +117,8 @@ callbacks, iterators, async, and the rest (see [Samples](docs/src/samples.md)). the library name all come from the crate. ```bash -weaveffi init # writes weaveffi.toml: [project] input = "." (this crate) -weaveffi generate # builds libkvstore, reads its API, writes every target to ./bindings +weaveffi init # writes weaveffi.toml: input = "." (this crate), targets = ["c", "python"] +weaveffi generate # builds libkvstore, reads its API, writes the targets to ./bindings ``` `weaveffi generate` builds the crate and reads the API from the library it @@ -139,35 +141,48 @@ with kvstore.Store.open("data.kv") as store: store.put("greeting", b"hello") print(store.count()) # 1 try: - store.put("another", b"value") - except kvstore.StoreFull as e: - print(e.code, e.message) # 1003 store has reached capacity + store.get("missing") + except kvstore.KvError as e: + print(e.code, e.message) # 1001 key not found ```
The C header every target binds to (bindings/c/kvstore.h) ```c -#define KVSTORE_ABI_VERSION 4u +#define KVSTORE_ABI_VERSION 5u KVSTORE_API uint32_t kvstore_abi_version(void); + +typedef struct kvstore_error { + int32_t code; /* 0 ok, >0 a KvError code, <0 a runtime code */ + const uint8_t* message_ptr; /* UTF-8, not NUL-terminated */ + size_t message_len; + const uint8_t* payload_ptr; /* the code's fields, or NULL */ + size_t payload_len; +} kvstore_error; + KVSTORE_API const kvstore_contract_entry* kvstore_kv_contract(size_t* out_len); -#define KVSTORE_KV_CONTRACT_LEN 5 +#define KVSTORE_KV_CONTRACT_LEN 8 -typedef enum { +typedef int32_t kvstore_kv_KvError; +enum { kvstore_kv_KvError_KeyNotFound = 1001, kvstore_kv_KvError_StoreFull = 1003 -} kvstore_kv_KvError; +}; typedef struct kvstore_kv_Store kvstore_kv_Store; KVSTORE_API kvstore_kv_Store* kvstore_kv_Store_open(const uint8_t* path_ptr, size_t path_len, kvstore_error* out_err); KVSTORE_API bool kvstore_kv_Store_put(const kvstore_kv_Store* self, const uint8_t* key_ptr, size_t key_len, const uint8_t* value_ptr, size_t value_len, kvstore_error* out_err); -KVSTORE_API int64_t kvstore_kv_Store_count(const kvstore_kv_Store* self, kvstore_error* out_err); +KVSTORE_API const uint8_t* kvstore_kv_Store_get(const kvstore_kv_Store* self, const uint8_t* key_ptr, size_t key_len, size_t* out_len, kvstore_error* out_err); +KVSTORE_API uint64_t kvstore_kv_Store_count(const kvstore_kv_Store* self, kvstore_error* out_err); KVSTORE_API kvstore_kv_Store* kvstore_kv_Store_clone(const kvstore_kv_Store* self); KVSTORE_API void kvstore_kv_Store_destroy(kvstore_kv_Store* self); ``` Every symbol and type carries the library's own prefix (`kvstore_`), so any -number of WeaveFFI-built libraries can link into one process. +number of WeaveFFI-built libraries can link into one process. Enums and +error codes are `int32_t` typedefs, never `typedef enum`, so their size is +fixed on every compiler, and a `usize` count crosses as `uint64_t`.
@@ -175,17 +190,19 @@ number of WeaveFFI-built libraries can link into one process. Swift (bindings/swift/Sources/Kvstore/Kvstore.swift) ```swift -public enum KvError: Error, LocalizedError, Sendable { +public enum KvError: Error, LocalizedError, Hashable, Sendable { case keyNotFound(message: String) case storeFull(message: String) + case unknown(code: Int32, message: String) // a code from a newer library } -public final class Store: @unchecked Sendable { +public final class Store: Hashable, @unchecked Sendable { deinit { kvstore_kv_Store_destroy(ptr) } public static func open(path: String) throws -> Store { /* ... */ } public func put(key: String, value: Data) throws -> Bool { /* ... */ } - public func count() -> Int64 { /* ... */ } + public func get(key: String) throws -> Data { /* ... */ } + public func count() -> UInt64 { /* ... */ } } ``` @@ -196,25 +213,19 @@ public final class Store: @unchecked Sendable { ```python class KvError(Error): - """Base exception for the `kv` module's error domain.""" + """Base exception of the `KvError` error domain (module `kv`).""" -class KeyNotFound(KvError): +class KeyNotFoundError(KvError): CODE = 1001 -class StoreFull(KvError): +class StoreFullError(KvError): CODE = 1003 class Store(_Object): # close() it, or use `with` @classmethod - def open(cls, path: str) -> Store: - _path_b = path.encode("utf-8") - _err = _ErrorStruct() - _ret = _c_kv_Store_open(_path_b, len(_path_b), ctypes.byref(_err)) - if _err.code: - raise _kv_error_from(*_read_error(_err)) - return cls._adopt(_required(_ret)) - + def open(cls, path: str) -> Store: ... def put(self, key: str, value: bytes) -> bool: ... + def get(self, key: str) -> bytes: ... def count(self) -> int: ... ``` @@ -227,19 +238,22 @@ Started](docs/src/getting-started.md). ## Targets -| Target | Package | Objects | Async | Cancellation | Callbacks | -|---|---|---|---|---|---| -| C | `{library}.h` (+ `{library}_buffer.h` codecs) | `_clone` / `_destroy` | completion callback | cancel token | vtable struct | -| C++ | `{library}.hpp`, CMake target | RAII class | `std::future` | `CancelToken` | abstract class | -| Swift | SwiftPM package | `deinit` | `async throws` | task cancellation | `protocol` | -| Kotlin | Gradle (Android or JVM) | `AutoCloseable` + cleaner | `suspend` | coroutine cancellation | `interface` | -| Node.js | npm package (N-API addon) | `close()`, `Symbol.dispose` | `Promise` | `AbortSignal` | object | -| WebAssembly | npm package (ESM) | `close()`, `Symbol.dispose` | `Promise` | `AbortSignal` | object | -| Python | `pyproject.toml` package (typed, `py.typed`) | `close()`, `with` | awaitable | task cancellation | ABC | -| .NET | `.csproj` (NuGet) | `IDisposable` over `SafeHandle` | `Task` | `CancellationToken` | interface | -| Dart | pub package | `dispose()` + `NativeFinalizer` | `Future` | `CancelToken` | abstract class | -| Go | Go module (cgo) | `Close() error` | blocking call | `context.Context` | interface | -| Ruby | gem | `close` | blocking call | `cancel:` token | module | +| Target | Tier | Package | Objects | Async | Cancellation | Callbacks | +|---|---|---|---|---|---|---| +| C | 1 | `{library}.h` (+ `{library}_buffer.h` codecs) | `_clone` / `_destroy` | completion callback | cancel token | vtable struct | +| C++ | 1 | `{library}.hpp`, CMake target | RAII class | `std::future` | `CancelToken` | abstract class | +| Swift | 1 | SwiftPM package | `deinit` | `async throws` | task cancellation | protocol | +| Kotlin | 1 | Gradle (Android or JVM) | `close()` + cleaner | `suspend` | coroutine cancellation | interface | +| Node.js | 1 | npm package (N-API addon) | `close()`, `Symbol.dispose` | `Promise` | `AbortSignal` | object | +| Python | 1 | `pyproject.toml` package (typed, `py.typed`) | `close()`, `with` | awaitable | task cancellation | abstract base class | +| .NET | 1 | `.csproj` (NuGet) | `Dispose()` over `SafeHandle` | `Task` | `CancellationToken` | interface | +| Go | 2 | Go module (cgo) | `Close()` | blocking call | `context.Context` | interface | +| Ruby | 2 | gem | `close` | blocking call | `cancel:` token | duck-typed object | +| Dart | 2 | pub package | `dispose()` + `NativeFinalizer` | `Future` | `CancelToken` | abstract class | +| WebAssembly | 2 | npm package (ESM) | `close()`, `Symbol.dispose` | `Promise` | `AbortSignal` | object | + +Tier 1 targets get new ABI work first; Tier 2 targets may lag a release +behind (see [Stability](docs/src/stability.md#target-tiers)). The [capability matrix](docs/src/generators/README.md) and the per-language pages have the details: type mapping, naming, loading, and known limits. @@ -260,14 +274,21 @@ release](https://github.com/weavefoundry/weaveffi/releases). | Command | What it does | |---|---| | `weaveffi init [dir]` | Write `weaveffi.toml` (and a starter IDL outside a Rust crate) | -| `weaveffi generate [input]` | Generate bindings from a Rust producer crate (built first; `--library ` reads an existing build) or an IDL; `--target c,swift` to subset, `--dry-run` to list files. Only changed files are rewritten, and files a previous run produced that are no longer generated are removed | +| `weaveffi generate [input]` | Generate bindings from a Rust producer crate (built first; `--library ` reads an existing build) or an IDL; `--target c,swift` to subset, `--dry-run` to list files, `--check` to exit 1 when anything would change (for CI), `--diff` to print the changes. Only changed files are rewritten, and files a previous run produced that are no longer generated are removed | | `weaveffi validate [input]` | Validate without generating; `--warn` for lints, `--format json` | -| `weaveffi diff [input]` | Show what regenerating would change; `--check` exits non-zero for CI | | `weaveffi build [input]` | Cross-compile the Rust producer per platform (`--platforms`) into `target/weaveffi//`, with prebuilt Node.js and JNI glue | | `weaveffi package [input]` | Build, then write installable artifacts per target to `dist/`: wheels, npm tarballs, gems, an `XCFramework` SwiftPM package, a `.nupkg`, and more | -| `weaveffi dev [input]` | Build the producer's debug library, generate, and point the bindings at the library | +| `weaveffi dev [input]` | Build the producer's library, generate, and point the bindings at the library | | `weaveffi extract [input]` | Print the IDL a Rust producer's built library embeds (`--library ` to read a given build) | -| `weaveffi schema` | Print the JSON Schema of the IDL | +| `weaveffi schema` | Print the JSON Schema of the IDL (`--version` prints its version) | +| `weaveffi completions ` | Print shell completions | + +Commands that build a Rust producer take `--profile `, the Cargo +profile to build with: `dev` for `generate`, `validate`, `extract`, and +`dev`, and `release` (or `[build] profile`) for `build` and `package`. +`build` and `package` skip, with a warning, an artifact whose external tool +(a C compiler, Xcode, the .NET SDK) is missing, and fail on it only with +`--strict`. With a `[project]` table in `weaveffi.toml`, every command works without arguments from anywhere in the project: @@ -304,11 +325,22 @@ others. ## Status -WeaveFFI is pre-1.0: the IDL schema is `0.11.0` and the C ABI is revision 4, -and either may change in a minor release. See [Stability and +WeaveFFI is pre-1.0: the IDL schema is `0.12.0` and the C ABI is revision 5, +and either may still change in a minor release. Revision 5 is designed to be +extended additively (new declarations, new callback methods, and new error +codes don't need a revision 6), and every binding checks the revision and a +per-declaration contract table when it loads the library. See [Stability and Versioning](docs/src/stability.md) for the policy and the migration notes, and the [Roadmap](docs/src/roadmap.md) for what's next. +The targets come in two tiers that pass the same conformance gate: + +- **Tier 1**: C, C++, Swift, Kotlin, Python, Node.js, and .NET. These get + new ABI and IDL features first, and their output is held to the strictest + review. +- **Tier 2**: Go, Ruby, Dart, and WebAssembly. These are fully supported and + conformance-tested, but may lag Tier 1 when the ABI grows. + Every pull request runs formatting, clippy, rustdoc, the test suite and snapshot corpus on Linux, macOS, and Windows, a compile check of every snapshot fixture with each language's own toolchain, and an end-to-end diff --git a/clippy.toml b/clippy.toml index c05e8849..8a6dfd62 100644 --- a/clippy.toml +++ b/clippy.toml @@ -27,5 +27,8 @@ doc-valid-idents = [ "lowerCamelCase", "SCREAMING_SNAKE", "SCREAMING_SNAKE_CASE", + # The C ABI family of optional scalars, named like its siblings + # (Direct, Slice, Buffer) in prose. + "OptDirect", "..", ] diff --git a/conformance/c/calculator.c b/conformance/c/calculator.c index deb1ed01..9d491589 100644 --- a/conformance/c/calculator.c +++ b/conformance/c/calculator.c @@ -1,13 +1,15 @@ -// Conformance consumer: calculator sample, C target (ABI revision 4). +// Conformance consumer: calculator sample, C target (ABI revision 5). // -// The getting-started surface: a direct-value call, a throwing call that -// reports the typed `CalcError` domain, and a string in and out (non-ASCII -// text and an interior NUL byte survive). Ends by asserting the producer's -// leak counters are zero. +// The getting-started surface: a direct-value call, throwing calls that +// report the typed `CalcError` and `ParseError` domains (one with a payload) +// and an untyped (`throws: any`) failure, a string in and out (non-ASCII +// text and an interior NUL byte survive), an optional scalar return +// (OptDirect), and typed arrays in both directions (Slice). Ends by asserting +// the producer's leak counters are zero. #include "harness.h" -#include "calculator.h" +#include "calculator_buffer.h" static void greet(const void* name, size_t name_len, const void* expected, size_t expected_len) { calculator_error err = {0}; @@ -19,6 +21,29 @@ static void greet(const void* name, size_t name_len, const void* expected, size_ calculator_free_bytes((uint8_t*)out, len); } +static void parse_fails(const char* text, const char* message) { + calculator_error err = {0}; + assert(calculator_calculator_parse(STR(text), &err) == 0); + assert(err.code == calculator_calculator_ParseError_NotANumber); + assert(MSG_EQ(err, message)); + calculator_calculator_ParseError_NotANumber_payload payload; + assert(calculator_calculator_ParseError_NotANumber_payload_decode(err.payload_ptr, + err.payload_len, &payload)); + assert(bytes_eq((const uint8_t*)payload.text.ptr, payload.text.len, text)); + calculator_calculator_ParseError_NotANumber_payload_free(&payload); + calculator_error_clear(&err); +} + +static void running_total(const int32_t* values, size_t n, const int32_t* expected) { + calculator_error err = {0}; + size_t len = 99; + int32_t* out = calculator_calculator_running_total(values, n, &len, &err); + assert(err.code == 0 && len == n); + assert(n == 0 || memcmp(out, expected, n * sizeof(int32_t)) == 0); + assert(((uintptr_t)out % 8) == 0 && "producer runs are 8-aligned"); + calculator_free_bytes((uint8_t*)out, len * sizeof(int32_t)); +} + int main(void) { calculator_error err = {0}; @@ -35,10 +60,37 @@ int main(void) { assert(calculator_calculator_divide(1, 0, &err) == 0); assert(err.code == calculator_calculator_CalcError_DivisionByZero); - assert(strcmp(err.message, "division by zero") == 0); + assert(MSG_EQ(err, "division by zero")); assert(err.payload_ptr == NULL && err.payload_len == 0); calculator_error_clear(&err); - assert(err.code == 0 && err.message == NULL); + assert(err.code == 0 && err.message_ptr == NULL && err.message_len == 0); + + // A second domain whose code carries a payload. Its code (1) equals + // CalcError's: a code means something only within the callable's domain. + assert(calculator_calculator_parse(STR("42"), &err) == 42 && err.code == 0); + assert(calculator_calculator_parse(STR(" 42 "), &err) == 42 && err.code == 0); + parse_fails("4x", "not a number: 4x"); + parse_fails("", "not a number: "); + + // `throws: any`: code -1 and a message, no payload. + assert(calculator_calculator_sqrt(9.0, &err) == 3.0 && err.code == 0); + assert(calculator_calculator_sqrt(-4.0, &err) == 0.0); + assert(err.code == -1 && MSG_EQ(err, "cannot take the square root of -4")); + assert(err.payload_ptr == NULL); + calculator_error_clear(&err); + + // OptDirect return, Slice parameter. + const double xs[3] = {1.0, 2.0, 6.0}; + double mean = -1.0; + assert(calculator_calculator_mean(xs, 3, &mean, &err) && err.code == 0 && mean == 3.0); + assert(!calculator_calculator_mean(NULL, 0, &mean, &err) && err.code == 0); + + // Slice parameter and return. + const int32_t ints[4] = {1, 2, 3, INT32_MAX}; + const int32_t totals[4] = {1, 3, 6, -2147483643}; + running_total(ints, 3, totals); + running_total(ints, 4, totals); + running_total(NULL, 0, NULL); greet(STR("World"), STR("Hello, World!")); greet(NULL, 0, STR("Hello, !")); @@ -49,6 +101,12 @@ int main(void) { size_t len = 0; const uint8_t bad_utf8[2] = {0xC3, 0x28}; assert(calculator_calculator_greet(BYTES(bad_utf8), &len, &err) == NULL); + assert(err.code == -3 && err.message_len > 0); + calculator_error_clear(&err); + + // A typed array that isn't aligned for its element type is -3 too. + _Alignas(8) uint8_t raw[16] = {0}; + assert(!calculator_calculator_mean((const double*)(const void*)(raw + 1), 1, &mean, &err)); assert(err.code == -3); calculator_error_clear(&err); diff --git a/conformance/c/codec.c b/conformance/c/codec.c index 32019734..1ed92e25 100644 --- a/conformance/c/codec.c +++ b/conformance/c/codec.c @@ -1,4 +1,4 @@ -// Conformance consumer: codec sample, C target (ABI revision 4). +// Conformance consumer: codec sample, C target (ABI revision 5). // // The shared-vector loop: for every vector the producer serves, decode it // with the generated `codec_buffer.h`, re-encode it (which must reproduce the @@ -8,8 +8,10 @@ // bug can't hide), spot checks of decoded fields, the typed out-of-range // error and its payload, malformed buffers (truncated, unknown tag, trailing // bytes, a bad bool, a duplicate map key) rejected as marshalling failures, -// and object identity and reference counting through buffers. Ends by -// asserting the producer's leak counters are zero. +// object identity and reference counting through buffers, and the ABI 5 +// shapes: optional scalars (OptDirect) and typed arrays (Slice) in both +// directions and as iterator items, a `usize`, a `char`, and a custom type. +// Ends by asserting the producer's leak counters are zero. #include "harness.h" @@ -309,7 +311,7 @@ static void out_of_range(uint32_t n) { assert(err.code == codec_codec_CodecError_OutOfRange); char expected[64]; snprintf(expected, sizeof expected, "vector %u is out of range (count %u)", n, n); - assert(strcmp(err.message, expected) == 0); + assert(MSG_EQ(err, expected)); codec_codec_CodecError_OutOfRange_payload p; assert(codec_codec_CodecError_OutOfRange_payload_decode(err.payload_ptr, err.payload_len, &p)); @@ -509,9 +511,142 @@ static void objects(uint32_t n) { codec_codec_Token_destroy(NULL); } +static void opt_scalars(void) { + codec_error err = {0}; + int32_t i = 7; + assert(!codec_codec_echo_opt_i32(false, 0, &i, &err) && err.code == 0 && i == 0); + assert(codec_codec_echo_opt_i32(true, INT32_MIN, &i, &err) && i == INT32_MIN); + assert(codec_codec_echo_opt_i32(true, 0, &i, &err) && i == 0); + + double d = 1.0; + assert(codec_codec_echo_opt_f64(true, -0.0, &d, &err) && d == 0.0 && signbit(d)); + assert(codec_codec_echo_opt_f64(true, NAN, &d, &err) && isnan(d)); + assert(!codec_codec_echo_opt_f64(false, 0.0, &d, &err)); + + bool flag = false; + assert(codec_codec_echo_opt_bool(true, true, &flag, &err) && flag); + assert(codec_codec_echo_opt_bool(true, false, &flag, &err) && !flag); + assert(!codec_codec_echo_opt_bool(false, false, &flag, &err)); + + codec_codec_Color c = 0; + assert(codec_codec_echo_opt_color(true, codec_codec_Color_Infrared, &c, &err) && + c == codec_codec_Color_Infrared); + assert(codec_codec_echo_opt_color(true, codec_codec_Color_Blue, &c, &err) && + c == codec_codec_Color_Blue); + assert(!codec_codec_echo_opt_color(false, 0, &c, &err) && err.code == 0); + // A present value is still checked; an absent one is ignored. + assert(!codec_codec_echo_opt_color(true, 3, &c, &err) && err.code == -3); + codec_error_clear(&err); + assert(!codec_codec_echo_opt_color(false, 3, &c, &err) && err.code == 0); +} + +static void slices(void) { + codec_error err = {0}; + size_t len = 0; + + uint64_t nan_bits = 0x7ff8000000000001ull; + double f64s[4] = {0.0, -0.0, 5e-324, INFINITY}; + memcpy(&f64s[0], &nan_bits, 8); + double* d = codec_codec_echo_f64s(f64s, 4, &len, &err); + assert(err.code == 0 && len == 4 && ((uintptr_t)d % 8) == 0); + assert(isnan(d[0]) && memcmp(&d[1], &f64s[1], 3 * sizeof(double)) == 0); + codec_free_bytes((uint8_t*)d, len * sizeof(double)); + + const int32_t i32s[3] = {INT32_MIN, 0, INT32_MAX}; + int32_t* i = codec_codec_echo_i32s(i32s, 3, &len, &err); + assert(err.code == 0 && len == 3 && memcmp(i, i32s, sizeof i32s) == 0); + codec_free_bytes((uint8_t*)i, len * sizeof(int32_t)); + assert(codec_codec_echo_i32s(NULL, 0, &len, &err) == NULL && len == 0 && err.code == 0); + + const uint64_t u64s[2] = {UINT64_MAX, 1ull << 63}; + uint64_t* u = codec_codec_echo_u64s(u64s, 2, &len, &err); + assert(err.code == 0 && len == 2 && memcmp(u, u64s, sizeof u64s) == 0); + codec_free_bytes((uint8_t*)u, len * sizeof(uint64_t)); + assert(codec_codec_echo_u64s(NULL, 0, &len, &err) == NULL && len == 0); + + // A null array with a length, and one not aligned for its element type. + assert(codec_codec_echo_u64s(NULL, 2, &len, &err) == NULL && err.code == -3); + codec_error_clear(&err); + _Alignas(8) uint8_t raw[24] = {0}; + const uint64_t* misaligned = (const uint64_t*)(const void*)(raw + 4); + assert(codec_codec_echo_u64s(misaligned, 2, &len, &err) == NULL && err.code == -3); + codec_error_clear(&err); +} + +// Pull every chunk of `chunks(values, size)`, checking it against `expected` +// (`counts[k]` elements each, laid out back to back). +static void chunks(const int32_t* values, size_t n, uint32_t size, const int32_t* expected, + const size_t* counts, size_t chunk_count) { + codec_error err = {0}; + codec_codec_ChunksIterator* it = codec_codec_chunks(values, n, size, &err); + assert(err.code == 0 && it != NULL); + size_t seen = 0, offset = 0; + for (;;) { + int32_t* item = NULL; + size_t len = 0; + int32_t more = codec_codec_ChunksIterator_next(it, &item, &len, &err); + assert(err.code == 0); + if (!more) break; + assert(seen < chunk_count && len == counts[seen]); + assert(memcmp(item, expected + offset, len * sizeof(int32_t)) == 0); + codec_free_bytes((uint8_t*)item, len * sizeof(int32_t)); + offset += len; + seen++; + } + assert(seen == chunk_count); + codec_codec_ChunksIterator_destroy(it); +} + +static void iterators(void) { + const int32_t extremes[3] = {INT32_MIN, 0, INT32_MAX}; + const size_t two_one[2] = {2, 1}; + chunks(extremes, 3, 2, extremes, two_one, 2); + const int32_t four[4] = {1, 2, 3, 4}; + const size_t two_two[2] = {2, 2}; + chunks(four, 4, 2, four, two_two, 2); + chunks(four, 2, 0, NULL, NULL, 0); + chunks(NULL, 0, 3, NULL, NULL, 0); +} + +// Echo a string through `f`, expecting `expected` (or, when `expected` is +// NULL, a -3 with `message`). +typedef const uint8_t* (*echo_fn)(const uint8_t*, size_t, size_t*, codec_error*); +static void echo_text(echo_fn f, const char* in, const char* expected, const char* message) { + codec_error err = {0}; + size_t len = 0; + const uint8_t* out = f(STR(in), &len, &err); + if (expected != NULL) { + assert(err.code == 0 && bytes_eq(out, len, expected)); + codec_free_bytes((uint8_t*)out, len); + } else { + assert(out == NULL && err.code == -3 && MSG_EQ(err, message)); + codec_error_clear(&err); + } +} + +static void conversions(void) { + codec_error err = {0}; + assert(codec_codec_echo_usize(4294967295u, &err) == 4294967295u && err.code == 0); + assert(codec_codec_echo_usize(UINT64_MAX, &err) == UINT64_MAX && err.code == 0); + + echo_text(codec_codec_echo_char, "\xf0\x9f\xa6\x80", "\xf0\x9f\xa6\x80", NULL); + echo_text(codec_codec_echo_char, "\xc3\xa9", "\xc3\xa9", NULL); + echo_text(codec_codec_echo_char, "a", "a", NULL); + echo_text(codec_codec_echo_char, "ab", NULL, "value: \"ab\" is not a valid char"); + echo_text(codec_codec_echo_char, "", NULL, "value: \"\" is not a valid char"); + + echo_text(codec_codec_echo_hex, "ff", "ff", NULL); + echo_text(codec_codec_echo_hex, "00FF", "ff", NULL); + echo_text(codec_codec_echo_hex, "0", "0", NULL); + echo_text(codec_codec_echo_hex, "xyz", NULL, "value: invalid digit found in string"); + echo_text(codec_codec_echo_hex, "", NULL, "value: cannot parse integer from empty string"); + echo_text(codec_codec_echo_hex, "100000000", NULL, + "value: number too large to fit in target type"); +} + int main(void) { codec_error err = {0}; - assert(CODEC_ABI_VERSION == 4u && codec_abi_version() == CODEC_ABI_VERSION); + assert(CODEC_ABI_VERSION == 5u && codec_abi_version() == CODEC_ABI_VERSION); assert(codec_codec_contract_check() == 0); assert(codec_debug_live(-1) == 1); @@ -524,6 +659,10 @@ int main(void) { out_of_range(n); malformed(); objects(n); + opt_scalars(); + slices(); + iterators(); + conversions(); ASSERT_NO_LEAKS(codec_debug_live); printf("c/codec: OK (%u vectors)\n", n); diff --git a/conformance/c/harness.h b/conformance/c/harness.h index 10441087..dcedc221 100644 --- a/conformance/c/harness.h +++ b/conformance/c/harness.h @@ -1,6 +1,7 @@ -// Shared helpers for the C conformance consumers: always-on assertions, -// string-slot shorthands for the ABI's (ptr, len) strings, and the leak -// check every consumer runs before exiting. +// Shared helpers for the C conformance consumers (C ABI revision 5): +// always-on assertions, string-slot shorthands for the ABI's (ptr, len) +// strings and error messages, and the leak check every consumer runs before +// exiting. #ifndef CONFORMANCE_HARNESS_H #define CONFORMANCE_HARNESS_H @@ -33,6 +34,10 @@ static inline int bytes_eq(const uint8_t* ptr, size_t len, const char* s) { return len == n && (n == 0 || memcmp(ptr, s, n) == 0); } +// `true` when an error struct's length-delimited UTF-8 message (ABI 5: not +// NUL-terminated; NULL with length 0 is the empty message) spells `s`. +#define MSG_EQ(err, s) bytes_eq((err).message_ptr, (err).message_len, (s)) + // Assert that every leak counter of a producer built with `leak-check` is // zero: 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned // allocations. A producer worker thread may still be unwinding a finished diff --git a/conformance/c/kvstore.c b/conformance/c/kvstore.c index 3045ed33..f58cbf38 100644 --- a/conformance/c/kvstore.c +++ b/conformance/c/kvstore.c @@ -1,4 +1,4 @@ -// Conformance consumer: kvstore sample, C target (ABI revision 4). +// Conformance consumer: kvstore sample, C target (ABI revision 5). // // Drives the feature-complete producer through its generated header and // `kvstore_buffer.h`: @@ -28,7 +28,14 @@ // launches; // * the nested `kv.stats` module (the parent's `Store` and error domain) // and the sibling `report` root (the shared `Entry` record and its own -// error domain). +// error domain); +// * the ABI 5 shapes: an optional scalar parameter, return, iterator item, +// async result, and callback parameter and return (OptDirect); typed +// arrays as a return, an async result, and a callback parameter and +// return (Slice); a `usize` return; a `throws: any` method; a callback +// failure converted into the domain's `CallbackFailed`; and a +// thread-affine vtable whose value-returning method the producer refuses +// to call off its thread. // // Ends by asserting the producer's leak counters are zero. @@ -84,9 +91,9 @@ static kvstore_kv_Store* open_store(const char* path) { return s; } -static uint32_t count(const kvstore_kv_Store* s) { +static uint64_t count(const kvstore_kv_Store* s) { kvstore_error err = {0}; - uint32_t n = kvstore_kv_Store_count(s, &err); + uint64_t n = kvstore_kv_Store_count(s, &err); assert(err.code == 0); return n; } @@ -111,13 +118,9 @@ static int path_is(const kvstore_kv_Store* s, const char* expected) { static bool try_put(const kvstore_kv_Store* s, const char* key, const char* value, kvstore_kv_EntryKind kind, const int64_t* ttl, kvstore_kv_Entry* out, kvstore_error* err) { - kvstore_writer t; - memset(&t, 0, sizeof t); - kvstore_opt_i64_write(&t, ttl); size_t len = 0; - const uint8_t* buf = kvstore_kv_Store_put(s, STR(key), (const uint8_t*)value, - strlen(value), kind, t.ptr, t.len, &len, err); - kvstore_writer_free(&t); + const uint8_t* buf = kvstore_kv_Store_put(s, STR(key), (const uint8_t*)value, strlen(value), + kind, ttl != NULL, ttl != NULL ? *ttl : 0, &len, err); if (buf == NULL) { assert(err->code != 0); return false; @@ -157,6 +160,21 @@ static void expect_key_not_found(kvstore_error* err, const char* key) { kvstore_error_clear(err); } +// `CallbackFailed` (a consumer callback's failure, converted by the sample), +// with the consumer's message when `message` isn't NULL. +static void expect_callback_failed(kvstore_error* err, const char* message) { + assert(err->code == kvstore_kv_KvError_CallbackFailed); + kvstore_kv_KvError_CallbackFailed_payload p; + assert(kvstore_kv_KvError_CallbackFailed_payload_decode(err->payload_ptr, err->payload_len, + &p)); + assert(p.message.len > 0); + if (message != NULL) { + assert(MSG_EQ(*err, message) && str_is(p.message, message)); + } + kvstore_kv_KvError_CallbackFailed_payload_free(&p); + kvstore_error_clear(err); +} + // ── listener (consumer-implemented, retained) ────────────────────────────── typedef struct { @@ -172,12 +190,15 @@ typedef struct { } listener_ctx; static atomic_int g_listeners_freed; +// Every listener's calls made off the main thread, accepts and on_change. +static atomic_int g_listener_calls_off_main; static bool listener_accepts(void* ctx, const uint8_t* key_ptr, size_t key_len, kvstore_error* out_err) { listener_ctx* l = (listener_ctx*)ctx; + if (!pthread_equal(pthread_self(), g_main_thread)) atomic_fetch_add(&g_listener_calls_off_main, 1); if (l->fail_on != NULL && bytes_eq(key_ptr, key_len, l->fail_on)) { - kvstore_error_set(out_err, -4, "listener refused"); + kvstore_error_set(out_err, -1, STR("listener refused")); return false; } return l->skip == NULL || !bytes_eq(key_ptr, key_len, l->skip); @@ -188,7 +209,10 @@ static void listener_on_change(void* ctx, const uint8_t* change_ptr, size_t chan kvstore_error* out_err) { (void)out_err; listener_ctx* l = (listener_ctx*)ctx; - if (!pthread_equal(pthread_self(), g_main_thread)) atomic_fetch_add(&l->off_main_thread, 1); + if (!pthread_equal(pthread_self(), g_main_thread)) { + atomic_fetch_add(&l->off_main_thread, 1); + atomic_fetch_add(&g_listener_calls_off_main, 1); + } kvstore_kv_Change c; assert(kvstore_kv_Change_decode(change_ptr, change_len, &c)); switch (c.tag) { @@ -219,6 +243,14 @@ static const kvstore_kv_Listener_vtable LISTENER_VTABLE = { sizeof(kvstore_kv_Listener_vtable), 0, listener_free, listener_accepts, listener_on_change, }; +// The same listener, declared thread-affine: the producer calls `accepts` +// (which returns a value) only on the thread that subscribed it, and fails +// such a call from any other thread with -4 instead of making it. +static const kvstore_kv_Listener_vtable AFFINE_LISTENER_VTABLE = { + sizeof(kvstore_kv_Listener_vtable), KVSTORE_VTABLE_THREAD_AFFINE, listener_free, + listener_accepts, listener_on_change, +}; + static listener_ctx* new_listener(const char* skip, const char* fail_on) { listener_ctx* l = (listener_ctx*)calloc(1, sizeof *l); assert(l != NULL); @@ -236,6 +268,25 @@ typedef struct { static atomic_int g_policies_freed; +// ttl_for: an optional scalar parameter and return. "short" lives one tick, +// "forever" never expires, "ttl-fail" fails with a typed error, and every +// other key keeps the requested TTL. +static bool policy_ttl_for(void* ctx, const uint8_t* key_ptr, size_t key_len, bool has_requested, + int64_t requested, int64_t* out_value, kvstore_error* out_err) { + (void)ctx; + if (bytes_eq(key_ptr, key_len, "short")) { + *out_value = 1; + return true; + } + if (bytes_eq(key_ptr, key_len, "forever")) return false; + if (bytes_eq(key_ptr, key_len, "ttl-fail")) { + kvstore_error_set(out_err, kvstore_kv_KvError_InvalidPath, STR("no ttl for you")); + return false; + } + *out_value = requested; + return has_requested; +} + static void policy_admit(void* ctx, const uint8_t* entry_ptr, size_t entry_len, uint8_t** out_ptr, size_t* out_len, kvstore_error* out_err) { policy_ctx* p = (policy_ctx*)ctx; @@ -247,7 +298,7 @@ static void policy_admit(void* ctx, const uint8_t* entry_ptr, size_t entry_len, memset(&w, 0, sizeof w); if (starts_with(e.key, "secret")) { // A typed domain error with its fields as the payload. - kvstore_error_set(out_err, kvstore_kv_KvError_Rejected, "secrets are not stored"); + kvstore_error_set(out_err, kvstore_kv_KvError_Rejected, STR("secrets are not stored")); kvstore_kv_KvError_Rejected_payload r; r.key = e.key; r.reason = kvstore_str_of("no secrets"); @@ -255,7 +306,7 @@ static void policy_admit(void* ctx, const uint8_t* entry_ptr, size_t entry_len, kvstore_error_set_payload(out_err, w.ptr, w.len); kvstore_writer_free(&w); } else if (starts_with(e.key, "boom")) { - kvstore_error_set(out_err, 4242, "policy exploded"); // not a domain code: -4 + kvstore_error_set(out_err, 4242, STR("policy exploded")); // not a domain code } else if (starts_with(e.key, "garbage")) { hand_over_text("?", out_ptr, out_len); // not an Entry: -3 } else { @@ -302,7 +353,7 @@ static void policy_free(void* ctx) { } static const kvstore_kv_Policy_vtable POLICY_VTABLE = { - sizeof(kvstore_kv_Policy_vtable), 0, policy_free, policy_admit, policy_route, + sizeof(kvstore_kv_Policy_vtable), 0, policy_free, policy_ttl_for, policy_admit, policy_route, }; // ── loader (consumer-implemented, passed as an optional parameter) ───────── @@ -330,7 +381,7 @@ static kvstore_kv_Store* loader_fallback(void* ctx, const uint8_t* key_ptr, size } static void fail_not_found(kvstore_error* out_err, const char* key) { - kvstore_error_set(out_err, kvstore_kv_KvError_KeyNotFound, "not in the loader"); + kvstore_error_set(out_err, kvstore_kv_KvError_KeyNotFound, STR("not in the loader")); kvstore_kv_KvError_KeyNotFound_payload p; p.key = kvstore_str_of(key); kvstore_writer w; @@ -352,7 +403,7 @@ static void loader_load(void* ctx, const uint8_t* key_ptr, size_t key_len, uint8 return; } if (bytes_eq(key_ptr, key_len, "broken")) { - kvstore_error_set(out_err, -1, "loader is broken"); + kvstore_error_set(out_err, -1, STR("loader is broken")); return; } char value[80]; @@ -389,7 +440,7 @@ static bool get_or_load(const kvstore_kv_Store* s, const char* key, loader_ctx* assert(buf == NULL); return false; } - assert(kvstore_opt_kv_Entry_decode(buf, len, out)); + assert(kvstore_opt_Entry_decode(buf, len, out)); kvstore_free_bytes((uint8_t*)buf, len); return true; } @@ -408,8 +459,9 @@ typedef struct { static void settle(call_state* c, kvstore_error* err) { c->code = err ? err->code : 0; - if (err != NULL && err->message != NULL) { - snprintf(c->message, sizeof c->message, "%s", err->message); + if (err != NULL && err->message_ptr != NULL) { + snprintf(c->message, sizeof c->message, "%.*s", (int)err->message_len, + (const char*)err->message_ptr); } kvstore_error_free(err); } @@ -454,7 +506,7 @@ static void constructors(void) { // Fallible constructor: an empty path is InvalidPath (no payload). assert(kvstore_kv_Store_open(STR(""), &err) == NULL); assert(err.code == kvstore_kv_KvError_InvalidPath); - assert(strcmp(err.message, "invalid path") == 0); + assert(MSG_EQ(err, "invalid path")); assert(err.payload_ptr == NULL && err.payload_len == 0); kvstore_error_clear(&err); // A NULL string with a nonzero length is a marshalling failure. @@ -506,17 +558,17 @@ static void basics(void) { assert(e.value.len == 3 && memcmp(e.value.ptr, "two", 3) == 0); kvstore_kv_Entry_free(&e); assert(kvstore_kv_Store_get(s, STR("nope"), &len, &err) == NULL); - assert(strcmp(err.message, "key not found: nope") == 0); + assert(MSG_EQ(err, "key not found: nope")); expect_key_not_found(&err, "nope"); buf = kvstore_kv_Store_find(s, STR("alpha"), &len, &err); kvstore_kv_Entry* found = NULL; - assert(err.code == 0 && kvstore_opt_kv_Entry_decode(buf, len, &found)); + assert(err.code == 0 && kvstore_opt_Entry_decode(buf, len, &found)); kvstore_free_bytes((uint8_t*)buf, len); assert(found != NULL && found->version == 2); - kvstore_opt_kv_Entry_free(&found); + kvstore_opt_Entry_free(&found); buf = kvstore_kv_Store_find(s, STR("nope"), &len, &err); - assert(err.code == 0 && kvstore_opt_kv_Entry_decode(buf, len, &found) && found == NULL); + assert(err.code == 0 && kvstore_opt_Entry_decode(buf, len, &found) && found == NULL); kvstore_free_bytes((uint8_t*)buf, len); // TTLs follow the logical clock; an expired get reports when. @@ -553,14 +605,11 @@ static void basics(void) { assert(!try_put(s, "k", "v", (kvstore_kv_EntryKind)9, NULL, NULL, &err)); assert(err.code == -3); kvstore_error_clear(&err); - kvstore_writer none = prefix_buf(NULL); const uint8_t bad_utf8[2] = {0xC3, 0x28}; assert(kvstore_kv_Store_put(s, BYTES(bad_utf8), BYTES(bad_utf8), - kvstore_kv_EntryKind_Volatile, none.ptr, none.len, &len, - &err) == NULL); + kvstore_kv_EntryKind_Volatile, false, 0, &len, &err) == NULL); assert(err.code == -3); kvstore_error_clear(&err); - kvstore_writer_free(&none); // A method on a null receiver too. kvstore_kv_Store_count(NULL, &err); assert(err.code == -3); @@ -715,12 +764,34 @@ static void listeners(void) { kvstore_error_clear(&err); assert(atomic_load(&g_listeners_freed) == 3); + // A thread-affine listener: compaction notifies from a producer thread, + // where the producer refuses to call `accepts` (it returns a value), so + // the listener fails (-4, never called) and is detached. A synchronous + // put on this thread still reaches it. + listener_ctx* affine = new_listener(NULL, NULL); + kvstore_kv_Store_subscribe(s, affine, &AFFINE_LISTENER_VTABLE, &err); + assert(err.code == 0 && kvstore_kv_Store_listener_count(s, &err) == 1); + const int64_t one = 1; + assert(try_put(s, "brief", "x", kvstore_kv_EntryKind_Volatile, &one, NULL, &err)); + assert(atomic_load(&affine->puts) == 1); + kvstore_kv_Store_tick(s, 1, &err); + int off_main = atomic_load(&g_listener_calls_off_main); + call_state c; + memset(&c, 0, sizeof c); + kvstore_kv_Store_compact(s, 0, NULL, on_u32, &c); + wait_for(&c); + assert(c.code == 0 && c.u32 == 1); + // Detached and released without one call off this thread. + assert(atomic_load(&g_listener_calls_off_main) == off_main); + assert(kvstore_kv_Store_listener_count(s, &err) == 0); + assert(atomic_load(&g_listeners_freed) == 4); + // Destroying the store releases the listeners it still holds. kvstore_kv_Store_subscribe(s, new_listener(NULL, NULL), &LISTENER_VTABLE, &err); kvstore_kv_Store_subscribe(s, new_listener(NULL, NULL), &LISTENER_VTABLE, &err); assert(kvstore_kv_Store_listener_count(s, &err) == 2); kvstore_kv_Store_destroy(s); - assert(atomic_load(&g_listeners_freed) == 5); + assert(atomic_load(&g_listeners_freed) == 6); } static void policies(void) { @@ -745,31 +816,47 @@ static void policies(void) { assert(count(s) == 1 && count(other) == 1); // A typed error from the throwing callback reaches the caller with its - // code, message, and payload. + // code and payload, and the domain's own message for them. assert(!try_put(s, "secret", "3", kvstore_kv_EntryKind_Volatile, NULL, NULL, &err)); assert(err.code == kvstore_kv_KvError_Rejected); - assert(strcmp(err.message, "secrets are not stored") == 0); + assert(MSG_EQ(err, "write to secret rejected: no secrets")); kvstore_kv_KvError_Rejected_payload r; assert(kvstore_kv_KvError_Rejected_payload_decode(err.payload_ptr, err.payload_len, &r)); assert(str_is(r.key, "secret") && str_is(r.reason, "no secrets")); kvstore_kv_KvError_Rejected_payload_free(&r); kvstore_error_clear(&err); - // A code outside the domain arrives as -4 with the consumer's message. + // A code outside the domain reaches the producer as a callback failure, + // which the sample turns into `CallbackFailed` with the consumer's + // message. assert(!try_put(s, "boom", "4", kvstore_kv_EntryKind_Volatile, NULL, NULL, &err)); - assert(err.code == -4 && strcmp(err.message, "policy exploded") == 0); - kvstore_error_clear(&err); - // A return the runtime can't accept is -3: a malformed record... + expect_callback_failed(&err, "policy exploded"); + // So is a return the runtime can't accept: a malformed record... assert(!try_put(s, "garbage", "5", kvstore_kv_EntryKind_Volatile, NULL, NULL, &err)); - assert(err.code == -3); - kvstore_error_clear(&err); + expect_callback_failed(&err, NULL); // ...or a null required object. assert(!try_put(s, "null/x", "6", kvstore_kv_EntryKind_Volatile, NULL, NULL, &err)); - assert(err.code == -3); - kvstore_error_clear(&err); + expect_callback_failed(&err, NULL); assert(count(s) == 1 && count(other) == 1); assert(atomic_load(&p->admitted) == 6); + // ttl_for: an optional scalar in and out, consulted before admit. + kvstore_kv_Entry e2; + const int64_t five = 5; + assert(try_put(s, "short", "x", kvstore_kv_EntryKind_Volatile, NULL, &e2, &err)); + assert(e2.expires_at != NULL && *e2.expires_at == 1); + kvstore_kv_Entry_free(&e2); + assert(try_put(s, "forever", "x", kvstore_kv_EntryKind_Volatile, &five, &e2, &err)); + assert(e2.expires_at == NULL); + kvstore_kv_Entry_free(&e2); + assert(try_put(s, "kept", "x", kvstore_kv_EntryKind_Volatile, &five, &e2, &err)); + assert(e2.expires_at != NULL && *e2.expires_at == 5); + kvstore_kv_Entry_free(&e2); + assert(!try_put(s, "ttl-fail", "x", kvstore_kv_EntryKind_Volatile, NULL, NULL, &err)); + assert(err.code == kvstore_kv_KvError_InvalidPath && MSG_EQ(err, "invalid path")); + kvstore_error_clear(&err); + assert(count(s) == 4 && atomic_load(&p->admitted) == 9); + // Replacing the policy releases the old one; NULL removes it. policy_ctx* p2 = (policy_ctx*)calloc(1, sizeof *p2); p2->other = kvstore_kv_Store_clone(other); @@ -779,7 +866,7 @@ static void policies(void) { assert(err.code == 0 && atomic_load(&g_policies_freed) == 2); assert(!kvstore_kv_Store_has_policy(s, &err)); put(s, "secret", "now allowed"); - assert(count(s) == 2); + assert(count(s) == 5); kvstore_kv_Store_destroy(other); kvstore_kv_Store_destroy(s); @@ -798,19 +885,19 @@ static void loaders(void) { assert(e->value.len == 8 && memcmp(e->value.ptr, "loaded:k", 8) == 0); assert(e->kind == kvstore_kv_EntryKind_Volatile && e->metadata.len == 1); assert(str_is(e->metadata.keys[0], "source") && str_is(e->metadata.values[0], "c-loader")); - kvstore_opt_kv_Entry_free(&e); + kvstore_opt_Entry_free(&e); assert(atomic_load(&g_loaders_freed) == 1 && "a loader is released after the call"); assert(count(s) == 1); // A hit doesn't consult the loader. assert(get_or_load(s, "k", new_loader(NULL), &e, &err) && e != NULL && e->version == 1); - kvstore_opt_kv_Entry_free(&e); + kvstore_opt_Entry_free(&e); // The fallback store (an optional object return) is consulted first. kvstore_kv_Store* backup = open_store("/backup"); put(backup, "fb", "from backup"); assert(get_or_load(s, "fb", new_loader(kvstore_kv_Store_clone(backup)), &e, &err)); assert(e != NULL && e->value.len == 11 && e->kind == kvstore_kv_EntryKind_Persistent); - kvstore_opt_kv_Entry_free(&e); + kvstore_opt_Entry_free(&e); kvstore_kv_Store_destroy(backup); // KeyNotFound for this key: the producer decoded the payload and @@ -818,12 +905,11 @@ static void loaders(void) { assert(get_or_load(s, "missing", new_loader(NULL), &e, &err) && e == NULL); // KeyNotFound for another key: passed through, payload intact. assert(!get_or_load(s, "elsewhere", new_loader(NULL), &e, &err)); - assert(strcmp(err.message, "not in the loader") == 0); + assert(MSG_EQ(err, "key not found: other")); expect_key_not_found(&err, "other"); - // Any other failure is -4. + // Any other failure is `CallbackFailed` with the loader's message. assert(!get_or_load(s, "broken", new_loader(NULL), &e, &err)); - assert(err.code == -4 && strcmp(err.message, "loader is broken") == 0); - kvstore_error_clear(&err); + expect_callback_failed(&err, "loader is broken"); assert(atomic_load(&g_loaders_freed) == 6); kvstore_kv_Store_destroy(s); @@ -897,11 +983,11 @@ static void async_calls(void) { for (int i = 0; i < CONCURRENT; i++) { wait_for(&many[i]); assert(many[i].code == 0); - kvstore_list_opt_kv_Entry got; - assert(kvstore_list_opt_kv_Entry_decode(many[i].buf, many[i].len, &got)); + kvstore_list_opt_Entry got; + assert(kvstore_list_opt_Entry_decode(many[i].buf, many[i].len, &got)); assert(got.len == 3 && got.items[0] != NULL && got.items[1] == NULL); assert(got.items[2] != NULL && str_is(got.items[2]->key, "keep")); - kvstore_list_opt_kv_Entry_free(&got); + kvstore_list_opt_Entry_free(&got); free(many[i].buf); } @@ -910,11 +996,11 @@ static void async_calls(void) { kvstore_kv_Store* other = open_store("/other"); put(other, "a", "123"); kvstore_kv_Store* both[2] = {s, other}; - kvstore_list_kv_Store sl; + kvstore_list_Store sl; sl.items = both; sl.len = 2; memset(&w, 0, sizeof w); - kvstore_list_kv_Store_write(&w, &sl); + kvstore_list_Store_write(&w, &sl); memset(&c, 0, sizeof c); kvstore_kv_stats_summarize_all(w.ptr, w.len, on_buffer, &c); kvstore_writer_free(&w); @@ -993,8 +1079,8 @@ static void object_graph(void) { kvstore_list_string_write(&w, &pl); const uint8_t* buf = kvstore_kv_Store_open_many(w.ptr, w.len, &len, &err); kvstore_writer_free(&w); - kvstore_list_kv_Store many; - assert(err.code == 0 && kvstore_list_kv_Store_decode(buf, len, &many)); + kvstore_list_Store many; + assert(err.code == 0 && kvstore_list_Store_decode(buf, len, &many)); kvstore_free_bytes((uint8_t*)buf, len); assert(many.len == 2 && path_is(many.items[0], "/a") && path_is(many.items[1], "/b")); paths[1] = kvstore_str_of(""); @@ -1012,15 +1098,15 @@ static void object_graph(void) { infos[1].store = many.items[0]; infos[1].mirror = NULL; infos[1].count = 0; - kvstore_list_kv_StoreInfo il; + kvstore_list_StoreInfo il; il.items = infos; il.len = 2; memset(&w, 0, sizeof w); - kvstore_list_kv_StoreInfo_write(&w, &il); + kvstore_list_StoreInfo_write(&w, &il); buf = kvstore_kv_Store_by_label(w.ptr, w.len, &len, &err); kvstore_writer_free(&w); - kvstore_map_string_kv_Store named; - assert(err.code == 0 && kvstore_map_string_kv_Store_decode(buf, len, &named)); + kvstore_map_string_Store named; + assert(err.code == 0 && kvstore_map_string_Store_decode(buf, len, &named)); kvstore_free_bytes((uint8_t*)buf, len); assert(named.len == 2); for (size_t i = 0; i < named.len; i++) { @@ -1032,16 +1118,16 @@ static void object_graph(void) { // objects (each written as a fresh reference the producer adopts). put(many.items[0], "m", "1"); kvstore_kv_Store* three[3] = {many.items[0], many.items[1], fork}; - kvstore_list_kv_Store stores; + kvstore_list_Store stores; stores.items = three; stores.len = 3; kvstore_writer ws, wn, we; memset(&ws, 0, sizeof ws); memset(&wn, 0, sizeof wn); memset(&we, 0, sizeof we); - kvstore_list_kv_Store_write(&ws, &stores); - kvstore_map_string_kv_Store_write(&wn, &named); - kvstore_opt_kv_StoreInfo_write(&we, &info); + kvstore_list_Store_write(&ws, &stores); + kvstore_map_string_Store_write(&wn, &named); + kvstore_opt_StoreInfo_write(&we, &info); // stores: 1 + 0 + 2; named: main 1 + first 1; extra: main 1. assert(kvstore_kv_Store_total_count(ws.ptr, ws.len, wn.ptr, wn.len, we.ptr, we.len, &err) == 6); @@ -1054,9 +1140,9 @@ static void object_graph(void) { memset(&ws, 0, sizeof ws); memset(&wn, 0, sizeof wn); memset(&we, 0, sizeof we); - kvstore_list_kv_Store_write(&ws, &stores); - kvstore_map_string_kv_Store_write(&wn, &named); - kvstore_opt_kv_StoreInfo_write(&we, NULL); + kvstore_list_Store_write(&ws, &stores); + kvstore_map_string_Store_write(&wn, &named); + kvstore_opt_StoreInfo_write(&we, NULL); assert(kvstore_kv_Store_total_count(ws.ptr, ws.len, wn.ptr, wn.len, we.ptr, we.len, &err) == 5); kvstore_writer_free(&ws); @@ -1065,9 +1151,9 @@ static void object_graph(void) { // Everything we hold is still intact; release each reference once. assert(count(s) == 1 && count(fork) == 2 && count(many.items[0]) == 1); - kvstore_map_string_kv_Store_free(&named); + kvstore_map_string_Store_free(&named); kvstore_kv_StoreInfo_free(&info); - kvstore_list_kv_Store_free(&many); + kvstore_list_Store_free(&many); kvstore_kv_Store_destroy(empty); kvstore_kv_Store_destroy(fork); kvstore_kv_Store_destroy(s); @@ -1114,11 +1200,11 @@ static void stats_and_report(void) { } kvstore_kv_Store_EntriesIterator_destroy(it); assert(n == 3); - kvstore_list_kv_Entry el; + kvstore_list_Entry el; el.items = entries; el.len = n; memset(&w, 0, sizeof w); - kvstore_list_kv_Entry_write(&w, &el); + kvstore_list_Entry_write(&w, &el); buf = kvstore_report_render_report(w.ptr, w.len, &len, &err); kvstore_writer_free(&w); kvstore_list_string lines; @@ -1133,19 +1219,181 @@ static void stats_and_report(void) { el.len = 0; memset(&w, 0, sizeof w); - kvstore_list_kv_Entry_write(&w, &el); + kvstore_list_Entry_write(&w, &el); assert(kvstore_report_render_report(w.ptr, w.len, &len, &err) == NULL); kvstore_writer_free(&w); assert(err.code == kvstore_report_ReportError_NothingToReport); - assert(strcmp(err.message, "nothing to report") == 0); + assert(MSG_EQ(err, "nothing to report")); kvstore_error_clear(&err); kvstore_kv_Store_destroy(s); } +// ── scorer (consumer-implemented, typed arrays in and out) ───────────────── + +typedef enum { SCORE_SIZES, SCORE_FAIL, SCORE_SHORT } score_mode; + +typedef struct { + score_mode mode; +} scorer_ctx; + +static atomic_int g_scorers_freed; + +// `sizes` is a borrowed typed array; the scores are a run of `count * +// sizeof(double)` bytes from kvstore_alloc, which the producer adopts. +static void scorer_scores(void* ctx, const uint64_t* sizes_ptr, size_t sizes_len, + double** out_ptr, size_t* out_len, kvstore_error* out_err) { + scorer_ctx* sc = (scorer_ctx*)ctx; + assert(((uintptr_t)sizes_ptr % 8) == 0 && "a borrowed typed array is aligned"); + if (sc->mode == SCORE_SIZES) { + // The store's value sizes in key order: a="1", b="333", c="22". + assert(sizes_len == 3 && sizes_ptr[0] == 1 && sizes_ptr[1] == 3 && sizes_ptr[2] == 2); + } + if (sc->mode == SCORE_FAIL) { + kvstore_error_set(out_err, -1, STR("scorer is out of order")); + return; + } + size_t n = sc->mode == SCORE_SHORT ? 1 : sizes_len; + double* scores = (double*)(void*)kvstore_alloc(n * sizeof(double)); + for (size_t i = 0; i < n; i++) scores[i] = (double)sizes_ptr[i]; + *out_ptr = scores; + *out_len = n; +} + +static void scorer_free(void* ctx) { + free(ctx); + atomic_fetch_add(&g_scorers_freed, 1); +} + +static const kvstore_kv_Scorer_vtable SCORER_VTABLE = { + sizeof(kvstore_kv_Scorer_vtable), 0, scorer_free, scorer_scores, +}; + +static void on_opt_u32(void* context, kvstore_error* err, bool has_result, uint32_t result) { + call_state* c = (call_state*)context; + settle(c, err); + c->u32 = has_result ? result : UINT32_MAX; + atomic_store(&c->done, 1); +} + +// A typed-array async result is an owned run of `count * sizeof(T)` bytes. +static void on_u32s(void* context, kvstore_error* err, const uint32_t* ptr, size_t len) { + call_state* c = (call_state*)context; + settle(c, err); + if (ptr != NULL) { + c->buf = (uint8_t*)malloc(len * sizeof(uint32_t)); + memcpy(c->buf, ptr, len * sizeof(uint32_t)); + c->len = len; + kvstore_free_bytes((uint8_t*)ptr, len * sizeof(uint32_t)); + } + atomic_store(&c->done, 1); +} + +static void abi5_shapes(void) { + kvstore_error err = {0}; + size_t len = 0; + kvstore_kv_Store* s = open_store("/abi5"); + + const int64_t seven = 7; + assert(try_put(s, "b", "12", kvstore_kv_EntryKind_Persistent, NULL, NULL, &err)); + assert(try_put(s, "a", "\x01\x02\x03", kvstore_kv_EntryKind_Volatile, &seven, NULL, &err)); + assert(count(s) == 2); + + // An optional scalar return. + int64_t at = -1; + assert(kvstore_kv_Store_expires_at(s, STR("a"), &at, &err) && at == 7 && err.code == 0); + assert(!kvstore_kv_Store_expires_at(s, STR("b"), &at, &err) && err.code == 0); + assert(!kvstore_kv_Store_expires_at(s, STR("zzz"), &at, &err) && err.code == 0); + + // A typed-array return, in key order. + uint64_t* sizes = kvstore_kv_Store_value_sizes(s, &len, &err); + assert(err.code == 0 && len == 2 && sizes[0] == 3 && sizes[1] == 2); + kvstore_free_bytes((uint8_t*)sizes, len * sizeof(uint64_t)); + + // Optional scalar iterator items. + kvstore_kv_Store_ExpirationsIterator* it = kvstore_kv_Store_expirations(s, &err); + assert(err.code == 0 && it != NULL); + bool has = false; + int64_t item = 0; + assert(kvstore_kv_Store_ExpirationsIterator_next(it, &has, &item, &err) == 1); + assert(has && item == 7); + assert(kvstore_kv_Store_ExpirationsIterator_next(it, &has, &item, &err) == 1); + assert(!has && item == 0); + assert(kvstore_kv_Store_ExpirationsIterator_next(it, &has, &item, &err) == 0); + kvstore_kv_Store_ExpirationsIterator_destroy(it); + + // An optional scalar async result. + call_state c; + memset(&c, 0, sizeof c); + kvstore_kv_Store_version_of(s, STR("a"), on_opt_u32, &c); + wait_for(&c); + assert(c.code == 0 && c.u32 == 1); + memset(&c, 0, sizeof c); + kvstore_kv_Store_version_of(s, STR("q"), on_opt_u32, &c); + wait_for(&c); + assert(c.code == 0 && c.u32 == UINT32_MAX); + + // A typed-array async result. + put(s, "b", "x"); + kvstore_str keys[3] = {kvstore_str_of("b"), kvstore_str_of("q"), kvstore_str_of("a")}; + kvstore_list_string kl = {keys, 3}; + kvstore_writer w; + memset(&w, 0, sizeof w); + kvstore_list_string_write(&w, &kl); + memset(&c, 0, sizeof c); + kvstore_kv_Store_versions(s, w.ptr, w.len, on_u32s, &c); + kvstore_writer_free(&w); + wait_for(&c); + assert(c.code == 0 && c.len == 3); + const uint32_t* versions = (const uint32_t*)(const void*)c.buf; + assert(versions[0] == 2 && versions[1] == 0 && versions[2] == 1); + free(c.buf); + kvstore_kv_Store_destroy(s); + + // A callback taking and returning typed arrays. + s = open_store("/rank"); + put(s, "a", "1"); + put(s, "b", "333"); + put(s, "c", "22"); + kvstore_list_string ranked; + scorer_ctx* sc = (scorer_ctx*)calloc(1, sizeof *sc); + sc->mode = SCORE_SIZES; + const uint8_t* buf = kvstore_kv_Store_rank(s, sc, &SCORER_VTABLE, &len, &err); + assert(err.code == 0 && kvstore_list_string_decode(buf, len, &ranked)); + kvstore_free_bytes((uint8_t*)buf, len); + assert(ranked.len == 3 && str_is(ranked.items[0], "b") && str_is(ranked.items[1], "c")); + assert(str_is(ranked.items[2], "a")); + kvstore_list_string_free(&ranked); + sc = (scorer_ctx*)calloc(1, sizeof *sc); + sc->mode = SCORE_FAIL; + assert(kvstore_kv_Store_rank(s, sc, &SCORER_VTABLE, &len, &err) == NULL); + expect_callback_failed(&err, "scorer is out of order"); + sc = (scorer_ctx*)calloc(1, sizeof *sc); + sc->mode = SCORE_SHORT; + assert(kvstore_kv_Store_rank(s, sc, &SCORER_VTABLE, &len, &err) == NULL); + expect_callback_failed(&err, "expected 3 scores, got 1"); + kvstore_kv_Store_destroy(s); + + // `throws: any` and a `usize` return: code -1 and a message, no payload. + s = open_store("/import"); + assert(kvstore_kv_Store_import_lines(s, STR("a=1\n\nb=two\n"), &err) == 2 && err.code == 0); + buf = kvstore_kv_Store_get(s, STR("b"), &len, &err); + kvstore_kv_Entry e; + assert(err.code == 0 && kvstore_kv_Entry_decode(buf, len, &e)); + kvstore_free_bytes((uint8_t*)buf, len); + assert(e.value.len == 3 && memcmp(e.value.ptr, "two", 3) == 0); + kvstore_kv_Entry_free(&e); + assert(kvstore_kv_Store_import_lines(s, STR("c=3\nbroken\nd=4"), &err) == 0); + assert(err.code == -1 && MSG_EQ(err, "line 2: expected key=value")); + assert(err.payload_ptr == NULL && err.payload_len == 0); + kvstore_error_clear(&err); + assert(count(s) == 3); + kvstore_kv_Store_destroy(s); +} + int main(void) { g_main_thread = pthread_self(); - assert(KVSTORE_ABI_VERSION == 4u && kvstore_abi_version() == KVSTORE_ABI_VERSION); + assert(KVSTORE_ABI_VERSION == 5u && kvstore_abi_version() == KVSTORE_ABI_VERSION); assert(kvstore_kv_contract_check() == 0); assert(kvstore_report_contract_check() == 0); assert(kvstore_debug_live(-1) == 1 && "the sample counts live allocations"); @@ -1159,10 +1407,12 @@ int main(void) { async_calls(); object_graph(); stats_and_report(); + abi5_shapes(); ASSERT_NO_LEAKS(kvstore_debug_live); - assert(atomic_load(&g_listeners_freed) == 6); + assert(atomic_load(&g_listeners_freed) == 7); assert(atomic_load(&g_policies_freed) == 2); + assert(atomic_load(&g_scorers_freed) == 3); printf("c/kvstore: OK\n"); return 0; } diff --git a/conformance/c/producer.c b/conformance/c/producer.c index ff7e238a..d7d15284 100644 --- a/conformance/c/producer.c +++ b/conformance/c/producer.c @@ -15,16 +15,44 @@ // The harness builds this file with -fvisibility=hidden and checks with `nm` // that every symbol is still exported (the header's CALCULATOR_API macro // marks each prototype), then runs producer_check.c against it. +#include #include +#include #include #include -#include "calculator.h" +#include "calculator_buffer.h" -// Live returned allocations and cancel tokens, for calculator_debug_live. +// Live runs (returned, consumer-allocated, and error messages and payloads) +// and cancel tokens, for calculator_debug_live. static atomic_uint_fast64_t live_allocations; static atomic_uint_fast64_t live_tokens; +// ── runs ─────────────────────────────────────────────────────────────────── + +// Every run this library hands out, and every run a consumer allocates with +// calculator_alloc, is one 8-aligned block (malloc's alignment is at least +// 8 on every supported platform), so calculator_free_bytes releases both. +static uint8_t* run_new(size_t len) { + if (len == 0) return NULL; + uint8_t* run = (uint8_t*)calloc(len, 1); + if (run != NULL) atomic_fetch_add(&live_allocations, 1); + return run; +} + +static uint8_t* run_copy(const void* data, size_t len) { + uint8_t* run = run_new(len); + if (run != NULL) memcpy(run, data, len); + return run; +} + +static void run_free(const uint8_t* run) { + if (run != NULL) { + atomic_fetch_sub(&live_allocations, 1); + free((void*)run); + } +} + // ── runtime ──────────────────────────────────────────────────────────────── uint32_t calculator_abi_version(void) { return CALCULATOR_ABI_VERSION; } @@ -37,39 +65,35 @@ const calculator_contract_entry* calculator_calculator_contract(size_t* out_len) return table; } -void calculator_error_set(calculator_error* err, int32_t code, const char* message) { +// The message is length-delimited UTF-8 (no NUL terminator); NULL is the +// empty message. A producer must not fail on a malformed one, and this one +// copies the bytes as they are. +void calculator_error_set(calculator_error* err, int32_t code, const uint8_t* message_ptr, + size_t message_len) { if (err == NULL) return; calculator_error_clear(err); err->code = code; - if (message != NULL) { - size_t n = strlen(message) + 1; - char* copy = (char*)malloc(n); - if (copy != NULL) memcpy(copy, message, n); - err->message = copy; + if (message_ptr != NULL && message_len != 0) { + err->message_ptr = run_copy(message_ptr, message_len); + err->message_len = err->message_ptr != NULL ? message_len : 0; } } void calculator_error_set_payload(calculator_error* err, const uint8_t* ptr, size_t len) { if (err == NULL) return; - free((void*)err->payload_ptr); + run_free(err->payload_ptr); err->payload_ptr = NULL; err->payload_len = 0; if (ptr == NULL || len == 0) return; - uint8_t* copy = (uint8_t*)malloc(len); - if (copy == NULL) return; - memcpy(copy, ptr, len); - err->payload_ptr = copy; - err->payload_len = len; + err->payload_ptr = run_copy(ptr, len); + err->payload_len = err->payload_ptr != NULL ? len : 0; } void calculator_error_clear(calculator_error* err) { if (err == NULL) return; - free((void*)err->message); - free((void*)err->payload_ptr); - err->code = 0; - err->message = NULL; - err->payload_ptr = NULL; - err->payload_len = 0; + run_free(err->message_ptr); + run_free(err->payload_ptr); + memset(err, 0, sizeof *err); } void calculator_error_free(calculator_error* err) { @@ -77,22 +101,15 @@ void calculator_error_free(calculator_error* err) { free(err); } -// Every byte run, whether this library returned it or a consumer -// allocated it, is one malloc block, so one release function covers both. -uint8_t* calculator_alloc(size_t len) { - if (len == 0) return NULL; - uint8_t* run = (uint8_t*)calloc(len, 1); - if (run != NULL) atomic_fetch_add(&live_allocations, 1); - return run; -} +uint8_t* calculator_alloc(size_t len) { return run_new(len); } void calculator_free_bytes(uint8_t* ptr, size_t len) { - if (ptr != NULL && len != 0) { - atomic_fetch_sub(&live_allocations, 1); - free(ptr); - } + if (len != 0) run_free(ptr); } +// A literal message, for calculator_error_set. +#define MSG(s) (const uint8_t*)(s), sizeof(s) - 1 + struct calculator_cancel_token { atomic_int refs; atomic_bool cancelled; @@ -145,7 +162,7 @@ int32_t calculator_calculator_add(int32_t a, int32_t b, calculator_error* out_er int32_t calculator_calculator_divide(int32_t a, int32_t b, calculator_error* out_err) { if (b == 0) { calculator_error_set(out_err, calculator_calculator_CalcError_DivisionByZero, - "division by zero"); + MSG("division by zero")); return 0; } if (a == INT32_MIN && b == -1) return INT32_MIN; // wraps, like the Rust sample @@ -159,19 +176,117 @@ const uint8_t* calculator_calculator_greet(const uint8_t* name_ptr, size_t name_ static const char prefix[] = "Hello, "; *out_len = 0; if (name_ptr == NULL && name_len != 0) { - calculator_error_set(out_err, -3, "null string with a nonzero length"); + calculator_error_set(out_err, -3, MSG("name is null or invalid")); return NULL; } size_t n = (sizeof prefix - 1) + name_len + 1; - uint8_t* run = (uint8_t*)malloc(n); + uint8_t* run = run_new(n); if (run == NULL) { - calculator_error_set(out_err, -1, "out of memory"); + calculator_error_set(out_err, -1, MSG("out of memory")); return NULL; } memcpy(run, prefix, sizeof prefix - 1); if (name_len != 0) memcpy(run + sizeof prefix - 1, name_ptr, name_len); run[n - 1] = '!'; - atomic_fetch_add(&live_allocations, 1); *out_len = n; return run; } + +static int is_space(uint8_t c) { return c == ' ' || (c >= '\t' && c <= '\r'); } + +// A domain error with fields: the code, a message, and the fields encoded +// with the value-buffer helper as the payload. +int32_t calculator_calculator_parse(const uint8_t* text_ptr, size_t text_len, + calculator_error* out_err) { + if (text_ptr == NULL && text_len != 0) { + calculator_error_set(out_err, -3, MSG("text is null or invalid")); + return 0; + } + size_t lo = 0, hi = text_len; + while (lo < hi && is_space(text_ptr[lo])) lo++; + while (hi > lo && is_space(text_ptr[hi - 1])) hi--; + int negative = lo < hi && text_ptr[lo] == '-'; + size_t i = lo + (lo < hi && (text_ptr[lo] == '-' || text_ptr[lo] == '+')); + int64_t value = 0; + int ok = i < hi; + for (; ok && i < hi; i++) { + if (text_ptr[i] < '0' || text_ptr[i] > '9') ok = 0; + else value = value * 10 + (text_ptr[i] - '0'); + if (value > (int64_t)INT32_MAX + 1) ok = 0; + } + if (ok && (negative ? -value : value) >= INT32_MIN && (negative ? -value : value) <= INT32_MAX) { + return (int32_t)(negative ? -value : value); + } + static const char prefix[] = "not a number: "; + size_t n = sizeof prefix - 1 + text_len; + char* message = (char*)malloc(n + 1); + if (message != NULL) { + memcpy(message, prefix, sizeof prefix - 1); + if (text_len != 0) memcpy(message + sizeof prefix - 1, text_ptr, text_len); + } + calculator_error_set(out_err, calculator_calculator_ParseError_NotANumber, + (const uint8_t*)message, message != NULL ? n : 0); + free(message); + calculator_calculator_ParseError_NotANumber_payload payload; + payload.text.ptr = (const char*)text_ptr; + payload.text.len = text_len; + calculator_writer w; + memset(&w, 0, sizeof w); + calculator_calculator_ParseError_NotANumber_payload_write(&w, &payload); + if (!w.failed) calculator_error_set_payload(out_err, w.ptr, w.len); + calculator_writer_free(&w); + return 0; +} + +// `throws: any`: a failure is code -1 and a message, with no payload. +double calculator_calculator_sqrt(double x, calculator_error* out_err) { + if (x < 0.0) { + char message[64]; + int n = snprintf(message, sizeof message, "cannot take the square root of %g", x); + calculator_error_set(out_err, -1, (const uint8_t*)message, n > 0 ? (size_t)n : 0); + return 0.0; + } + return sqrt(x); +} + +// A typed array arrives borrowed and must be aligned for its element type; +// an optional scalar returns as `bool` (present) plus the out slot. +bool calculator_calculator_mean(const double* values_ptr, size_t values_len, double* out_value, + calculator_error* out_err) { + *out_value = 0.0; + if ((values_ptr == NULL && values_len != 0) || + (uintptr_t)values_ptr % _Alignof(double) != 0) { + calculator_error_set(out_err, -3, MSG("values: the array is null or misaligned")); + return false; + } + if (values_len == 0) return false; + double sum = 0.0; + for (size_t i = 0; i < values_len; i++) sum += values_ptr[i]; + *out_value = sum / (double)values_len; + return true; +} + +// A returned typed array is a run of `count * sizeof(T)` bytes; `out_len` +// is the element count. +int32_t* calculator_calculator_running_total(const int32_t* values_ptr, size_t values_len, + size_t* out_len, calculator_error* out_err) { + *out_len = 0; + if ((values_ptr == NULL && values_len != 0) || + (uintptr_t)values_ptr % _Alignof(int32_t) != 0) { + calculator_error_set(out_err, -3, MSG("values: the array is null or misaligned")); + return NULL; + } + if (values_len == 0) return NULL; + int32_t* run = (int32_t*)(void*)run_new(values_len * sizeof(int32_t)); + if (run == NULL) { + calculator_error_set(out_err, -1, MSG("out of memory")); + return NULL; + } + uint32_t total = 0; + for (size_t i = 0; i < values_len; i++) { + total += (uint32_t)values_ptr[i]; + run[i] = (int32_t)total; + } + *out_len = values_len; + return run; +} diff --git a/conformance/c/producer_check.c b/conformance/c/producer_check.c index b040262a..0620480f 100644 --- a/conformance/c/producer_check.c +++ b/conformance/c/producer_check.c @@ -1,12 +1,13 @@ // Drives the hand-written C producer (producer.c) through the generated // calculator header exactly as a consumer of the Rust producer would: the -// load-time contract check, calls, a typed error (and an error payload), a -// (ptr, len) string in and out with an interior NUL, a consumer-allocated -// run, a cancel token, and the leak counters. +// load-time contract check, calls, typed errors (one with a payload) and an +// untyped one, a (ptr, len) string in and out with an interior NUL, an +// optional scalar return, typed arrays in both directions, a +// consumer-allocated run, a cancel token, and the leak counters. #include "harness.h" -#include "calculator.h" +#include "calculator_buffer.h" int main(void) { calculator_error err = {0}; @@ -20,9 +21,35 @@ int main(void) { assert(calculator_calculator_divide(-7, 2, &err) == -3 && err.code == 0); assert(calculator_calculator_divide(9, 0, &err) == 0); assert(err.code == calculator_calculator_CalcError_DivisionByZero); - assert(strcmp(err.message, "division by zero") == 0); + assert(MSG_EQ(err, "division by zero")); calculator_error_clear(&err); - assert(err.code == 0 && err.message == NULL); + assert(err.code == 0 && err.message_ptr == NULL && err.message_len == 0); + + assert(calculator_calculator_parse(STR(" -12 "), &err) == -12 && err.code == 0); + assert(calculator_calculator_parse(STR("4x"), &err) == 0); + assert(err.code == calculator_calculator_ParseError_NotANumber); + assert(MSG_EQ(err, "not a number: 4x")); + calculator_calculator_ParseError_NotANumber_payload payload; + assert(calculator_calculator_ParseError_NotANumber_payload_decode(err.payload_ptr, + err.payload_len, &payload)); + assert(bytes_eq((const uint8_t*)payload.text.ptr, payload.text.len, "4x")); + calculator_calculator_ParseError_NotANumber_payload_free(&payload); + calculator_error_clear(&err); + + assert(calculator_calculator_sqrt(-4.0, &err) == 0.0); + assert(err.code == -1 && MSG_EQ(err, "cannot take the square root of -4")); + calculator_error_clear(&err); + + const double xs[2] = {1.0, 2.0}; + double mean = 0.0; + assert(calculator_calculator_mean(xs, 2, &mean, &err) && mean == 1.5); + assert(!calculator_calculator_mean(NULL, 0, &mean, &err) && err.code == 0); + + const int32_t ints[3] = {1, 2, 3}; + size_t count = 0; + int32_t* totals = calculator_calculator_running_total(ints, 3, &count, &err); + assert(err.code == 0 && count == 3 && totals[2] == 6); + calculator_free_bytes((uint8_t*)totals, count * sizeof(int32_t)); static const char name[] = "a\0b"; size_t len = 0; @@ -34,9 +61,10 @@ int main(void) { uint8_t* staged = calculator_alloc(8); assert(staged != NULL && calculator_alloc(0) == NULL); - calculator_error_set(&err, calculator_calculator_CalcError_DivisionByZero, "with payload"); + assert(((uintptr_t)staged % 8) == 0); + calculator_error_set(&err, calculator_calculator_CalcError_DivisionByZero, STR("with payload")); calculator_error_set_payload(&err, staged, 8); - assert(err.payload_len == 8); + assert(err.payload_len == 8 && MSG_EQ(err, "with payload")); calculator_error_clear(&err); calculator_free_bytes(staged, 8); diff --git a/conformance/c/run.sh b/conformance/c/run.sh index bd36c7eb..561acc9f 100755 --- a/conformance/c/run.sh +++ b/conformance/c/run.sh @@ -37,13 +37,15 @@ c_producer_exports() { local incdir="$GENROOT/calculator/c" local lib="$OUT/libcalc_producer.$EXT" clang -std=c11 -Wall -Wextra -Werror -shared -fPIC -fvisibility=hidden \ - -I "$incdir" "$ROOT/conformance/c/producer.c" -o "$lib" \ + -I "$incdir" "$ROOT/conformance/c/producer.c" -lm -o "$lib" \ || { echo "producer compile failed" >&2; return 1; } local syms syms=$(nm -g --defined-only "$lib" 2>/dev/null) || syms=$(nm -gU "$lib" 2>/dev/null) local sym for sym in calculator_calculator_add calculator_calculator_divide \ - calculator_calculator_greet \ + calculator_calculator_greet calculator_calculator_parse \ + calculator_calculator_sqrt calculator_calculator_mean \ + calculator_calculator_running_total \ calculator_abi_version calculator_calculator_contract \ calculator_error_set calculator_error_set_payload calculator_error_clear \ calculator_error_free calculator_alloc calculator_free_bytes \ diff --git a/conformance/cpp/calculator.cpp b/conformance/cpp/calculator.cpp index d283638d..218b2743 100644 --- a/conformance/cpp/calculator.cpp +++ b/conformance/cpp/calculator.cpp @@ -1,16 +1,23 @@ -// Conformance consumer: calculator sample, C++ target (ABI revision 4). +// Conformance consumer: calculator sample, C++ target (ABI revision 5). // // The getting-started surface: the load-time check, a direct-value call -// (wrapping), a throwing call whose typed DivisionByZeroError derives from -// CalcError and Error, and a string in and out through `std::string_view` -// (non-ASCII text, an astral character, and an interior NUL survive the -// pointer-and-length ABI). Invalid UTF-8 is a producer-side marshalling -// failure on a call that declares no errors, so it throws InternalError. -// Ends by asserting the producer's leak counters are all zero. +// (wrapping), throwing calls whose typed exceptions come from two domains of +// one module (`CalcError` and `ParseError`, whose code 1 values differ and +// whose `NotANumberError` carries a field), an untyped (`throws: any`) +// failure thrown as the root `Error` with code -1, a string in and out +// through `std::string_view` (non-ASCII text, an astral character, and an +// interior NUL survive the pointer-and-length ABI), an optional scalar +// return (`std::optional`), and typed arrays in both directions +// (`std::vector` storage passed as is). Invalid UTF-8 is a producer-side +// marshalling failure on a call that declares no errors, so it throws +// InternalError, which is an Error too. Ends by asserting the producer's +// leak counters are all zero. #include #include +#include #include +#include #include "calculator.hpp" #include "check.hpp" @@ -20,10 +27,10 @@ using calculator::CalcError; using calculator::DivisionByZeroError; using calculator::Error; using calculator::InternalError; +using calculator::NotANumberError; +using calculator::ParseError; -static void run() { - calculator::check_library(); - +static void arithmetic() { CHECK(calc::add(2, 3) == 5); CHECK(calc::add(-7, 7) == 0); CHECK(calc::add(INT32_MAX, 1) == INT32_MIN); @@ -37,7 +44,32 @@ static void run() { CHECK(std::string(e.what()) == "division by zero"); CHECK(dynamic_cast(&e) != nullptr); CHECK(dynamic_cast(&e) != nullptr); +} + +static void second_domain() { + CHECK(calc::parse("42") == 42); + CHECK(calc::parse(" 42 ") == 42); + // ParseError's code 1 is NotANumber, not CalcError's DivisionByZero. + NotANumberError e = expect_throw([] { calc::parse("4x"); }, "parse('4x')"); + CHECK(e.code() == 1 && e.text == "4x"); + CHECK(std::string(e.what()) == "not a number: 4x"); + CHECK(dynamic_cast(&e) != nullptr); + CHECK(dynamic_cast(&e) == nullptr); + + e = expect_throw([] { calc::parse(""); }, "parse('')"); + CHECK(e.text.empty() && std::string(e.what()) == "not a number: "); +} + +static void untyped() { + CHECK(calc::sqrt(9.0) == 3.0); + Error e = expect_throw([] { calc::sqrt(-4.0); }, "sqrt(-4)"); + CHECK(e.code() == -1); + CHECK(std::string(e.what()) == "cannot take the square root of -4"); + CHECK(dynamic_cast(&e) == nullptr); +} + +static void strings() { CHECK(calc::greet("World") == "Hello, World!"); CHECK(calc::greet("") == "Hello, !"); CHECK(calc::greet("W\xc3\xb6rld \xf0\x9f\xa6\x80") == "Hello, W\xc3\xb6rld \xf0\x9f\xa6\x80!"); @@ -47,10 +79,32 @@ static void run() { InternalError bad = expect_throw([] { calc::greet("\xc3\x28"); }, "greet(non-UTF-8)"); CHECK(bad.code() == -3); + CHECK(dynamic_cast(&bad) != nullptr); +} + +static void optional_and_slices() { + std::optional m = calc::mean({1.0, 2.0, 6.0}); + CHECK(m.has_value() && *m == 3.0); + CHECK(!calc::mean({}).has_value()); + + CHECK((calc::running_total({1, 2, 3}) == std::vector{1, 3, 6})); + CHECK(calc::running_total({}).empty()); + CHECK((calc::running_total({1, 2, 3, INT32_MAX}) == std::vector{1, 3, 6, -2147483643})); + + // A vector passes its own storage, so a large one crosses without a copy. + std::vector ones(100000, 1); + std::vector totals = calc::running_total(ones); + CHECK(totals.size() == ones.size() && totals.back() == 100000); } int main() { - run(); + calculator::check_library(); + arithmetic(); + second_domain(); + untyped(); + strings(); + optional_and_slices(); + check_no_leaks(calculator_debug_live, "calculator"); std::printf("cpp/calculator: OK\n"); return 0; diff --git a/conformance/cpp/codec.cpp b/conformance/cpp/codec.cpp index 187b007b..748b3a8d 100644 --- a/conformance/cpp/codec.cpp +++ b/conformance/cpp/codec.cpp @@ -1,4 +1,4 @@ -// Conformance consumer: codec sample, C++ target (ABI revision 4). +// Conformance consumer: codec sample, C++ target (ABI revision 5). // // The shared-vector loop: fetch every vector the producer serves (decoded by // the generated codecs), hand it back to `check_vector` (re-encoded by them), @@ -7,14 +7,20 @@ // literals (so a symmetric encode/decode bug can't hide), spot checks of // decoded fields, the typed out-of-range error and its payload, malformed // input rejected on both sides, and object identity and reference counting -// through value buffers. Ends by asserting the producer's leak counters are -// zero. +// through value buffers. Then the ABI 5 shapes: optional scalars and enums +// in both directions (`std::optional`), typed arrays in both directions +// (`std::vector` storage passed as is, bit-exact), a `usize` crossing as +// `uint64_t`, a `char` and a custom type crossing as strings (their +// marshalling failures are InternalError -3), a lazy `Range` of typed +// arrays, and record equality. Ends by asserting the producer's leak +// counters are zero. #include #include #include #include #include +#include #include #include #include @@ -204,7 +210,7 @@ static void malformed() { }; for (const auto& buf : bad) { InternalError d = expect_throw( - [&buf] { codec::detail::decode(buf.data(), buf.size(), &codec::detail::read_Vector); }, + [&buf] { codec::detail::decode(buf.data(), buf.size()); }, "decoding a malformed Vector"); CHECK(d.code() == -3); } @@ -245,6 +251,121 @@ static void objects(uint32_t n) { CHECK(moved.handle() == twin.handle() && copy.handle() == nullptr); } +static void optional_scalars() { + CHECK(!api::echo_opt_i32(std::nullopt).has_value()); + CHECK(api::echo_opt_i32(INT32_MIN) == INT32_MIN); + CHECK(api::echo_opt_i32(0) == 0); + + std::optional neg_zero = api::echo_opt_f64(-0.0); + CHECK(neg_zero.has_value() && *neg_zero == 0.0 && std::signbit(*neg_zero)); + std::optional nan = api::echo_opt_f64(std::numeric_limits::quiet_NaN()); + CHECK(nan.has_value() && std::isnan(*nan)); + CHECK(!api::echo_opt_f64(std::nullopt).has_value()); + + CHECK(api::echo_opt_bool(true) == true); + CHECK(api::echo_opt_bool(false) == false); + CHECK(!api::echo_opt_bool(std::nullopt).has_value()); + + CHECK(api::echo_opt_color(Color::Infrared) == Color::Infrared); + CHECK(api::echo_opt_color(Color::Blue) == Color::Blue); + CHECK(!api::echo_opt_color(std::nullopt).has_value()); + + // A present value that isn't a declared variant is a marshalling failure. + InternalError e = expect_throw( + [] { api::echo_opt_color(static_cast(3)); }, "echo_opt_color(3)"); + CHECK(e.code() == -3); +} + +template +static bool same_bits(const std::vector& a, const std::vector& b) { + return a.size() == b.size() && (a.empty() || std::memcmp(a.data(), b.data(), a.size() * sizeof(T)) == 0); +} + +static void typed_arrays() { + uint64_t nan_bits = 0x7ff8000000000001ull; + double nan = 0; + std::memcpy(&nan, &nan_bits, sizeof nan); + const std::vector floats{nan, -0.0, 5e-324, std::numeric_limits::infinity()}; + CHECK(same_bits(api::echo_f64s(floats), floats)); + CHECK(api::echo_f64s({}).empty()); + + const std::vector ints{INT32_MIN, 0, INT32_MAX}; + CHECK(api::echo_i32s(ints) == ints); + CHECK(api::echo_i32s({}).empty()); + + const std::vector wide{UINT64_MAX, 9223372036854775808ull}; + CHECK(api::echo_u64s(wide) == wide); + CHECK(api::echo_u64s({}).empty()); + + // Every primitive vector's value through the matching typed array too. + std::vector one{-7}; + CHECK(api::echo_i32s(one) == one); +} + +static void usize_char_custom() { + CHECK(api::echo_usize(4294967295ull) == 4294967295ull); + CHECK(api::echo_usize(UINT64_MAX) == UINT64_MAX); + + CHECK(api::echo_char("\xf0\x9f\xa6\x80") == "\xf0\x9f\xa6\x80"); + CHECK(api::echo_char("\xc3\xa9") == "\xc3\xa9"); + CHECK(api::echo_char("a") == "a"); + InternalError e = expect_throw([] { api::echo_char("ab"); }, "echo_char('ab')"); + CHECK(e.code() == -3 && std::string(e.what()) == "value: \"ab\" is not a valid char"); + e = expect_throw([] { api::echo_char(""); }, "echo_char('')"); + CHECK(e.code() == -3 && std::string(e.what()) == "value: \"\" is not a valid char"); + + CHECK(api::echo_hex("ff") == "ff"); + CHECK(api::echo_hex("00FF") == "ff"); + CHECK(api::echo_hex("0") == "0"); + e = expect_throw([] { api::echo_hex("xyz"); }, "echo_hex('xyz')"); + CHECK(e.code() == -3 && std::string(e.what()) == "value: invalid digit found in string"); + e = expect_throw([] { api::echo_hex(""); }, "echo_hex('')"); + CHECK(e.code() == -3 && std::string(e.what()) == "value: cannot parse integer from empty string"); + e = expect_throw([] { api::echo_hex("100000000"); }, "echo_hex(2^32)"); + CHECK(e.code() == -3 && std::string(e.what()) == "value: number too large to fit in target type"); +} + +static std::vector> pull(codec::Range> range) { + std::vector> out; + for (std::vector& chunk : range) out.push_back(std::move(chunk)); + return out; +} + +static void chunked_iterators() { + CHECK((pull(api::chunks({INT32_MIN, 0, INT32_MAX}, 2)) == + std::vector>{{INT32_MIN, 0}, {INT32_MAX}})); + CHECK((pull(api::chunks({1, 2, 3, 4}, 2)) == std::vector>{{1, 2}, {3, 4}})); + CHECK(pull(api::chunks({1, 2}, 0)).empty()); + CHECK(pull(api::chunks({}, 3)).empty()); + + // Manual pulls, then an early close releases the producer iterator. + codec::Range> range = api::chunks({1, 2, 3}, 1); + CHECK(range.next() == std::vector{1}); + CHECK(codec_debug_live(2) == 1); + range.close(); + CHECK(codec_debug_live(2) == 0); + CHECK(!range.next().has_value()); +} + +static void equality(uint32_t n) { + // Records compare memberwise; NaN fields compare unequal, as in C++. + CHECK(canonical_scalars() == canonical_scalars()); + Scalars changed = canonical_scalars(); + changed.u16_value = 1; + CHECK(changed != canonical_scalars()); + + // Rich enums compare by active variant and payload; tokens by identity. + CHECK(fetch(n, "shape labeled") == fetch(n, "shape labeled")); + CHECK(fetch(n, "shape labeled") != fetch(n, "blank")); + Token t(5); + CHECK((Holder{t, std::nullopt, {}, {}} == Holder{t, std::nullopt, {}, {}})); + CHECK((Holder{t, std::nullopt, {}, {}} != Holder{Token(5), std::nullopt, {}, {}})); + + // Default member initializers zero every scalar. + Scalars zero; + CHECK(zero.i32_value == 0 && zero.f64_value == 0.0 && !zero.flag && zero.color == Color{}); +} + int main() { codec::check_library(); uint32_t n = api::vector_count(); @@ -256,6 +377,11 @@ int main() { out_of_range(n); malformed(); objects(n); + optional_scalars(); + typed_arrays(); + usize_char_custom(); + chunked_iterators(); + equality(n); check_no_leaks(codec_debug_live, "codec"); std::printf("cpp/codec: OK (%u vectors)\n", n); diff --git a/conformance/cpp/kvstore.cpp b/conformance/cpp/kvstore.cpp index 8838dea2..96498941 100644 --- a/conformance/cpp/kvstore.cpp +++ b/conformance/cpp/kvstore.cpp @@ -1,4 +1,4 @@ -// Conformance consumer: kvstore sample, C++ target (ABI revision 4). +// Conformance consumer: kvstore sample, C++ target (ABI revision 5). // // Drives the feature-complete producer through the generated header: // @@ -8,22 +8,29 @@ // statics, the deprecated `size`, records (`Entry`, `StoreInfo`), the // `EntryKind` enum, maps and optionals, and the logical clock; // * the `KvError` hierarchy with payload fields (`KeyNotFoundError`, -// `ExpiredError`, `StoreFullError`, `RejectedError`) and runtime codes on -// throwing calls (the root `Error`); -// * lazy ranges of strings (throwing), records, and objects; -// * three callback interfaces implemented as subclasses: a `Listener` +// `ExpiredError`, `StoreFullError`, `RejectedError`, +// `CallbackFailedError`) and runtime codes on throwing calls (the root +// `Error`); +// * lazy `Range`s of strings (throwing), records, objects, and optional +// scalars; +// * four callback interfaces implemented as subclasses: a `Listener` // (retained, filtered by `accepts`, told about every `Change`, detached // when it throws, and notified from a producer thread during -// compaction), a `Policy` (a record return, a typed `RejectedError` -// thrown back through `put` with its fields, an object parameter and -// object return, and returns the producer rejects), and a `Loader` passed -// as an optional callback (string, bytes, and optional-object returns; -// typed errors decoded by the producer or passed through); -// * `Store` objects in every position, compared by native identity; +// compaction), a `Policy` (an optional scalar parameter and return in +// `ttl_for`, a record return, a typed `RejectedError` thrown back through +// `put` with its fields, an object parameter and object return, and +// failures the sample turns into `CallbackFailedError`), a `Loader` +// passed as an optional callback (string, bytes, and optional-object +// returns; typed errors decoded by the producer or passed through), and +// a `Scorer` taking and returning typed arrays; +// * `Store` objects in every position, compared by identity (`==`); // * futures: an async free function returning an object, a cancellable // method cancelled mid-pause, an async list, an async function in the // nested `kv.stats` module, and concurrent calls; -// * the nested `kv.stats` module and the sibling `report` root. +// * the nested `kv.stats` module and the sibling `report` root; +// * the ABI 5 shapes: an optional scalar return, iterator item, and async +// result; typed arrays as a return and an async result; a `usize` +// return; and a `throws: any` method failing with the root `Error` (-1). // // Ends by asserting every callback implementation was released and the // producer's leak counters are all zero. @@ -55,6 +62,7 @@ static const std::thread::id g_main_thread = std::this_thread::get_id(); static std::atomic g_listeners_freed{0}; static std::atomic g_policies_freed{0}; static std::atomic g_loaders_freed{0}; +static std::atomic g_scorers_freed{0}; static std::vector bytes(std::string_view s) { return std::vector(s.begin(), s.end()); } @@ -190,8 +198,8 @@ static void iterators() { CHECK(parts.size() == 3); const uint32_t counts[] = {2, 1, 0}; for (size_t i = 0; i < parts.size(); ++i) { - CHECK(parts[i].handle() != s.handle()); - for (size_t j = 0; j < i; ++j) CHECK(parts[i].handle() != parts[j].handle()); + CHECK(parts[i] != s); + for (size_t j = 0; j < i; ++j) CHECK(parts[i] != parts[j]); CHECK(parts[i].count() == counts[i] && parts[i].path() == prefixes[i]); } } @@ -296,8 +304,9 @@ static void listeners() { put(s, "boom", "1"); CHECK(s.count() == 2 && s.listener_count() == 0 && failing.use_count() == 1); - // A null listener is refused before the call. - expect_throw([&] { s.subscribe(nullptr); }, "subscribe(nullptr)"); + // A null listener is refused before the call, as a marshalling failure. + Error null_listener = expect_throw([&] { s.subscribe(nullptr); }, "subscribe(nullptr)"); + CHECK(null_listener.code() == -3); // Destroying the store releases the listeners it still holds. freed = g_listeners_freed; @@ -322,6 +331,15 @@ class TestPolicy : public Policy { explicit TestPolicy(Store other) : other_(std::move(other)) {} ~TestPolicy() override { ++g_policies_freed; } + // "short" lives one tick, "forever" never expires, "ttl-fail" fails with + // a typed error, and every other key keeps the requested TTL. + std::optional ttl_for(std::string_view key, std::optional requested) override { + if (key == "short") return 1; + if (key == "forever") return std::nullopt; + if (key == "ttl-fail") throw InvalidPathError("no ttl for you"); + return requested; + } + Entry admit(const Entry& entry) override { ++admitted; if (entry.version != 0) saw_version_zero = false; @@ -367,25 +385,33 @@ static void policies() { CHECK(s.count() == 1 && other.count() == 1); // A typed error thrown by the throwing callback reaches the caller with - // its code, message, and fields. + // its code and fields, and the domain's own message for them. RejectedError rej = expect_throw([&] { put(s, "secret", "3"); }, "Rejected"); CHECK(rej.code() == 1005 && rej.key == "secret" && rej.reason == "no secrets"); - CHECK(std::string(rej.what()) == "secrets are not stored"); + CHECK(std::string(rej.what()) == "write to secret rejected: no secrets"); - // Any other exception arrives as -4 with its message. - Error boom = expect_throw([&] { put(s, "boom", "4"); }, "non-domain failure"); - CHECK(boom.code() == -4 && std::string(boom.what()) == "policy exploded"); - CHECK(dynamic_cast(&boom) == nullptr); + // Any other exception reaches the producer as a callback failure, which + // the sample turns into CallbackFailed with the exception's message. + CallbackFailedError boom = + expect_throw([&] { put(s, "boom", "4"); }, "non-domain failure"); + CHECK(boom.code() == 1006 && boom.message == "policy exploded"); + CHECK(std::string(boom.what()) == "policy exploded"); - // A return the producer can't accept is -3: a malformed record, or a + // So is a return the producer can't accept: a malformed record, or a // null required object. - Error garbage = expect_throw([&] { put(s, "garbage", "5"); }, "malformed admit"); - CHECK(garbage.code() == -3); - Error null_route = expect_throw([&] { put(s, "null/x", "6"); }, "null route"); - CHECK(null_route.code() == -3); + expect_throw([&] { put(s, "garbage", "5"); }, "malformed admit"); + expect_throw([&] { put(s, "null/x", "6"); }, "null route"); CHECK(s.count() == 1 && other.count() == 1); CHECK(policy->admitted == 6 && policy->saw_version_zero); + // ttl_for: an optional scalar in and out, consulted before admit. + CHECK(put(s, "short", "x", EntryKind::Volatile).expires_at == 1); + CHECK(!put(s, "forever", "x", EntryKind::Volatile, 5).expires_at.has_value()); + CHECK(put(s, "kept", "x", EntryKind::Volatile, 5).expires_at == 5); + InvalidPathError ttl = expect_throw([&] { put(s, "ttl-fail", "x"); }, "ttl_for fails"); + CHECK(ttl.code() == 1004 && std::string(ttl.what()) == "invalid path"); + CHECK(s.count() == 4 && policy->admitted == 9); + // Replacing the policy releases the old one; an empty pointer removes it. s.set_policy(std::make_shared(other)); CHECK(policy.use_count() == 1); @@ -393,7 +419,7 @@ static void policies() { s.set_policy(nullptr); CHECK(g_policies_freed == freed + 1 && !s.has_policy()); put(s, "secret", "now allowed"); - CHECK(s.count() == 2); + CHECK(s.count() == 5); } // ── loader ────────────────────────────────────────────────────────────────── @@ -442,9 +468,10 @@ static void loaders() { CHECK(!s.get_or_load("missing", loader).has_value()); KeyNotFoundError nf = expect_key_not_found([&] { s.get_or_load("elsewhere", loader); }, "other", "elsewhere"); - CHECK(std::string(nf.what()) == "not in the loader"); - Error broken = expect_throw([&] { s.get_or_load("broken", loader); }, "broken loader"); - CHECK(broken.code() == -4 && std::string(broken.what()) == "loader is broken"); + CHECK(std::string(nf.what()) == "key not found: other"); + CallbackFailedError broken = + expect_throw([&] { s.get_or_load("broken", loader); }, "broken loader"); + CHECK(broken.message == "loader is broken"); CHECK(loader.use_count() == 1); int freed = g_loaders_freed; loader.reset(); @@ -537,8 +564,9 @@ static void object_graph() { CHECK(larger.has_value() && larger->handle() == s.handle()); StoreInfo info = s.describe("main", fork); - CHECK(info.label == "main" && info.store.handle() == s.handle() && info.count == 1); - CHECK(info.mirror.has_value() && info.mirror->handle() == fork.handle()); + CHECK(info.label == "main" && info.store == s && info.count == 1); + CHECK(info.mirror == fork); + CHECK(info == s.describe("main", fork) && info != s.describe("other", fork)); CHECK(info.mirror->count() == 2); std::vector many = Store::open_many({"/a", "/b"}); @@ -582,6 +610,78 @@ static void stats_and_report() { CHECK(dynamic_cast(&none) == nullptr); } +// ── scorer and the ABI 5 shapes ───────────────────────────────────────────── + +enum class ScoreMode { Sizes, Fail, Short }; + +// Scores each value size as itself; fails, or returns too few scores, on +// request. +class TestScorer : public Scorer { + ScoreMode mode_; + +public: + explicit TestScorer(ScoreMode mode) : mode_(mode) {} + ~TestScorer() override { ++g_scorers_freed; } + + std::vector scores(const std::vector& sizes) override { + if (mode_ == ScoreMode::Sizes) CHECK((sizes == std::vector{1, 3, 2})); + if (mode_ == ScoreMode::Fail) throw std::runtime_error("scorer is out of order"); + std::vector out; + for (uint64_t size : sizes) out.push_back(static_cast(size)); + if (mode_ == ScoreMode::Short) out.resize(1); + return out; + } +}; + +static void abi5_shapes() { + Store s = Store::open("/abi5"); + put(s, "b", "12"); + put(s, "a", "\x01\x02\x03", EntryKind::Volatile, 7); + CHECK(s.count() == 2); + + // An optional scalar return. + CHECK(s.expires_at("a") == 7); + CHECK(!s.expires_at("b").has_value()); + CHECK(!s.expires_at("zzz").has_value()); + + // A typed-array return, in key order. + CHECK((s.value_sizes() == std::vector{3, 2})); + + // Optional scalar iterator items: an absent item isn't the end. + std::vector> expirations; + for (const std::optional& at : s.expirations()) expirations.push_back(at); + CHECK((expirations == std::vector>{7, std::nullopt})); + + // An optional scalar async result and a typed-array async result. + CHECK(s.version_of("a").get() == 1u); + CHECK(!s.version_of("q").get().has_value()); + put(s, "b", "x"); + CHECK((s.versions({"b", "q", "a"}).get() == std::vector{2, 0, 1})); + + // A callback taking and returning typed arrays. + Store r = Store::open("/rank"); + put(r, "a", "1"); + put(r, "b", "333"); + put(r, "c", "22"); + CHECK((r.rank(std::make_shared(ScoreMode::Sizes)) == std::vector{"b", "c", "a"})); + CallbackFailedError failed = expect_throw( + [&] { r.rank(std::make_shared(ScoreMode::Fail)); }, "failing scorer"); + CHECK(failed.message == "scorer is out of order"); + failed = expect_throw( + [&] { r.rank(std::make_shared(ScoreMode::Short)); }, "short scorer"); + CHECK(failed.message == "expected 3 scores, got 1"); + CHECK(g_scorers_freed == 3); + + // `throws: any` and a `usize` return: the root Error with code -1. + Store imp = Store::open("/import"); + CHECK(imp.import_lines("a=1\n\nb=two\n") == 2u); + CHECK(imp.get("b").value == bytes("two")); + Error e = expect_throw([&] { imp.import_lines("c=3\nbroken\nd=4"); }, "import_lines"); + CHECK(e.code() == -1 && std::string(e.what()) == "line 2: expected key=value"); + CHECK(dynamic_cast(&e) == nullptr); + CHECK(imp.count() == 3); +} + int main() { load_checks(); constructors(); @@ -593,10 +693,12 @@ int main() { async_calls(); object_graph(); stats_and_report(); + abi5_shapes(); CHECK(kvstore_debug_live(1) == 0); check_no_leaks(kvstore_debug_live, "kvstore"); - std::printf("cpp/kvstore: OK (%d listeners, %d policies, %d loaders released)\n", - g_listeners_freed.load(), g_policies_freed.load(), g_loaders_freed.load()); + std::printf("cpp/kvstore: OK (%d listeners, %d policies, %d loaders, %d scorers released)\n", + g_listeners_freed.load(), g_policies_freed.load(), g_loaders_freed.load(), + g_scorers_freed.load()); return 0; } diff --git a/conformance/dart/calculator.dart b/conformance/dart/calculator.dart index 3eb9bb4e..546b9a4b 100644 --- a/conformance/dart/calculator.dart +++ b/conformance/dart/calculator.dart @@ -3,9 +3,14 @@ // The getting-started surface: a direct-value call (wrapping at the i32 // edges), a throwing call that raises the typed DivisionByZeroException, and // a string in and out (non-ASCII and astral text and an interior NUL -// survive). Loading the bindings checks the ABI revision and the -// `calculator` contract. Ends by asserting the producer's leak counters are -// zero. +// survive). Then a second error domain in the same module (`ParseError`, +// whose code 1 must not be mistaken for `CalcError`'s), an untyped +// `throws any` error, an optional scalar return (OptDirect), and numeric +// lists in and out (typed arrays). Loading the bindings checks the ABI +// revision and the `calculator` contract. Ends by asserting the producer's +// leak counters are zero. + +import 'dart:typed_data'; import 'package:calculator/calculator.dart' as calc; @@ -41,6 +46,68 @@ void run() { nul == 'Hello, a\u0000b!', 'greet keeps an interior NUL (got ${nul.codeUnits})', ); + + // A second domain: its code 1 is `NotANumber`, not `DivisionByZero`. + expect(calc.parse('42') == 42, 'parse(42)'); + expect(calc.parse(' 42 ') == 42, 'parse trims'); + final nan = expectThrows( + () => calc.parse('4x'), + 'parse(4x) raises NotANumber', + ); + expect(nan is calc.ParseException, 'NotANumber is a ParseException'); + expect( + nan.code == 1 && nan.text == '4x' && nan.message == 'not a number: 4x', + 'NotANumber payload and message (got ${nan.code}, ${nan.text}, ' + '${nan.message})', + ); + final empty = expectThrows( + () => calc.parse(''), + 'parse("") raises NotANumber', + ); + expect( + empty.text == '' && empty.message == 'not a number: ', + 'NotANumber of the empty string', + ); + + // `throws any`: a plain NativeException with the generic code. + expect(calc.sqrt(9.0) == 3.0, 'sqrt(9)'); + final untyped = expectThrows( + () => calc.sqrt(-4.0), + 'sqrt(-4) raises', + ); + expect( + untyped.runtimeType == calc.NativeException && + untyped.code == calc.NativeException.genericCode && + untyped.message == 'cannot take the square root of -4', + 'untyped error (got ${untyped.runtimeType} ${untyped.code}: ' + '${untyped.message})', + ); + + // A numeric list in (a typed array), an optional scalar out. + expect(calc.mean([1.0, 2.0, 6.0]) == 3.0, 'mean([1, 2, 6])'); + expect(calc.mean(Float64List.fromList([2.5])) == 2.5, 'mean of a Float64List'); + expect(calc.mean([]) == null, 'mean([]) is absent'); + + // Typed arrays both ways, wrapping at the i32 edge. + expect(listEquals(calc.runningTotal([1, 2, 3]), [1, 3, 6]), 'runningTotal'); + expect(calc.runningTotal([]).isEmpty, 'runningTotal([])'); + final wrapped = calc.runningTotal([1, 2, 3, i32Max]); + expect( + listEquals(wrapped, [1, 3, 6, -2147483643]), + 'runningTotal wraps (got $wrapped)', + ); + expect( + calc.runningTotal(Int32List.fromList([5, 5])) is Int32List, + 'a returned typed array is an Int32List', + ); +} + +bool listEquals(List a, List b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; } Future main() async { diff --git a/conformance/dart/codec.dart b/conformance/dart/codec.dart index adf94e7e..0cc213d8 100644 --- a/conformance/dart/codec.dart +++ b/conformance/dart/codec.dart @@ -9,7 +9,13 @@ // OutOfRangeException and its payload, malformed input rejected as // marshalling failures (sent through raw `dart:ffi`, since the bindings only // encode well-formed values), and object identity and reference counting -// through buffers. Ends by asserting the producer's leak counters are zero. +// through buffers. Then the direct transports: optional scalars (OptDirect) +// in both directions, numeric lists as typed arrays (bit-exact floats, the +// u64 edges, a misaligned raw array rejected), `usize`, `char`, and a custom +// type crossing as strings (their marshalling failures surface as a +// NativeError, since those functions don't declare `throws`), and an +// iterator of typed arrays. Ends by asserting the producer's leak counters +// are zero. import 'dart:convert'; import 'dart:ffi'; @@ -319,11 +325,13 @@ void outOfRange(int n) { ); } -/// The C `codec_error` struct, for raw calls. +/// The C `codec_error` struct (ABI 5), for raw calls. final class RawError extends Struct { @Int32() external int code; - external Pointer message; + external Pointer messagePtr; + @Size() + external int messageLen; external Pointer payloadPtr; @Size() external int payloadLen; @@ -365,6 +373,26 @@ final class Raw { Int64 Function(Pointer, Size, Pointer), int Function(Pointer, int, Pointer) >('codec_codec_sum_holder'); + late final echoOptColor = _lib + .lookupFunction< + Bool Function(Bool, Int32, Pointer, Pointer), + bool Function(bool, int, Pointer, Pointer) + >('codec_codec_echo_opt_color'); + late final echoU64s = _lib + .lookupFunction< + Pointer Function( + Pointer, + Size, + Pointer, + Pointer, + ), + Pointer Function( + Pointer, + int, + Pointer, + Pointer, + ) + >('codec_codec_echo_u64s'); late final errorClear = _lib .lookupFunction< Void Function(Pointer), @@ -434,6 +462,32 @@ void malformed(Raw raw) { ], (p, n, e) => raw.echoText(p, n, len, e)); calloc.free(len); expect(textCode == -3, 'invalid UTF-8 is rejected (got $textCode)'); + // An optional Color that's present must be a declared Color. + final out = calloc(); + final optColorCode = raw.code( + const [], + (_, __, e) => raw.echoOptColor(true, 3, out, e), + ); + final absentCode = raw.code( + const [], + (_, __, e) => raw.echoOptColor(false, 3, out, e), + ); + calloc.free(out); + expect( + optColorCode == -3 && absentCode == 0, + 'a present undeclared Color? is rejected, an absent one ignored ' + '(got $optColorCode, $absentCode)', + ); + // A typed array must be aligned for its element type. + final misalignedCode = raw.code(List.filled(17, 0), (p, n, e) { + final len = calloc(); + raw.echoU64s((p + 1).cast(), 2, len, e); + calloc.free(len); + }); + expect( + misalignedCode == -3, + 'a misaligned array is rejected (got $misalignedCode)', + ); // A zero token is a marshalling failure, not a crash. final zeroCode = raw.code( List.filled(17, 0), @@ -501,6 +555,111 @@ void objects(int n) { expectThrows(() => p.value(), "a disposed wrapper can't be used"); } +bool listEquals(List a, List b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; +} + +void optionalScalars() { + expect(c.echoOptI32(null) == null, 'echoOptI32(null)'); + expect(c.echoOptI32(i32Min) == i32Min, 'echoOptI32(i32 min)'); + expect(c.echoOptI32(0) == 0, 'echoOptI32(0) is present'); + final negZero = c.echoOptF64(-0.0); + expect(negZero != null && f64Bits(negZero) == f64Bits(-0.0), 'echoOptF64(-0.0)'); + expect(c.echoOptF64(double.nan)?.isNaN ?? false, 'echoOptF64(NaN)'); + expect(c.echoOptF64(null) == null, 'echoOptF64(null)'); + expect(c.echoOptBool(true) == true, 'echoOptBool(true)'); + expect(c.echoOptBool(false) == false, 'echoOptBool(false) is present'); + expect(c.echoOptBool(null) == null, 'echoOptBool(null)'); + expect(c.echoOptColor(c.Color.infrared) == c.Color.infrared, 'Infrared'); + expect(c.echoOptColor(c.Color.blue) == c.Color.blue, 'Blue'); + expect(c.echoOptColor(null) == null, 'echoOptColor(null)'); +} + +void typedArrays() { + final floats = [double.nan, -0.0, 5e-324, double.infinity]; + final echoed = c.echoF64s(floats); + expect( + echoed.length == 4 && + [for (var i = 0; i < 4; i++) f64Bits(echoed[i]) == f64Bits(floats[i])] + .every((same) => same), + 'echoF64s is bit-identical (got $echoed)', + ); + expect(echoed is Float64List, 'a returned [f64] is a Float64List'); + expect(c.echoF64s(const []).isEmpty, 'echoF64s([])'); + const ints = [i32Min, 0, 0x7fffffff]; + expect(listEquals(c.echoI32s(ints), ints), 'echoI32s'); + expect( + listEquals(c.echoI32s(Int32List.fromList(ints)), ints), + 'echoI32s(Int32List)', + ); + expect(c.echoI32s(const []).isEmpty, 'echoI32s([])'); + // u64 travels as its bit pattern: u64::MAX is -1, 2^63 is i64 min. + expect( + listEquals(c.echoU64s(const [-1, i64Min]), const [-1, i64Min]), + 'echoU64s edges', + ); + expect(c.echoU64s(const []).isEmpty, 'echoU64s([])'); + + // An iterator of typed arrays. + List> chunks(List values, int size) => + [for (final chunk in c.chunks(values, size)) chunk.toList()]; + final edges = chunks(ints, 2); + expect( + edges.length == 2 && + listEquals(edges[0], [i32Min, 0]) && + listEquals(edges[1], [0x7fffffff]), + 'chunks of the edges (got $edges)', + ); + final pairs = chunks([1, 2, 3, 4], 2); + expect( + pairs.length == 2 && listEquals(pairs[0], [1, 2]) && listEquals(pairs[1], [3, 4]), + 'chunks([1, 2, 3, 4], 2)', + ); + expect(chunks([1, 2], 0).isEmpty && chunks(const [], 3).isEmpty, 'no chunks'); +} + +void customAndChar() { + expect(c.echoUsize(4294967295) == 4294967295, 'echoUsize(u32 max)'); + expect(c.echoUsize(-1) == -1, 'echoUsize(u64 max)'); + for (final s in ['\u{1F980}', 'é', 'a']) { + expect(c.echoChar(s) == s, 'echoChar($s)'); + } + for (final (input, message) in [ + ('ab', 'value: "ab" is not a valid char'), + ('', 'value: "" is not a valid char'), + ]) { + final e = expectThrows( + () => c.echoChar(input), + 'echoChar("$input")', + ); + expect( + e.code == c.NativeException.marshalCode && e.message == message, + 'echoChar("$input") is -3 (got ${e.code}: ${e.message})', + ); + } + expect(c.echoHex('ff') == 'ff', 'echoHex(ff)'); + expect(c.echoHex('00FF') == 'ff', 'echoHex normalizes'); + expect(c.echoHex('0') == '0', 'echoHex(0)'); + for (final (input, message) in [ + ('xyz', 'value: invalid digit found in string'), + ('', 'value: cannot parse integer from empty string'), + ('100000000', 'value: number too large to fit in target type'), + ]) { + final e = expectThrows( + () => c.echoHex(input), + 'echoHex("$input")', + ); + expect( + e.code == -3 && e.message == message, + 'echoHex("$input") is -3 (got ${e.code}: ${e.message})', + ); + } +} + Future main() async { final n = c.vectorCount(); expect(n >= 60, 'at least 60 vectors (got $n)'); @@ -511,6 +670,9 @@ Future main() async { outOfRange(n); malformed(Raw()); objects(n); + optionalScalars(); + typedArrays(); + customAndChar(); await expectNoLeaks('codec'); print('dart/codec: OK ($n vectors)'); diff --git a/conformance/dart/kvstore.dart b/conformance/dart/kvstore.dart index 77b61e4b..2dd14851 100644 --- a/conformance/dart/kvstore.dart +++ b/conformance/dart/kvstore.dart @@ -6,37 +6,38 @@ // which loading the bindings performs; // * `Store`: fallible and infallible constructors (`Store.open`, `Store()`), // methods, statics, the deprecated `size()`, the `Entry` and `StoreInfo` -// value classes, the `EntryKind` enum, maps and optionals, and the -// logical clock; -// * the sealed `KvException` hierarchy with its payload fields -// (`KeyNotFound`, `Expired`, `StoreFull`, `Rejected`), and runtime -// failures (-4) as the root `NativeException`; -// * lazy `Iterable`s of strings (throwing at launch), records, and objects, -// including one abandoned part-way; -// * three callback interfaces implemented in Dart: a `Listener` (retained, +// value classes, the `EntryKind` enum, maps and optionals, the logical +// clock, an optional TTL parameter and an optional expiry return (optional +// scalars crossing directly), a `[u64]` return (a typed array), `usize` +// counts, and a `throws any` method (`importLines`); +// * the open `KvException` hierarchy with its payload fields +// (`KeyNotFound`, `Expired`, `StoreFull`, `Rejected`, `CallbackFailed`); +// * lazy `Iterable`s of strings (throwing at launch), records, objects, and +// optional scalars, including one abandoned part-way; +// * four callback interfaces implemented in Dart: a `Listener` (retained, // filtered by `accepts`, told about every `Change` on the event loop, -// and detached when it throws), a `Policy` (a record return, a throwing -// method whose `RejectedException` reaches the `put` caller with its -// payload, an object parameter and object return), and a `Loader` passed -// as an optional callback (string, bytes, and optional-object returns; -// typed errors decoded by the producer or passed through); +// detached when it throws, and detached when the producer calls its +// value-returning `accepts` from a worker thread, which the thread-affine +// vtable refuses instead of letting the VM abort), a `Policy` (an +// optional-scalar method choosing the TTL, a record return, a typed +// `RejectedException` that reaches the `put` caller with its payload, any +// other failure arriving as `CallbackFailed`, an object parameter and +// object return, and calls back into the store from inside a callback, +// each in its own call frame), a `Loader` passed as an optional callback +// (string, bytes, and optional-object returns; typed errors decoded by the +// producer or passed through), and a `Scorer` (a typed array in and out); // * `Store` objects in every position: parameter, return, optional, list, // map value, record field, iterator element, async result, and callback // parameter and return; // * futures: an async free function returning an object, a cancellable // method cancelled mid-pause (completing at once with // `CancelledException` while its background work stops cooperatively, -// shown by `activeJobs`), an async list launched concurrently, and an -// async function in the nested `kv.stats` module; +// shown by `activeJobs`), an async list launched concurrently, async +// optional-scalar and typed-array results, and an async function in the +// nested `kv.stats` module; // * the sibling `report` root (the shared `Entry` record and its own error // domain). // -// One assertion other languages make is skipped: compaction notifies -// listeners from a producer thread, and a listener's `accepts` returns a -// value, which Dart can only run on the isolate's thread (a call from -// another thread aborts the process; see the Dart generator's threading -// rules). So no listener is subscribed while `compact` runs. -// // Releases of consumer callbacks are observed through the producer's // callback counter (`debug_live(1)`). Ends by asserting the producer's leak // counters are zero. @@ -112,9 +113,11 @@ class RecordingListener implements Listener { final String? skip; final String? failOn; final List changes = []; + int asked = 0; @override bool accepts(String key) { + asked++; if (key == failOn) throw StateError('listener refused'); return key != skip; } @@ -144,10 +147,18 @@ class TestPolicy implements Policy { final Store other; int admitted = 0; + @override + int? ttlFor(String key, int? requested) => requested; + @override Entry admit(Entry entry) { admitted++; expect(entry.version == 0, 'the store assigns the version after admission'); + // Calls back into the library from inside a callback run in frames of + // their own: a string return, and a failure caught here, leave the + // outer `put` untouched. + expect(other.path() == '/other', 'a nested call returns its own string'); + expectKeyNotFound('nested', () => other.get('nested')); if (entry.key.startsWith('secret')) { throw RejectedException( entry.key, @@ -178,6 +189,43 @@ class TestPolicy implements Policy { } } +/// Picks TTLs by key: `short` gets 1, `forever` none, `full` and `oops` +/// fail (typed and untyped), and anything else keeps the requested TTL. +class TtlPolicy implements Policy { + @override + int? ttlFor(String key, int? requested) => switch (key) { + 'short' => 1, + 'forever' => null, + 'full' => throw StoreFullException(7), + 'oops' => throw Failure('ttl exploded'), + _ => requested, + }; + + @override + Entry admit(Entry entry) => entry; + + @override + Store route(String key, Store home) => home; +} + +// ── scorer (consumer-implemented, typed arrays in and out) ─────────────── + +class TestScorer implements Scorer { + TestScorer({this.fail = false, this.drop = false}); + + final bool fail; + final bool drop; + List? seen; + + @override + List scores(List sizes) { + seen = sizes.toList(); + if (fail) throw Failure('scorer is broken'); + final scores = [for (final size in sizes) size * 1.0]; + return drop ? scores.sublist(1) : scores; + } +} + // ── loader (consumer-implemented, passed as an optional parameter) ──────── class TestLoader implements Loader { @@ -453,13 +501,14 @@ void policies() { expect(s.count() == 1 && other.count() == 1, 'route redirected b/x'); // A typed error from the throwing callback reaches the caller with its - // code, message, and payload. + // code and payload, and the message the producer renders from them. final rejected = expectThrows( () => s.putText('secret', '3'), 'put(secret)', ); expect( - rejected.code == 1005 && rejected.message == 'secrets are not stored', + rejected.code == 1005 && + rejected.message == 'write to secret rejected: no secrets', 'Rejected code and message (got ${rejected.code}: ${rejected.message})', ); expect( @@ -467,16 +516,16 @@ void policies() { 'Rejected payload', ); - // Anything else arrives as -4 with the consumer's message. - final boom = expectThrows( + // Anything else arrives as CallbackFailed with the consumer's message. + final boom = expectThrows( () => s.putText('boom', '4'), 'put(boom)', ); expect( - boom is! KvException && - boom.code == -4 && + boom.code == 1006 && + boom.message_ == 'policy exploded' && boom.message == 'policy exploded', - '-4 (got ${boom.code}: ${boom.message})', + 'CallbackFailed (got ${boom.code}: ${boom.message})', ); expect( s.count() == 1 && other.count() == 1 && p.admitted == 4, @@ -502,6 +551,36 @@ void policies() { replacement.other.dispose(); other.dispose(); s.dispose(); + + // ttl_for: an optional scalar in and out picks each write's TTL. + final t = Store.open('/ttl'); + t.setPolicy(TtlPolicy()); + expect(t.putText('short', 'x').expiresAt == 1, 'ttlFor overrides none'); + expect( + t.putText('forever', 'x', EntryKind.persistent, 5).expiresAt == null, + 'ttlFor removes a TTL', + ); + expect( + t.putText('other', 'x', EntryKind.persistent, 9).expiresAt == 9, + 'ttlFor keeps the requested TTL', + ); + expect(t.putText('plain', 'x').expiresAt == null, 'no TTL stays none'); + final full = expectThrows( + () => t.putText('full', 'x'), + 'ttlFor raising StoreFull', + ); + expect( + full.capacity == 7 && full.message == 'store is full (7 entries)', + 'a typed ttlFor failure reaches put (got ${full.message})', + ); + final oops = expectThrows( + () => t.putText('oops', 'x'), + 'ttlFor failing otherwise', + ); + expect(oops.message_ == 'ttl exploded', 'CallbackFailed from ttlFor'); + expect(t.count() == 4, 'failed puts stored nothing'); + t.setPolicy(null); + t.dispose(); } void loaders() { @@ -542,39 +621,53 @@ void loaders() { s.getOrLoad('missing', TestLoader()) == null, 'KeyNotFound for the same key is none', ); - // KeyNotFound for another key: passed through, payload intact. + // KeyNotFound for another key: passed through, payload intact, with the + // message the producer renders from it. final other = expectThrows( () => s.getOrLoad('elsewhere', TestLoader()), 'getOrLoad(elsewhere)', ); expect( - other.key == 'other' && other.message == 'not in the loader', + other.key == 'other' && other.message == 'key not found: other', 'KeyNotFound(other) passed through', ); - // Any other failure is -4. - final broken = expectThrows( + // Any other failure is CallbackFailed with the loader's message. + final broken = expectThrows( () => s.getOrLoad('broken', TestLoader()), 'getOrLoad(broken)', ); expect( - broken.code == -4 && broken.message == 'loader is broken', - '-4 (got ${broken.code}: ${broken.message})', + broken.code == 1006 && broken.message_ == 'loader is broken', + 'CallbackFailed (got ${broken.code}: ${broken.message})', ); expect(liveCallbacks() == base, 'every loader was released'); s.dispose(); } Future asyncCalls() async { + final base = liveCallbacks(); final s = Store.open('/async-calls'); + // Subscribed on this thread, so `put` (a synchronous call) asks it here. + final affine = RecordingListener(); + s.subscribe(affine); s.putText('old1', 'x', EntryKind.volatile, 1); s.putText('old2', 'x', EntryKind.volatile, 1); s.putText('keep', 'x'); s.tick(5); + expect(affine.asked == 3, 'accepts ran for each put (got ${affine.asked})'); - // compact runs on a producer thread (no listener: see the header). + // compact runs on a producer thread and tells listeners there. The + // listener's vtable is thread-affine, so the producer refuses to call its + // `accepts` (it returns a value) off this thread: the method never runs, + // the listener fails with -4 and is detached, and compaction carries on. final token = CancelToken(); expect(await s.compact(0, cancelToken: token) == 2, 'compact(0) removed 2'); expect(s.count() == 1, 'one entry left'); + expect(affine.asked == 3, 'accepts never ran off its thread'); + expect( + s.listenerCount() == 0 && liveCallbacks() == base, + 'the refused listener was detached and released', + ); expect(await s.compact(5) == 0, 'compact(5) removed nothing'); // Cancel mid-pause: the call completes at once with a cancellation, and @@ -603,6 +696,21 @@ Future asyncCalls() async { timeout: const Duration(seconds: 2), ); + // Async optional-scalar and typed-array results. + final v = Store.open('/versions'); + v.putText('b', '12'); + v.putText('a', '123', EntryKind.volatile, 7); + expect(await v.versionOf('a') == 1, 'versionOf(a)'); + expect(await v.versionOf('q') == null, 'versionOf(q) is absent'); + v.putText('b', 'x'); + final versions = await v.versions(['b', 'q', 'a']); + expect(listEquals(versions, [2, 0, 1]), 'versions (got $versions)'); + expect( + listEquals(await v.versions(const []), const []), + 'versions([])', + ); + v.dispose(); + // getMany: an async list of optional records, launched concurrently. final results = await Future.wait([ for (var i = 0; i < 32; i++) s.getMany(['keep', 'gone', 'keep']), @@ -721,6 +829,70 @@ void objectGraph() { expectThrows(() => s.count(), "a disposed wrapper can't be used"); } +void directTransports() { + final base = liveCallbacks(); + final s = Store.open('/direct'); + s.putText('b', '12'); + s.put('a', [1, 2, 3], EntryKind.volatile, 7); + expect(s.count() == 2, 'count is a usize'); + expect(s.expiresAt('a') == 7, 'expiresAt(a)'); + expect(s.expiresAt('b') == null, 'expiresAt(b) is absent'); + expect(s.expiresAt('zzz') == null, 'expiresAt(zzz) is absent'); + final sizes = s.valueSizes(); + expect(listEquals(sizes, [3, 2]), 'valueSizes (got $sizes)'); + final expirations = s.expirations().toList(); + expect( + listEquals(expirations, [7, null]), + 'expirations (got $expirations)', + ); + s.dispose(); + + // rank: the scorer gets a typed array and returns one. + final r = Store.open('/rank'); + r.putText('a', '1'); + r.putText('b', '333'); + r.putText('c', '22'); + final scorer = TestScorer(); + final ranked = r.rank(scorer); + expect(listEquals(ranked, ['b', 'c', 'a']), 'rank (got $ranked)'); + expect(listEquals(scorer.seen!, [1, 3, 2]), 'scores(sizes) (got ${scorer.seen})'); + final broken = expectThrows( + () => r.rank(TestScorer(fail: true)), + 'rank with a failing scorer', + ); + expect( + broken.code == 1006 && broken.message_ == 'scorer is broken', + 'a failing scorer (got ${broken.message})', + ); + final short = expectThrows( + () => r.rank(TestScorer(drop: true)), + 'rank with too few scores', + ); + expect( + short.message_ == 'expected 3 scores, got 2', + 'a short score list (got ${short.message})', + ); + expect(liveCallbacks() == base, 'every scorer was released'); + r.dispose(); + + // importLines: `throws any`, failing with -1 and the producer's message. + final i = Store.open('/import'); + expect(i.importLines('a=1\n\nb=two\n') == 2, 'importLines stored 2'); + expect(text(i.get('b').value) == 'two', 'imported b'); + final bad = expectThrows( + () => i.importLines('c=3\nbroken\nd=4'), + 'importLines with a malformed line', + ); + expect( + bad.runtimeType == NativeException && + bad.code == NativeException.genericCode && + bad.message == 'line 2: expected key=value', + 'untyped error (got ${bad.runtimeType} ${bad.code}: ${bad.message})', + ); + expect(i.count() == 3 && i.find('d') == null, 'c was stored, d wasn\'t'); + i.dispose(); +} + void statsAndReport() { final s = Store.open('/stats'); s.putText('b', '12'); @@ -771,6 +943,7 @@ Future main() async { loaders(); await asyncCalls(); objectGraph(); + directTransports(); statsAndReport(); await expectNoLeaks('kvstore'); diff --git a/conformance/dotnet/Calculator.cs b/conformance/dotnet/Calculator.cs index ac5e0cce..abec4877 100644 --- a/conformance/dotnet/Calculator.cs +++ b/conformance/dotnet/Calculator.cs @@ -1,18 +1,26 @@ // Conformance consumer: calculator sample, .NET target. // -// Loading the generated project checks the ABI revision and the -// `calculator` contract table (the first call runs NativeMethods' static -// constructor). Covers wrapping addition, division rounding toward zero -// (including i32::MIN / -1), the typed CalcException.DivisionByZero (code 1, -// message "division by zero", no fields), and strings crossing as UTF-8 -// (pointer, length) pairs: empty, non-ASCII, astral, and an interior NUL. -// Ends by asserting the producer's leak counters are zero. +// CalculatorLibrary.Check() loads the producer and checks the ABI revision +// and the `calculator` contract table up front. Covers wrapping addition, +// division rounding toward zero (including i32::MIN / -1), the typed +// CalcException.DivisionByZero (code 1, message "division by zero", no +// fields), a second domain in the same module (ParseException.NotANumber, +// also code 1, with its `text` field), a `throws: any` function (the root +// NativeException with code -1), an optional scalar return (`double?`), +// typed arrays in and out (spans in, arrays out), and strings crossing as +// UTF-8 (pointer, length) pairs: empty, non-ASCII, astral, and an interior +// NUL. Ends by asserting the producer's leak counters are zero. +// +// Run with EXPECT_LOAD_FAILURE=1 and CALCULATOR_LIBRARY naming a file that +// doesn't exist, it checks instead that the load failure is a catchable +// NativeLoadException, from Check() and from every later call. // // The namespace is `Calculator` and the module is also `calculator`, so the // free-function class is `Calculator.Calculator`; `using static` imports // its statics. using System; +using System.Linq; using Calculator; using static Calculator.Calculator; @@ -27,7 +35,35 @@ static void Expect(bool cond, string msg) } } - static int Main() + static T Throws(Action action, string what) where T : Exception + { + try + { + action(); + } + catch (T e) + { + return e; + } + catch (Exception e) + { + Expect(false, $"{what}: expected {typeof(T).Name}, got {e.GetType().Name}: {e.Message}"); + } + Expect(false, $"{what}: expected {typeof(T).Name}, nothing thrown"); + return null; + } + + static int LoadFailure() + { + var first = Throws(CalculatorLibrary.Check, "Check() with a missing library"); + Expect(first.Message.Contains("CALCULATOR_LIBRARY"), $"the message names the variable (got '{first.Message}')"); + var again = Throws(() => Add(1, 2), "a call after a failed load"); + Expect(ReferenceEquals(first, again), "every call throws the same failure"); + Console.WriteLine("dotnet/calculator (load failure): OK"); + return 0; + } + + static void Arithmetic() { Expect(Add(2, 3) == 5, "add(2, 3)"); Expect(Add(int.MaxValue, 1) == int.MinValue, "add wraps"); @@ -35,22 +71,75 @@ static int Main() Expect(Divide(10, 2) == 5, "divide(10, 2)"); Expect(Divide(-7, 2) == -3, "divide rounds toward zero"); Expect(Divide(int.MinValue, -1) == int.MinValue, "divide(MIN, -1) wraps"); - try - { - Divide(1, 0); - Expect(false, "divide(1, 0) throws"); - } - catch (CalcException.DivisionByZero e) - { - Expect(e.Code == 1 && e.Code == CalcException.DivisionByZero.ErrorCode, $"code (got {e.Code})"); - Expect(e.Message == "division by zero", $"message (got '{e.Message}')"); - Expect(e is CalcException && e is NativeException, "hierarchy"); - } + var e = Throws(() => Divide(1, 0), "divide(1, 0)"); + Expect(e.Code == 1 && e.Code == CalcException.DivisionByZero.ErrorCode, $"code (got {e.Code})"); + Expect(e.Message == "division by zero", $"message (got '{e.Message}')"); + Expect(e is CalcException && e is NativeException, "hierarchy"); + } + + static void SecondDomain() + { + Expect(Parse("42") == 42 && Parse(" 42 ") == 42, "parse"); + // Both domains use code 1; the call's domain decides the type. + var bad = Throws(() => Parse("4x"), "parse(4x)"); + Expect(bad.Code == 1 && bad.Text == "4x", $"NotANumber fields (got {bad.Code}, '{bad.Text}')"); + Expect(bad.Message == "not a number: 4x", $"NotANumber message (got '{bad.Message}')"); + Expect(!((NativeException)bad is CalcException), "not the other domain"); + var empty = Throws(() => Parse(""), "parse(\"\")"); + Expect(empty.Text == "" && empty.Message == "not a number: ", $"empty text (got '{empty.Message}')"); + } + + static void Untyped() + { + Expect(Sqrt(9.0) == 3.0, "sqrt(9)"); + var e = Throws(() => Sqrt(-4.0), "sqrt(-4)"); + Expect(e.Code == NativeException.GenericErrorCode && e.Code == -1, $"code -1 (got {e.Code})"); + Expect(e.Message == "cannot take the square root of -4", $"message (got '{e.Message}')"); + Expect(e.GetType() == typeof(NativeException), "the root type itself"); + } + + static void Shapes() + { + Expect(Mean(new[] { 1.0, 2.0, 6.0 }) == 3.0, "mean present"); + Expect(Mean(ReadOnlySpan.Empty) == null, "mean absent"); + // A span over part of an array, pinned in place. + var values = new[] { 100.0, 4.0, 8.0, 100.0 }; + Expect(Mean(values.AsSpan(1, 2)) == 6.0, "mean of a slice"); + Expect(RunningTotal(new[] { 1, 2, 3 }).SequenceEqual(new[] { 1, 3, 6 }), "running_total"); + var none = RunningTotal(Array.Empty()); + Expect(none.Length == 0, "running_total([])"); + Expect(RunningTotal(new[] { 1, 2, 3, int.MaxValue }).SequenceEqual(new[] { 1, 3, 6, -2147483643 }), + "running_total wraps"); + Span stack = stackalloc int[] { 5, 5 }; + Expect(RunningTotal(stack).SequenceEqual(new[] { 5, 10 }), "running_total of stack memory"); + } + + static void Strings() + { Expect(Greet("World") == "Hello, World!", "greet"); Expect(Greet("") == "Hello, !", "greet empty"); Expect(Greet("Wörld 🦀") == "Hello, Wörld 🦀!", "greet non-ASCII and astral"); Expect(Greet("a\0b") == "Hello, a\0b!", "greet interior NUL"); + var big = new string('x', 100_000); + Expect(Greet(big) == "Hello, " + big + "!", "greet a large string"); + } + + static int Main() + { + if (Environment.GetEnvironmentVariable("EXPECT_LOAD_FAILURE") == "1") + { + return LoadFailure(); + } + CalculatorLibrary.Check(); + CalculatorLibrary.Check(); + Expect(CalculatorLibrary.AbiVersion == 5, "ABI revision 5"); + + Arithmetic(); + SecondDomain(); + Untyped(); + Shapes(); + Strings(); LeakCheck.AssertNoLeaks("calculator"); Console.WriteLine("dotnet/calculator: OK"); diff --git a/conformance/dotnet/Codec.cs b/conformance/dotnet/Codec.cs index e83c945d..9ebd9880 100644 --- a/conformance/dotnet/Codec.cs +++ b/conformance/dotnet/Codec.cs @@ -17,6 +17,15 @@ // 6. objects: token values, sum_holder twice over the same holder, // primary_of returning the same native object, identity (not value) // in same_primary, one wrapper in several slots; +// 7. records are value-equal: every object-free vector decoded twice is +// equal (lists, maps, and byte arrays compare by content) with equal +// hashes, and `with` makes an unequal copy; +// 8. the ABI 5 shapes: optional scalars (`int?`, `double?`, `bool?`, +// `Color?`) both ways, an undeclared optional enum value rejected +// (-3), typed arrays both ways (bit-identical floats, extreme +// integers, empty, a span over part of an array), a `usize`, a `char` +// (and its rejections), a custom type (and its lift failures), and an +// iterator of typed arrays, enumerated twice; // // and ends by asserting the producer's leak counters are zero. @@ -185,19 +194,19 @@ static void SpotChecks(uint n) Expect(c.Blob.Length == 6 && c.Blob[5] == 255, "composite blob"); Expect(c.SomeI64 == long.MinValue && c.NoneI64 == null, "composite optionals"); Expect(c.SomeText == "", "composite some_text is present and empty"); - Expect(c.Names.Length == 3 && c.Names[1] == "", "composite names"); - Expect(c.Matrix.Length == 3 && c.Matrix[1].Length == 0 && c.Matrix[2][0] == -4, "composite matrix"); - Expect(c.Floats.Length == 6 && double.IsNaN(c.Floats[0]) && double.IsNegative(c.Floats[3]), + Expect(c.Names.Count == 3 && c.Names[1] == "", "composite names"); + Expect(c.Matrix.Count == 3 && c.Matrix[1].Count == 0 && c.Matrix[2][0] == -4, "composite matrix"); + Expect(c.Floats.Count == 6 && double.IsNaN(c.Floats[0]) && double.IsNegative(c.Floats[3]), "composite floats"); Expect(c.ByName.Count == 4 && c.ById.Count == 3 && c.ByColor.Count == 2 && c.Flags.Count == 2, "composite map sizes"); Expect(c.Scalars.U32Value == 4_000_000_000U, "composite scalars"); Expect(c.Shape is Shape.Labeled { Count: 3 }, "composite shape"); - Expect(c.Shapes.Length == 6 && c.Shapes[5] is Shape.Nested { Note: null }, "composite shapes"); + Expect(c.Shapes.Count == 6 && c.Shapes[5] is Shape.Nested { Note: null }, "composite shapes"); Expect(c.MaybeShape is Shape.Nested, "composite maybe_shape"); Expect(c.MaybeList != null && c.MaybeList.Length == 2, "composite maybe_list"); - Expect(c.Sparse.Length == 3 && c.Sparse[1] == null && c.Sparse[0] == true, "composite sparse"); - Expect(c.Colors.Length == 4 && c.Colors[3] == Color.Infrared, "composite colors"); + Expect(c.Sparse.Count == 3 && c.Sparse[1] == null && c.Sparse[0] == true, "composite sparse"); + Expect(c.Colors.Count == 4 && c.Colors[3] == Color.Infrared, "composite colors"); } static void OutOfRange(uint n) @@ -245,7 +254,7 @@ static void Objects(uint n) { var full = Fetch(n, "objects full").Value; Expect(full.Primary.Value() == 10 && full.Spare != null && full.Spare.Value() == 11, "primary and spare"); - Expect(full.Many.Length == 3 && full.Many[2].Value() == long.MinValue, "many"); + Expect(full.Many.Count == 3 && full.Many[2].Value() == long.MinValue, "many"); Expect(full.ByName.Count == 2 && full.ByName["b"].Value() == 21, "by_name"); // Each encoding mints fresh references, so the holder can be sent twice. var expected = unchecked(10L + 11 + 12 + 13 + 20 + 21 + long.MinValue); @@ -277,8 +286,154 @@ static void Objects(uint n) } } + static bool HasObjects(Vector v) => v is Vector.Objects; + + static void ValueEquality(uint n) + { + for (uint i = 0; i < n; i++) + { + var a = C.Vector(i); + var b = C.Vector(i); + if (HasObjects(a)) + { + // Distinct wrappers of fresh references: equal only by identity. + Release(a); + Release(b); + continue; + } + var name = C.VectorName(i); + if (name.Contains("nan")) + { + // Record equality follows double.Equals, where NaN equals NaN. + Expect(a.Equals(b), $"{name}: NaN equals itself in a record"); + continue; + } + Expect(a == b && a.GetHashCode() == b.GetHashCode(), $"{name}: decoded twice is equal"); + Expect(!ReferenceEquals(a, b), $"{name}: two decodes are two objects"); + } + var deep = Fetch(n, "composite canonical").Value; + var copy = deep with { Names = deep.Names.ToList() }; + Expect(copy == deep && copy.GetHashCode() == deep.GetHashCode(), "a list compares by content"); + var changed = deep with { Names = deep.Names.Append("extra").ToList() }; + Expect(changed != deep, "a longer list differs"); + var blob = deep with { Blob = (byte[])deep.Blob.Clone() }; + Expect(blob == deep, "a byte array compares by content"); + blob.Blob[0] ^= 1; + Expect(blob != deep, "a changed byte differs"); + var map = deep with { ByName = new Dictionary(deep.ByName) }; + Expect(map == deep, "a map compares by content"); + } + + static void OptionalScalars() + { + Expect(C.EchoOptI32(null) == null, "echo_opt_i32(none)"); + Expect(C.EchoOptI32(int.MinValue) == int.MinValue, "echo_opt_i32(MIN)"); + Expect(C.EchoOptI32(0) == 0, "echo_opt_i32(0) is present"); + var negZero = C.EchoOptF64(-0.0); + Expect(negZero.HasValue && SameBits(negZero.Value, -0.0), "echo_opt_f64(-0.0) keeps the sign"); + Expect(C.EchoOptF64(double.NaN) is double nan && double.IsNaN(nan), "echo_opt_f64(NaN)"); + Expect(C.EchoOptF64(null) == null, "echo_opt_f64(none)"); + Expect(C.EchoOptBool(true) == true && C.EchoOptBool(false) == false && C.EchoOptBool(null) == null, + "echo_opt_bool"); + Expect(C.EchoOptColor(Color.Infrared) == Color.Infrared && C.EchoOptColor(Color.Blue) == Color.Blue, + "echo_opt_color"); + Expect(C.EchoOptColor(null) == null, "echo_opt_color(none)"); + var e = Throws(() => C.EchoOptColor((Color)3), "echo_opt_color(3)"); + Expect(e.Code == NativeException.MarshalErrorCode, $"an undeclared present value is -3 (got {e.Code})"); + } + + static void TypedArrays() + { + var floats = new[] { double.NaN, -0.0, 5e-324, double.PositiveInfinity }; + var echoed = C.EchoF64s(floats); + Expect(echoed.Length == 4, "echo_f64s length"); + for (var i = 0; i < floats.Length; i++) + { + Expect(SameBits(echoed[i], floats[i]), $"echo_f64s[{i}] is bit-identical"); + } + Expect(C.EchoI32s(new[] { int.MinValue, 0, int.MaxValue }).SequenceEqual(new[] { int.MinValue, 0, int.MaxValue }), + "echo_i32s"); + Expect(C.EchoI32s(Array.Empty()).Length == 0, "echo_i32s([])"); + var big = new[] { ulong.MaxValue, 1UL << 63 }; + Expect(C.EchoU64s(big).SequenceEqual(big), "echo_u64s"); + Expect(C.EchoU64s(default).Length == 0, "echo_u64s([])"); + // A span over the middle of an array crosses without a copy. + var window = new[] { 1, 2, 3, 4, 5 }; + Expect(C.EchoI32s(window.AsSpan(1, 3)).SequenceEqual(new[] { 2, 3, 4 }), "echo_i32s of a slice"); + var many = Enumerable.Range(0, 100_000).Select(i => (double)i).ToArray(); + Expect(C.EchoF64s(many).SequenceEqual(many), "echo_f64s of 100k elements"); + } + + static void Conversions() + { + Expect(C.EchoUsize(4_294_967_295UL) == 4_294_967_295UL, "echo_usize(u32::MAX)"); + Expect(C.EchoUsize(ulong.MaxValue) == ulong.MaxValue, "echo_usize(u64::MAX)"); + + Expect(C.EchoChar("🦀") == "🦀" && C.EchoChar("é") == "é" && C.EchoChar("a") == "a", "echo_char"); + var two = Throws(() => C.EchoChar("ab"), "echo_char(ab)"); + Expect(two.Code == -3 && two.Message.Contains("value: \"ab\" is not a valid char"), + $"echo_char(ab) message (got '{two.Message}')"); + var none = Throws(() => C.EchoChar(""), "echo_char(\"\")"); + Expect(none.Code == -3 && none.Message.Contains("value: \"\" is not a valid char"), + $"echo_char(\"\") message (got '{none.Message}')"); + + Expect(C.EchoHex("ff") == "ff" && C.EchoHex("00FF") == "ff" && C.EchoHex("0") == "0", "echo_hex"); + foreach (var (input, message) in new[] + { + ("xyz", "value: invalid digit found in string"), + ("", "value: cannot parse integer from empty string"), + ("100000000", "value: number too large to fit in target type"), + }) + { + var e = Throws(() => C.EchoHex(input), $"echo_hex({input})"); + Expect(e.Code == -3 && e.Message.Contains(message), $"echo_hex({input}) message (got '{e.Message}')"); + } + } + + static void Chunks() + { + var chunks = C.Chunks(new[] { int.MinValue, 0, int.MaxValue }, 2); + // The sequence is re-enumerable: each enumeration is a new native + // iterator over the same (copied) input. + for (var pass = 0; pass < 2; pass++) + { + var got = chunks.ToList(); + Expect(got.Count == 2 && got[0].SequenceEqual(new[] { int.MinValue, 0 }) + && got[1].SequenceEqual(new[] { int.MaxValue }), $"chunks pass {pass}"); + } + var pairs = C.Chunks(new[] { 1, 2, 3, 4 }, 2).ToList(); + Expect(pairs.Count == 2 && pairs[1].SequenceEqual(new[] { 3, 4 }), "chunks([1, 2, 3, 4], 2)"); + Expect(!C.Chunks(new[] { 1, 2 }, 0).Any(), "chunks(size 0) is empty"); + Expect(!C.Chunks(Array.Empty(), 3).Any(), "chunks([]) is empty"); + // Abandoning an enumeration part-way releases its iterator. + using (var e = chunks.GetEnumerator()) + { + Expect(e.MoveNext() && LeakCheck.Live("codec", LeakCheck.Iterators) == 1, "one live iterator"); + } + Expect(LeakCheck.Live("codec", LeakCheck.Iterators) == 0, "released on dispose"); + } + + static T Throws(Action action, string what) where T : Exception + { + try + { + action(); + } + catch (T e) + { + return e; + } + catch (Exception e) + { + Expect(false, $"{what}: expected {typeof(T).Name}, got {e.GetType().Name}: {e.Message}"); + } + Expect(false, $"{what}: expected {typeof(T).Name}, nothing thrown"); + return null; + } + static int Main() { + CodecLibrary.Check(); var n = C.VectorCount(); Expect(n >= 60, $"vector_count >= 60 (got {n})"); @@ -288,6 +443,11 @@ static int Main() OutOfRange(n); Malformed(); Objects(n); + ValueEquality(n); + OptionalScalars(); + TypedArrays(); + Conversions(); + Chunks(); LeakCheck.AssertNoLeaks("codec"); Console.WriteLine($"dotnet/codec: OK ({n} vectors)"); diff --git a/conformance/dotnet/Kvstore.cs b/conformance/dotnet/Kvstore.cs index c0276c87..f6efbd4b 100644 --- a/conformance/dotnet/Kvstore.cs +++ b/conformance/dotnet/Kvstore.cs @@ -28,7 +28,15 @@ // OperationCanceledException at once, its background work stopping // cooperatively), 32 concurrent async lists, and the nested module's // async function; -// * the nested KvStats class and the sibling Report root. +// * the nested KvStats class and the sibling Report root; +// * the ABI 5 shapes: an optional scalar (`long?`) parameter, return, +// iterator item, async result, and callback parameter and return; typed +// arrays as a return, an async result, and a callback parameter (a +// span) and return (an array); `usize` counts (`ulong`); a `throws: +// any` method (the root exception with code -1); callback failures the +// sample turns into KvException.CallbackFailed; records with value +// equality; re-enumerable iterators; and async calls that can't be +// cancelled abandoning the wait when their token fires. // // Callback releases are observed through the producer's live-callback // counter. Ends by asserting every leak counter is zero. @@ -101,6 +109,24 @@ public TestPolicy(Store other) _other = other; } + // An optional scalar in and out: "short" lives one tick, "forever" + // never expires, "ttl-fail" fails with a typed error, and every other + // key keeps the requested TTL. + public long? TtlFor(string key, long? requested) + { + switch (key) + { + case "short": + return 1; + case "forever": + return null; + case "ttl-fail": + throw new KvException.InvalidPath("no ttl for you"); + default: + return requested; + } + } + public Entry Admit(Entry entry) { Interlocked.Increment(ref Admitted); @@ -115,8 +141,7 @@ public Entry Admit(Entry entry) } // Tag it and store it encrypted (and try to rename it, which the // store ignores). - return new Entry("renamed", entry.Value, EntryKind.Encrypted, entry.Version, entry.ExpiresAt, - new[] { "admitted" }, entry.Metadata); + return entry with { Key = "renamed", Kind = EntryKind.Encrypted, Tags = new[] { "admitted" } }; } public Store Route(string key, Store home) @@ -162,6 +187,36 @@ public byte[] Load(string key) } } +internal sealed class TestScorer : IScorer +{ + public enum Mode { Sizes, Fail, Short } + + private readonly Mode _mode; + public ulong[] Seen; + + public TestScorer(Mode mode) + { + _mode = mode; + } + + // `sizes` is a span over the producer's typed array, valid for the call. + public double[] Scores(ReadOnlySpan sizes) + { + Seen = sizes.ToArray(); + if (_mode == Mode.Fail) + { + throw new InvalidOperationException("scorer is out of order"); + } + var n = _mode == Mode.Short ? 1 : sizes.Length; + var scores = new double[n]; + for (var i = 0; i < n; i++) + { + scores[i] = sizes[i] * 1.0; + } + return scores; + } +} + internal static class Program { // The thread synchronous calls notify on (async continuations hop threads, @@ -256,12 +311,17 @@ static void Basics() var e = Put(s, "alpha", "one"); Expect(e.Key == "alpha" && Text(e.Value) == "one" && e.Kind == EntryKind.Persistent && e.Version == 1, "first put"); - Expect(e.ExpiresAt == null && e.Tags.Length == 0 && e.Metadata.Count == 0, "first put defaults"); + Expect(e.ExpiresAt == null && e.Tags.Count == 0 && e.Metadata.Count == 0, "first put defaults"); e = Put(s, "alpha", "two", EntryKind.Volatile); Expect(e.Version == 2 && e.Kind == EntryKind.Volatile, "second put"); // get (throwing record) and find (optional record). Expect(Text(s.Get("alpha").Value) == "two", "get"); + // Records compare by value, byte arrays and collections included. + var first = s.Get("alpha"); + var second = s.Get("alpha"); + Expect(first == second && first.GetHashCode() == second.GetHashCode(), "entries are value-equal"); + Expect(first != (second with { Tags = new[] { "x" } }), "a different list differs"); var missing = Throws(() => s.Get("nope"), "get(nope)"); Expect(missing.Key == "nope" && missing.Message == "key not found: nope", $"KeyNotFound (got '{missing.Message}')"); Expect(s.Find("alpha")?.Version == 2, "find present"); @@ -306,7 +366,13 @@ static void Iterators() Put(s, "sys.x", "xx"); Expect(s.Keys(null).SequenceEqual(new[] { "sys.x", "user.alice", "user.bob" }), "keys in order"); - ExpectKeyNotFound(() => s.Keys("zzz"), "zzz", "keys(zzz)"); + // Each enumeration launches its own native iterator, so a sequence + // can be enumerated again, and a launch failure throws on enumeration. + var all = s.Keys(null); + Expect(all.SequenceEqual(all) && all.Count() == 3, "keys re-enumerates"); + var missing = s.Keys("zzz"); + ExpectKeyNotFound(() => missing.GetEnumerator(), "zzz", "keys(zzz)"); + ExpectKeyNotFound(() => missing.ToList(), "zzz", "keys(zzz) again"); // Abandoning an iterator part-way releases it. using (var keys = s.Keys("user.").GetEnumerator()) @@ -405,21 +471,30 @@ static void Policies() Expect(s.Count() == 1 && other.Count() == 1, "routed"); // A typed error from the throwing callback reaches the caller with - // its code, message, and fields. + // its code and fields, and the domain's own message for them. var rejected = Throws(() => Put(s, "secret", "3", EntryKind.Volatile), "secret"); - Expect(rejected.Code == 1005 && rejected.Message == "secrets are not stored", $"Rejected (got '{rejected.Message}')"); + Expect(rejected.Code == 1005 && rejected.Message == "write to secret rejected: no secrets", + $"Rejected (got '{rejected.Message}')"); Expect(rejected.Key == "secret" && rejected.Reason == "no secrets", "Rejected fields"); - // Any other exception arrives as -4 with the implementation's message. - var foreign = Throws(() => Put(s, "boom", "4", EntryKind.Volatile), "boom"); - Expect(foreign.Code == NativeException.ForeignErrorCode && foreign.Message == "policy exploded", - $"foreign failure (got {foreign.Code} '{foreign.Message}')"); - // A null required object is a return the runtime can't accept: -3. - var nullRoute = Throws(() => Put(s, "null/x", "6", EntryKind.Volatile), "null route"); - Expect(nullRoute.Code == NativeException.MarshalErrorCode, $"null route is -3 (got {nullRoute.Code})"); + // Any other exception is a callback failure, which the sample turns + // into CallbackFailed with the implementation's message. + var foreign = Throws(() => Put(s, "boom", "4", EntryKind.Volatile), "boom"); + Expect(foreign.Code == 1006 && foreign.Message == "policy exploded" && foreign.Message_ == "policy exploded", + $"callback failure (got {foreign.Code} '{foreign.Message}')"); + // So is a null required object, a return the runtime can't accept. + Throws(() => Put(s, "null/x", "6", EntryKind.Volatile), "null route"); Expect(s.Count() == 1 && other.Count() == 1, "failed puts store nothing"); Expect(p.Admitted == 5, $"admitted 5 times (got {p.Admitted})"); + // ttl_for: an optional scalar in and out, consulted before admit. + Expect(Put(s, "short", "x", EntryKind.Volatile).ExpiresAt == 1, "ttl_for short"); + Expect(Put(s, "forever", "x", EntryKind.Volatile, 5).ExpiresAt == null, "ttl_for forever"); + Expect(Put(s, "kept", "x", EntryKind.Volatile, 5).ExpiresAt == 5, "ttl_for keeps the request"); + var noTtl = Throws(() => Put(s, "ttl-fail", "x", EntryKind.Volatile), "ttl-fail"); + Expect(noTtl.Code == 1004 && noTtl.Message == "invalid path", $"ttl_for's typed error (got '{noTtl.Message}')"); + Expect(s.Count() == 4 && p.Admitted == 8, $"three more admitted (got {s.Count()}, {p.Admitted})"); + // Replacing the policy releases the old one; null removes it. Expect(LiveCallbacks() == baseline + 1, "one policy held"); s.SetPolicy(new TestPolicy(other)); @@ -427,7 +502,7 @@ static void Policies() s.SetPolicy(null); Expect(LiveCallbacks() == baseline && !s.HasPolicy(), "SetPolicy(null) releases it"); Put(s, "secret", "now allowed"); - Expect(s.Count() == 2, "no policy"); + Expect(s.Count() == 5, "no policy"); } static void Loaders() @@ -461,11 +536,10 @@ static void Loaders() Expect(s.GetOrLoad("missing", new TestLoader()) == null, "missing is none"); // KeyNotFound for another key: passed through, fields intact. var elsewhere = Throws(() => s.GetOrLoad("elsewhere", new TestLoader()), "elsewhere"); - Expect(elsewhere.Key == "other" && elsewhere.Message == "not in the loader", "passed through"); - // Any other failure is -4. - var broken = Throws(() => s.GetOrLoad("broken", new TestLoader()), "broken"); - Expect(broken.Code == NativeException.ForeignErrorCode && broken.Message == "loader is broken", - $"broken loader (got {broken.Code} '{broken.Message}')"); + Expect(elsewhere.Key == "other" && elsewhere.Message == "key not found: other", "passed through"); + // Any other failure is CallbackFailed with the loader's message. + var broken = Throws(() => s.GetOrLoad("broken", new TestLoader()), "broken"); + Expect(broken.Message == "loader is broken", $"broken loader (got {broken.Code} '{broken.Message}')"); Expect(LiveCallbacks() == baseline, "every loader is released"); } @@ -519,7 +593,7 @@ static async Task AsyncCalls() var many = Enumerable.Range(0, 32).Select(_ => s.GetMany(new[] { "keep", "gone", "keep" })).ToArray(); foreach (var got in await Task.WhenAll(many)) { - Expect(got.Length == 3 && got[0] != null && got[1] == null && got[2]?.Key == "keep", "get_many"); + Expect(got.Count == 3 && got[0] != null && got[1] == null && got[2]?.Key == "keep", "get_many"); } // The nested module's async free function: objects in a list in, a @@ -529,6 +603,28 @@ static async Task AsyncCalls() var st = await KvStats.SummarizeAll(new[] { s, other }); Expect(st.Entries == 2 && st.Bytes == 4, "summarize_all totals"); Expect(st.ByKind.Count == 1 && st.ByKind[EntryKind.Persistent] == 2, "summarize_all by_kind"); + + // A call that can't be cancelled stops waiting when its token fires; + // the native call finishes and its result (here a Store) is released. + await ThrowsAsync(() => Kv.OpenStore("/never", new CancellationToken(true)), + "pre-cancelled open_store"); + var abandoned = 0; + for (var i = 0; i < 32; i++) + { + using var cts = new CancellationTokenSource(); + var pending = Kv.OpenStore("/abandoned", cts.Token); + cts.Cancel(); + try + { + using var opened = await pending; + } + catch (OperationCanceledException) + { + abandoned++; + } + Expect(pending.IsCanceled || pending.IsCompletedSuccessfully, "abandoned or finished"); + } + Console.WriteLine($"dotnet/kvstore: {abandoned} of 32 open_store calls abandoned"); } static void ObjectGraph() @@ -570,7 +666,7 @@ static void ObjectGraph() // open_many(): a list of objects; one bad path fails the whole call. var many = Store.OpenMany(new[] { "/a", "/b" }); - Expect(many.Length == 2 && many[0].Path() == "/a" && many[1].Path() == "/b", "open_many"); + Expect(many.Count == 2 && many[0].Path() == "/a" && many[1].Path() == "/b", "open_many"); Throws(() => Store.OpenMany(new[] { "/a", "" }), "open_many with a bad path"); // by_label(): records with objects in, a map with object values out. @@ -614,8 +710,56 @@ static void StatsAndReport() Expect(nothing.Code == 2001 && nothing.Message == "nothing to report", "NothingToReport"); } + static async Task Abi5Shapes() + { + using var s = Store.Open("/abi5"); + Put(s, "b", "12"); + s.Put("a", new byte[] { 1, 2, 3 }, EntryKind.Volatile, 7); + Expect(s.Count() == 2UL, "count is a usize"); + + // An optional scalar return. + Expect(s.ExpiresAt("a") == 7 && s.ExpiresAt("b") == null && s.ExpiresAt("zzz") == null, "expires_at"); + // A typed-array return, in key order. + Expect(s.ValueSizes().SequenceEqual(new ulong[] { 3, 2 }), "value_sizes"); + // Optional scalar iterator items, enumerated twice. + var expirations = s.Expirations(); + for (var pass = 0; pass < 2; pass++) + { + Expect(expirations.SequenceEqual(new long?[] { 7, null }), $"expirations pass {pass}"); + } + // An optional scalar async result. + Expect(await s.VersionOf("a") == 1u && await s.VersionOf("q") == null, "version_of"); + // A typed-array async result. + Put(s, "b", "x"); + Expect((await s.Versions(new[] { "b", "q", "a" })).SequenceEqual(new uint[] { 2, 0, 1 }), "versions"); + + // A callback taking and returning typed arrays. + using var r = Store.Open("/rank"); + Put(r, "a", "1"); + Put(r, "b", "333"); + Put(r, "c", "22"); + var scorer = new TestScorer(TestScorer.Mode.Sizes); + Expect(r.Rank(scorer).SequenceEqual(new[] { "b", "c", "a" }), "rank"); + Expect(scorer.Seen.SequenceEqual(new ulong[] { 1, 3, 2 }), "the scorer saw the sizes"); + var failed = Throws(() => r.Rank(new TestScorer(TestScorer.Mode.Fail)), "failing scorer"); + Expect(failed.Message == "scorer is out of order", $"failing scorer message (got '{failed.Message}')"); + var shortScores = Throws(() => r.Rank(new TestScorer(TestScorer.Mode.Short)), "short scorer"); + Expect(shortScores.Message == "expected 3 scores, got 1", $"short scorer message (got '{shortScores.Message}')"); + + // `throws: any` and a `usize` return: the root exception, code -1. + using var imported = Store.Open("/import"); + Expect(imported.ImportLines("a=1\n\nb=two\n") == 2UL, "import_lines"); + Expect(Text(imported.Get("b").Value) == "two", "imported value"); + var untyped = Throws(() => imported.ImportLines("c=3\nbroken\nd=4"), "import_lines broken"); + Expect(untyped.GetType() == typeof(NativeException) && untyped.Code == NativeException.GenericErrorCode, + $"untyped failure (got {untyped.GetType().Name} {untyped.Code})"); + Expect(untyped.Message == "line 2: expected key=value", $"untyped message (got '{untyped.Message}')"); + Expect(imported.Count() == 3, "c was stored, d wasn't"); + } + static async Task Run() { + KvstoreLibrary.Check(); await Constructors(); Basics(); Iterators(); @@ -625,6 +769,7 @@ static async Task Run() await AsyncCalls(); ObjectGraph(); StatsAndReport(); + await Abi5Shapes(); } static int Main() diff --git a/conformance/dotnet/run.sh b/conformance/dotnet/run.sh index fcbd08bb..bc5a7bcd 100755 --- a/conformance/dotnet/run.sh +++ b/conformance/dotnet/run.sh @@ -40,7 +40,14 @@ EOF dotnet run -c Release --nologo -v quiet 2>&1 ) } -dotnet_calculator() { dotnet_consumer calculator Calculator.cs; } +# The calculator consumer also runs against a library path that doesn't +# exist, where it checks that the load failure is a catchable exception. +dotnet_calculator() { + dotnet_consumer calculator Calculator.cs || return 1 + ( cd "$OUT/dotnet-calculator" \ + && env EXPECT_LOAD_FAILURE=1 "$(library_env calculator)=$OUT/missing/libcalculator-missing.$EXT" \ + dotnet run -c Release --no-build --nologo -v quiet 2>&1 ) +} dotnet_codec() { dotnet_consumer codec Codec.cs; } dotnet_kvstore() { dotnet_consumer kvstore Kvstore.cs; } diff --git a/conformance/go/calculator.go b/conformance/go/calculator.go index ada165f3..fd88b23f 100644 --- a/conformance/go/calculator.go +++ b/conformance/go/calculator.go @@ -1,21 +1,29 @@ -// Conformance consumer: calculator sample, Go target (ABI revision 4). +// Conformance consumer: calculator sample, Go target (ABI revision 5). // -// The getting-started surface: a direct-value call (wrapping on overflow), -// a throwing call that returns the typed *DivisionByZeroError, and a string -// in and out (non-ASCII and astral text and an interior NUL byte survive). -// Importing the bindings checks the ABI revision and the contract table. -// Ends by asserting the library's leak counters are zero. +// The getting-started surface: Check (the ABI revision and the contract +// table), a direct-value call (wrapping on overflow), a throwing call that +// returns the typed *DivisionByZeroError, and a string in and out +// (non-ASCII and astral text and an interior NUL byte survive). Then the +// ABI 5 shapes: a second error domain whose code value 1 is also CalcError's +// (dispatched by the function's domain), a payload field, a function that +// fails with any error (*Error, code -1), an optional scalar return, and +// typed arrays in and out. Ends by asserting the library's leak counters are +// zero. package main import ( + "errors" "fmt" "math" + "slices" calc "__MODPATH__" ) func main() { + expect(calc.Check() == nil, fmt.Sprintf("Check() = %v", calc.Check())) + expect(calc.Add(2, 3) == 5, "add(2, 3)") expect(calc.Add(-7, 7) == 0, "add(-7, 7)") expect(calc.Add(math.MaxInt32, 1) == math.MinInt32, "add wraps") @@ -46,6 +54,47 @@ func main() { expect(got == want, fmt.Sprintf("greet(%q) = %q", name, got)) } + abi5() + expectNoLeaks(calc.DebugLive) fmt.Println("go/calculator: OK") } + +func abi5() { + // A second domain: ParseError's code 1 is not CalcError's code 1. + for _, text := range []string{"42", " 42 "} { + n, err := calc.Parse(text) + expect(err == nil && n == 42, fmt.Sprintf("parse(%q) = %d, %v", text, n, err)) + } + for _, text := range []string{"4x", ""} { + _, err := calc.Parse(text) + e := expectAs[*calc.NotANumberError](err, fmt.Sprintf("parse(%q)", text)) + expect(e.Code() == 1 && e.Text == text, fmt.Sprintf("NotANumber code %d text %q", e.Code(), e.Text)) + expect(e.Error() == "not a number: "+text, "NotANumber message: "+e.Error()) + expectAs[calc.ParseError](err, "a NotANumberError is a ParseError") + var calcErr calc.CalcError + expect(!errors.As(err, &calcErr), "a ParseError isn't a CalcError") + } + + // A function that fails with any error returns an *Error with code -1. + root, err := calc.Sqrt(9) + expect(err == nil && root == 3, fmt.Sprintf("sqrt(9) = %v, %v", root, err)) + _, err = calc.Sqrt(-4) + e := expectAs[*calc.Error](err, "sqrt(-4)") + expect(e.Code == -1 && e.Message == "cannot take the square root of -4", fmt.Sprintf("sqrt(-4): %d %q", e.Code, e.Message)) + + // An optional scalar return from a typed-array parameter. + m := calc.Mean([]float64{1, 2, 6}) + expect(m != nil && *m == 3, "mean([1, 2, 6])") + expect(calc.Mean(nil) == nil && calc.Mean([]float64{}) == nil, "mean([]) is absent") + + // Typed arrays in both directions. + expect(slices.Equal(calc.RunningTotal([]int32{1, 2, 3}), []int32{1, 3, 6}), "running_total([1, 2, 3])") + empty := calc.RunningTotal(nil) + expect(empty != nil && len(empty) == 0, "running_total([]) is an empty slice") + wrapped := calc.RunningTotal([]int32{1, 2, 3, math.MaxInt32}) + expect(slices.Equal(wrapped, []int32{1, 3, 6, -2147483643}), fmt.Sprintf("running_total wraps (got %v)", wrapped)) + // The input is lent, not copied or changed. + in := []int32{5, 5} + expect(slices.Equal(calc.RunningTotal(in), []int32{5, 10}) && slices.Equal(in, []int32{5, 5}), "the input is unchanged") +} diff --git a/conformance/go/codec.go b/conformance/go/codec.go index d891f23a..2a80abf5 100644 --- a/conformance/go/codec.go +++ b/conformance/go/codec.go @@ -1,4 +1,4 @@ -// Conformance consumer: codec sample, Go target (ABI revision 4). +// Conformance consumer: codec sample, Go target (ABI revision 5). // // The shared-vector loop: for every vector the library serves, decode it // through the generated bindings, hand it back to CheckVector (which must @@ -8,8 +8,11 @@ // spot checks of decoded fields, the typed *OutOfRangeError and its payload, // malformed input rejected (an undeclared enum value, text that isn't UTF-8, // a nil sum-type value the encoder refuses), and object identity and -// reference counting through buffers. Ends by asserting the library's leak -// counters are zero. +// reference counting through buffers. Then the ABI 5 shapes: optional +// scalars (*int32, *float64, *bool, *Color) and typed arrays ([]float64, +// []int32, []uint64) in and out, a usize, a char and a custom type crossing +// as strings (and rejected as -3), and an iterator of typed arrays. Ends by +// asserting the library's leak counters are zero. package main @@ -17,6 +20,7 @@ import ( "bytes" "fmt" "math" + "slices" codec "__MODPATH__" ) @@ -80,24 +84,37 @@ func echo(v codec.Vector) { expect(codec.EchoU16(x.Value) == x.Value, "echo u16") case codec.VectorI32: expect(codec.EchoI32(x.Value) == x.Value, "echo i32") + got := codec.EchoOptI32(&x.Value) + expect(got != nil && *got == x.Value, "echo opt i32") + expect(slices.Equal(codec.EchoI32s([]int32{x.Value}), []int32{x.Value}), "echo i32s") case codec.VectorU32: expect(codec.EchoU32(x.Value) == x.Value, "echo u32") case codec.VectorI64: expect(codec.EchoI64(x.Value) == x.Value, "echo i64") case codec.VectorU64: expect(codec.EchoU64(x.Value) == x.Value, "echo u64") + expect(codec.EchoUsize(x.Value) == x.Value, "echo usize") + expect(slices.Equal(codec.EchoU64s([]uint64{x.Value}), []uint64{x.Value}), "echo u64s") case codec.VectorF32: expect(sameF32(codec.EchoF32(x.Value), x.Value), "echo f32 (bitwise)") case codec.VectorF64: expect(sameF64(codec.EchoF64(x.Value), x.Value), "echo f64 (bitwise)") + got := codec.EchoOptF64(&x.Value) + expect(got != nil && (sameF64(*got, x.Value) || math.IsNaN(x.Value) && math.IsNaN(*got)), "echo opt f64") + echoed := codec.EchoF64s([]float64{x.Value}) + expect(len(echoed) == 1 && sameF64(echoed[0], x.Value), "echo f64s (bitwise)") case codec.VectorFlag: expect(codec.EchoBool(x.Value) == x.Value, "echo bool") + got := codec.EchoOptBool(&x.Value) + expect(got != nil && *got == x.Value, "echo opt bool") case codec.VectorText: expect(codec.EchoText(x.Value) == x.Value, fmt.Sprintf("echo text %q", x.Value)) case codec.VectorBlob: expect(bytes.Equal(codec.EchoBlob(x.Value), x.Value), "echo blob") case codec.VectorHue: expect(codec.EchoColor(x.Value) == x.Value, "echo color") + got := codec.EchoOptColor(&x.Value) + expect(got != nil && *got == x.Value, "echo opt color") } } @@ -284,6 +301,7 @@ func objects(n uint32) { } func main() { + expect(codec.Check() == nil, fmt.Sprintf("Check() = %v", codec.Check())) n := codec.VectorCount() expect(n >= 60, fmt.Sprintf("vector_count() = %d", n)) @@ -293,7 +311,103 @@ func main() { outOfRange(n) malformed() objects(n) + abi5() expectNoLeaks(codec.DebugLive) fmt.Printf("go/codec: OK (%d vectors)\n", n) } + +func abi5() { + // Optional scalars cross as a flag and a value. + expect(codec.EchoOptI32(nil) == nil, "echo_opt_i32(absent)") + for _, v := range []int32{math.MinInt32, 0} { + got := codec.EchoOptI32(&v) + expect(got != nil && *got == v, fmt.Sprintf("echo_opt_i32(%d)", v)) + } + negZero := math.Copysign(0, -1) + got := codec.EchoOptF64(&negZero) + expect(got != nil && sameF64(*got, negZero), "echo_opt_f64(-0.0) keeps the sign") + nan := math.NaN() + got = codec.EchoOptF64(&nan) + expect(got != nil && math.IsNaN(*got), "echo_opt_f64(NaN)") + expect(codec.EchoOptF64(nil) == nil, "echo_opt_f64(absent)") + for _, v := range []bool{true, false} { + b := codec.EchoOptBool(&v) + expect(b != nil && *b == v, fmt.Sprintf("echo_opt_bool(%v)", v)) + } + expect(codec.EchoOptBool(nil) == nil, "echo_opt_bool(absent)") + for _, c := range []codec.Color{codec.ColorInfrared, codec.ColorBlue} { + got := codec.EchoOptColor(&c) + expect(got != nil && *got == c, fmt.Sprintf("echo_opt_color(%d)", c)) + } + expect(codec.ColorInfrared == -1 && codec.ColorBlue == 7, "color values") + expect(codec.EchoOptColor(nil) == nil, "echo_opt_color(absent)") + bad := codec.Color(3) + expectMarshalFailure("echo_opt_color(3)", func() { codec.EchoOptColor(&bad) }) + + // Typed arrays: bit-identical floats, the extremes, and empty. + floats := []float64{math.NaN(), negZero, 5e-324, math.Inf(1)} + echoed := codec.EchoF64s(floats) + expect(len(echoed) == 4 && math.IsNaN(echoed[0]), "echo_f64s NaN") + for i := 1; i < 4; i++ { + expect(sameF64(echoed[i], floats[i]), fmt.Sprintf("echo_f64s[%d] bitwise", i)) + } + ints := []int32{math.MinInt32, 0, math.MaxInt32} + expect(slices.Equal(codec.EchoI32s(ints), ints), "echo_i32s extremes") + for _, empty := range [][]int32{nil, {}} { + e := codec.EchoI32s(empty) + expect(e != nil && len(e) == 0, "echo_i32s([])") + } + big := []uint64{math.MaxUint64, 1 << 63} + expect(slices.Equal(codec.EchoU64s(big), big), "echo_u64s extremes") + expect(len(codec.EchoU64s(nil)) == 0, "echo_u64s([])") + expect(len(codec.EchoF64s(nil)) == 0, "echo_f64s([])") + // A sub-slice passes its own window of the backing array. + window := []int32{9, 1, 2, 9}[1:3] + expect(slices.Equal(codec.EchoI32s(window), []int32{1, 2}), "echo_i32s(sub-slice)") + + // usize crosses as u64. + for _, v := range []uint64{4294967295, math.MaxUint64} { + expect(codec.EchoUsize(v) == v, fmt.Sprintf("echo_usize(%d)", v)) + } + + // A char is a one-scalar string. + for _, c := range []string{"\U0001F980", "é", "a"} { + expect(codec.EchoChar(c) == c, fmt.Sprintf("echo_char(%q)", c)) + } + expectMarshalMessage("echo_char(ab)", "value: \"ab\" is not a valid char", func() { codec.EchoChar("ab") }) + expectMarshalMessage("echo_char()", "value: \"\" is not a valid char", func() { codec.EchoChar("") }) + + // A custom type crossing as a string, normalized by the producer. + for in, want := range map[string]string{"ff": "ff", "00FF": "ff", "0": "0"} { + expect(codec.EchoHex(in) == want, fmt.Sprintf("echo_hex(%q)", in)) + } + expectMarshalMessage("echo_hex(xyz)", "value: invalid digit found in string", func() { codec.EchoHex("xyz") }) + expectMarshalMessage("echo_hex()", "value: cannot parse integer from empty string", func() { codec.EchoHex("") }) + expectMarshalMessage("echo_hex(100000000)", "value: number too large to fit in target type", func() { codec.EchoHex("100000000") }) + + // An iterator of typed arrays. + chunks := func(values []int32, size uint32) [][]int32 { + var out [][]int32 + for c := range codec.Chunks(values, size) { + out = append(out, c) + } + return out + } + expect(slices.EqualFunc(chunks(ints, 2), [][]int32{{math.MinInt32, 0}, {math.MaxInt32}}, slices.Equal), "chunks(extremes, 2)") + expect(slices.EqualFunc(chunks([]int32{1, 2, 3, 4}, 2), [][]int32{{1, 2}, {3, 4}}, slices.Equal), "chunks([1 2 3 4], 2)") + expect(len(chunks([]int32{1, 2}, 0)) == 0 && len(chunks(nil, 3)) == 0, "empty chunks") + for c := range codec.Chunks([]int32{1, 2, 3, 4}, 1) { + expect(slices.Equal(c, []int32{1}), "the first chunk") + break // an abandoned iterator is released + } + expect(codec.DebugLive(2) == 0, "every chunks iterator was released") +} + +// expectMarshalMessage asserts that f panics with the runtime error for a +// value the library can't take (-3) carrying message. +func expectMarshalMessage(what, message string, f func()) { + r := catchPanic(f) + e, ok := r.(*codec.Error) + expect(ok && e.Code == -3 && e.Message == message, fmt.Sprintf("%s: want -3 %q, got %v", what, message, r)) +} diff --git a/conformance/go/kvstore.go b/conformance/go/kvstore.go index 89cfb965..1c90baa9 100644 --- a/conformance/go/kvstore.go +++ b/conformance/go/kvstore.go @@ -1,27 +1,30 @@ -// Conformance consumer: kvstore sample, Go target (ABI revision 4). +// Conformance consumer: kvstore sample, Go target (ABI revision 5). // // Drives the feature-complete producer through the generated bindings: // // - the load-time checks (ABI revision, both modules' contract tables), -// which importing the package performs; +// which Check reports; // - Store: the throwing OpenStore and plain NewStore factories, methods, // statics (StoreOpenMany, StoreDefaultCapacity, ...), the deprecated // Size, the Entry and StoreInfo structs, the EntryKind constants, maps, // optionals as pointers, and the logical clock; // - KvError code types with their payload fields (*KeyNotFoundError, -// *ExpiredError, *StoreFullError, *InvalidPathError, *RejectedError), -// matched with errors.As, and runtime failures (-3, -4) as *Error; +// *ExpiredError, *StoreFullError, *InvalidPathError, *RejectedError, +// *CallbackFailedError), matched with errors.As, runtime failures (-3) +// as *Error, and a function that fails with any error (*Error, -1); // - lazy iter.Seq2 and iter.Seq sequences of strings (throwing at launch), // records, and objects, including one abandoned part-way; -// - three callback interfaces implemented in Go: a Listener (retained, +// - four callback interfaces implemented in Go: a Listener (retained, // filtered by Accepts, told about every Change, detached when it -// panics, and called on a producer thread during compaction), a Policy -// (a record return, a throwing method whose *RejectedError reaches the -// Put caller with its payload, a plain error that arrives as -4, a -// malformed record return and a nil required object rejected as -3, an -// object parameter and object return), and a Loader passed as an -// optional callback (string, bytes, and optional-object returns; typed -// errors decoded by the producer or passed through); +// panics or fails, and called on a producer thread during compaction), +// a Policy (an optional scalar in and out, a record return, a typed +// *RejectedError that reaches the Put caller with its payload, a plain +// error, a malformed record return, and a nil required object that +// arrive as *CallbackFailedError, an object parameter and object +// return), a Loader passed as an optional callback (string, bytes, and +// optional-object returns; typed errors decoded by the producer or +// passed through; a typed nil passes no loader), and a Scorer (typed +// arrays in and out); // - Store objects in every position: parameter, return, optional, list, // map value, record field, sequence element, async result, and callback // parameter and return; @@ -31,7 +34,9 @@ // cooperatively, shown by StoreActiveJobs), an async list launched from // 32 goroutines, and an async function in the nested kv.stats module; // - the sibling report root (the shared Entry record and its own error -// domain). +// domain); +// - the ABI 5 shapes: an optional scalar return, iterator item, and async +// result, typed-array returns and async results, and usize counts. // // Releases of consumer callbacks are observed through the producer's // callback counter (DebugLive(1)). Ends by asserting the producer's leak @@ -99,37 +104,33 @@ func expectKeyNotFound(err error, key string) { expect(e.Code() == 1001 && e.Key == key, fmt.Sprintf("KeyNotFound key %q (got %q)", key, e.Key)) } -// expectRuntime asserts that err is the runtime *Error with code. -func expectRuntime(err error, code int32, message string) { - e := expectAs[*kv.Error](err, fmt.Sprintf("code %d", code)) - expect(e.Code == code && e.Message == message, fmt.Sprintf("want %d %q, got %d %q", code, message, e.Code, e.Message)) - var domain kv.KvError - expect(!errors.As(err, &domain), "a runtime failure isn't a KvError") -} - // ── listener (consumer-implemented, retained) ───────────────────────────── type recordingListener struct { - skip, failOn string - mu sync.Mutex - changes []kv.Change - offMain atomic.Int32 + skip, failOn, errOn string + mu sync.Mutex + changes []kv.Change + offMain atomic.Int32 } -func (l *recordingListener) Accepts(key string) bool { +func (l *recordingListener) Accepts(key string) (bool, error) { if l.failOn != "" && key == l.failOn { panic("listener refused") } - return key != l.skip + if l.errOn != "" && key == l.errOn { + return false, errors.New("listener declined") + } + return key != l.skip, nil } -func (l *recordingListener) OnChange(change kv.Change) { +func (l *recordingListener) OnChange(change kv.Change) error { if C.on_main_thread() == 0 { l.offMain.Add(1) } l.mu.Lock() defer l.mu.Unlock() l.changes = append(l.changes, change) + return nil } func (l *recordingListener) last() kv.Change { @@ -170,6 +171,20 @@ type testPolicy struct { admitted atomic.Int32 } +// TTLFor: "short" lives one tick, "forever" never expires, "ttl-fail" fails +// with a typed error, and every other key keeps the requested TTL. +func (p *testPolicy) TTLFor(key string, requested *int64) (*int64, error) { + switch key { + case "short": + return ptr(int64(1)), nil + case "forever": + return nil, nil + case "ttl-fail": + return nil, &kv.InvalidPathError{Message: "no ttl for you"} + } + return requested, nil +} + func (p *testPolicy) Admit(entry kv.Entry) (kv.Entry, error) { p.admitted.Add(1) expect(entry.Version == 0, "the store assigns the version after admission") @@ -194,29 +209,29 @@ func (p *testPolicy) Admit(entry kv.Entry) (kv.Entry, error) { // Route receives home as a wrapper of its own; the bindings hand the // producer a fresh reference to whatever it returns. -func (p *testPolicy) Route(key string, home *kv.Store) *kv.Store { +func (p *testPolicy) Route(key string, home *kv.Store) (*kv.Store, error) { switch { case strings.HasPrefix(key, "b/"): home.Close() - return p.other + return p.other, nil case strings.HasPrefix(key, "null/"): home.Close() - return nil // a required object may not be nil: -3 + return nil, nil // a required object may not be nil } - return home + return home, nil } // ── loader (consumer-implemented, passed as an optional parameter) ──────── type testLoader struct{ backup *kv.Store } -func (testLoader) Name() string { return "go-loader" } +func (testLoader) Name() (string, error) { return "go-loader", nil } -func (l testLoader) Fallback(key string) *kv.Store { +func (l testLoader) Fallback(key string) (*kv.Store, error) { if key == "fb" { - return l.backup + return l.backup, nil } - return nil + return nil, nil } func (testLoader) Load(key string) ([]byte, error) { @@ -231,6 +246,38 @@ func (testLoader) Load(key string) ([]byte, error) { return []byte("loaded:" + key), nil } +// ── scorer (consumer-implemented, typed arrays in and out) ──────────────── + +type testScorer struct { + mode string + seen []uint64 +} + +func (sc *testScorer) Scores(sizes []uint64) ([]float64, error) { + sc.seen = slices.Clone(sizes) + switch sc.mode { + case "fail": + return nil, errors.New("scorer is out of order") + case "short": + return []float64{1}, nil + } + scores := make([]float64, len(sizes)) + for i, n := range sizes { + scores[i] = float64(n) + } + return scores, nil +} + +// expectCallbackFailed asserts that err is the CallbackFailed code a +// consumer callback's failure becomes, with message unless it's empty. +func expectCallbackFailed(err error, message string) { + e := expectAs[*kv.CallbackFailedError](err, "CallbackFailed") + expect(e.Code() == 1006 && e.Message_ != "", fmt.Sprintf("CallbackFailed code %d message %q", e.Code(), e.Message_)) + if message != "" { + expect(e.Message_ == message && e.Error() == message, fmt.Sprintf("CallbackFailed: want %q, got %q / %q", message, e.Message_, e.Error())) + } +} + // ── sections ────────────────────────────────────────────────────────────── func constructors() { @@ -305,7 +352,7 @@ func basics() { put(s, "beta", "b") expect(s.Delete("beta") && !s.Delete("beta"), "delete twice") size := s.Size() //nolint:staticcheck // the deprecated method still works - expect(size == s.Count() && size == 1, "deprecated Size == Count") + expect(uint64(size) == s.Count() && size == 1, "deprecated Size == Count") expect(s.Clear() == 1 && s.Count() == 0, "clear") } @@ -348,7 +395,7 @@ func iterators() { // Partition: objects, created as they're pulled. prefixes := []string{"user.", "sys.", "none."} - counts := []uint32{2, 1, 0} + counts := []uint64{2, 1, 0} i := 0 for part := range s.Partition(prefixes) { expect(part.Count() == counts[i] && part.Path() == prefixes[i], fmt.Sprintf("partition %d", i)) @@ -402,8 +449,17 @@ func listeners() { expect(s.Count() == 2 && s.ListenerCount() == 0, "a failing listener is detached") expect(liveCallbacks() == base, "the failing listener was released") - // A required callback can't be nil. + // So is one that returns an error. + declining := &recordingListener{errOn: "no"} + s.Subscribe(declining) + put(s, "no", "1") + expect(s.Count() == 3 && s.ListenerCount() == 0 && liveCallbacks() == base, "a declining listener is detached") + + // A required callback can't be nil, nor a typed nil. expect(catchPanic(func() { s.Subscribe(nil) }) != nil, "Subscribe(nil) panics") + var typedNil *recordingListener + expect(catchPanic(func() { s.Subscribe(typedNil) }) != nil, "Subscribe(typed nil) panics") + expect(s.ListenerCount() == 0 && liveCallbacks() == base, "no nil listener was attached") // Closing the store releases the listeners it still holds. s.Subscribe(&recordingListener{}) @@ -430,31 +486,43 @@ func policies() { _, err = putKind(s, "b/x", "2", kv.EntryKindVolatile, nil) expect(err == nil && s.Count() == 1 && other.Count() == 1, "route redirected b/x") - // A typed error from the throwing callback reaches the caller with its - // code, message, and payload. + // A typed error from the callback reaches the caller with its code and + // payload; the producer renders the message from the fields. _, err = putKind(s, "secret", "3", kv.EntryKindVolatile, nil) r := expectAs[*kv.RejectedError](err, "put(secret)") - expect(r.Code() == 1005 && r.Error() == "secrets are not stored", "Rejected code and message") + expect(r.Code() == 1005 && r.Error() == "write to secret rejected: no secrets", "Rejected code and message: "+r.Error()) expect(r.Key == "secret" && r.Reason == "no secrets", "Rejected payload") - // Any other error arrives as -4 with the consumer's message. + // Any other error becomes CallbackFailed with the consumer's message. _, err = putKind(s, "boom", "4", kv.EntryKindVolatile, nil) - expectRuntime(err, -4, "policy exploded") - // A return the producer can't accept is -3: a malformed record or a nil - // required object. + expectCallbackFailed(err, "policy exploded") + // So does a return the producer can't accept: a malformed record or a + // nil required object. _, err = putKind(s, "garbage", "5", kv.EntryKindVolatile, nil) - expect(expectAs[*kv.Error](err, "put(garbage)").Code == -3, "a malformed admit return is -3") + expectCallbackFailed(err, "") _, err = putKind(s, "null/x", "6", kv.EntryKindVolatile, nil) - expect(expectAs[*kv.Error](err, "put(null/x)").Code == -3, "a nil route return is -3") + expectCallbackFailed(err, "") expect(s.Count() == 1 && other.Count() == 1 && p.admitted.Load() == 6, "failed puts changed nothing") + // TTLFor: an optional scalar in and out, consulted before Admit. + e, err := putKind(s, "short", "x", kv.EntryKindVolatile, nil) + expect(err == nil && e.ExpiresAt != nil && *e.ExpiresAt == 1, "ttl_for overrides none") + e, err = putKind(s, "forever", "x", kv.EntryKindVolatile, ptr(int64(5))) + expect(err == nil && e.ExpiresAt == nil, "ttl_for removes the TTL") + e, err = putKind(s, "kept", "x", kv.EntryKindVolatile, ptr(int64(5))) + expect(err == nil && e.ExpiresAt != nil && *e.ExpiresAt == 5, "ttl_for keeps the TTL") + _, err = putKind(s, "ttl-fail", "x", kv.EntryKindVolatile, nil) + invalid := expectAs[*kv.InvalidPathError](err, "put(ttl-fail)") + expect(invalid.Error() == "invalid path", "InvalidPath message: "+invalid.Error()) + expect(s.Count() == 4 && p.admitted.Load() == 9, "ttl_for runs before admit") + // Replacing the policy releases the old one; nil removes it. s.SetPolicy(&testPolicy{other: other.Share()}) expect(liveCallbacks() == base+1, "replacing the policy released the old one") s.SetPolicy(nil) expect(!s.HasPolicy() && liveCallbacks() == base, "SetPolicy(nil) released it") put(s, "secret", "now allowed") - expect(s.Count() == 2, "no policy, no veto") + expect(s.Count() == 5, "no policy, no veto") other.Close() s.Close() @@ -465,9 +533,12 @@ func loaders() { defer s.Close() base := liveCallbacks() - // No loader (a nil optional callback): a miss is none. + // No loader (a nil optional callback, or a typed nil): a miss is none. e, err := s.GetOrLoad("k", nil) expect(err == nil && e == nil, "no loader") + var typedNil *testLoader + e, err = s.GetOrLoad("k", typedNil) + expect(err == nil && e == nil && liveCallbacks() == base, "a typed nil passes no loader") // Load's bytes are stored, tagged with the loader's name. e, err = s.GetOrLoad("k", testLoader{}) @@ -490,13 +561,14 @@ func loaders() { // answers none. e, err = s.GetOrLoad("missing", testLoader{}) expect(err == nil && e == nil, "KeyNotFound for the same key is none") - // KeyNotFound for another key: passed through, payload intact. + // KeyNotFound for another key: passed through, payload intact, the + // message rendered from it. _, err = s.GetOrLoad("elsewhere", testLoader{}) expectKeyNotFound(err, "other") - expect(err.Error() == "not in the loader", "the loader's message passed through") - // Any other error is -4. + expect(err.Error() == "key not found: other", "the domain's message: "+err.Error()) + // Any other error is CallbackFailed with the loader's message. _, err = s.GetOrLoad("broken", testLoader{}) - expectRuntime(err, -4, "loader is broken") + expectCallbackFailed(err, "loader is broken") expect(liveCallbacks() == base, "every loader was released") } @@ -679,6 +751,9 @@ func statsAndReport() { } func main() { + expect(kv.Check() == nil, fmt.Sprintf("Check() = %v", kv.Check())) + expect(kv.Check() == kv.Check(), "Check reports one result") + constructors() basics() iterators() @@ -688,7 +763,78 @@ func main() { asyncCalls() objectGraph() statsAndReport() + abi5() expectNoLeaks(kv.DebugLive) fmt.Println("go/kvstore: OK") } + +func abi5() { + ctx := context.Background() + s, _ := kv.OpenStore("/abi5") + _, err := putKind(s, "b", "12", kv.EntryKindPersistent, nil) + expect(err == nil, "put b") + _, err = s.Put("a", []byte{1, 2, 3}, kv.EntryKindVolatile, ptr(int64(7))) + expect(err == nil && s.Count() == 2, "put a") + + // An optional scalar return. + at := s.ExpiresAt("a") + expect(at != nil && *at == 7, "expires_at(a)") + expect(s.ExpiresAt("b") == nil && s.ExpiresAt("zzz") == nil, "expires_at(b), expires_at(zzz)") + + // A typed-array return, in key order. + expect(slices.Equal(s.ValueSizes(), []uint64{3, 2}), fmt.Sprintf("value_sizes (got %v)", s.ValueSizes())) + + // Optional scalar iterator items. + var expirations []*int64 + for e := range s.Expirations() { + expirations = append(expirations, e) + } + expect(len(expirations) == 2 && *expirations[0] == 7 && expirations[1] == nil, "expirations") + expect(kv.DebugLive(2) == 0, "the expirations iterator was released") + + // An optional scalar async result. + v, err := s.VersionOf(ctx, "a") + expect(err == nil && v != nil && *v == 1, fmt.Sprintf("version_of(a) = %v, %v", v, err)) + v, err = s.VersionOf(ctx, "q") + expect(err == nil && v == nil, "version_of(q) is absent") + + // A typed-array async result. + put(s, "b", "x") + versions, err := s.Versions(ctx, []string{"b", "q", "a"}) + expect(err == nil && slices.Equal(versions, []uint32{2, 0, 1}), fmt.Sprintf("versions = %v, %v", versions, err)) + versions, err = s.Versions(ctx, nil) + expect(err == nil && versions != nil && len(versions) == 0, "versions([])") + s.Close() + + // A callback taking and returning typed arrays. + s, _ = kv.OpenStore("/rank") + put(s, "a", "1") + put(s, "b", "333") + put(s, "c", "22") + base := liveCallbacks() + sc := &testScorer{} + ranked, err := s.Rank(sc) + expect(err == nil && slices.Equal(ranked, []string{"b", "c", "a"}), fmt.Sprintf("rank = %v, %v", ranked, err)) + expect(slices.Equal(sc.seen, []uint64{1, 3, 2}), fmt.Sprintf("the scorer saw %v", sc.seen)) + _, err = s.Rank(&testScorer{mode: "fail"}) + expectCallbackFailed(err, "scorer is out of order") + _, err = s.Rank(&testScorer{mode: "short"}) + expectCallbackFailed(err, "expected 3 scores, got 1") + expect(liveCallbacks() == base, "every scorer was released") + s.Close() + + // A function that fails with any error, counting with a usize. + s, _ = kv.OpenStore("/import") + n, err := s.ImportLines("a=1\n\nb=two\n") + expect(err == nil && n == 2, fmt.Sprintf("import_lines = %d, %v", n, err)) + got, err := s.Get("b") + expect(err == nil && string(got.Value) == "two", "imported b") + _, err = s.ImportLines("c=3\nbroken\nd=4") + e := expectAs[*kv.Error](err, "import_lines(broken)") + expect(e.Code == -1 && e.Message == "line 2: expected key=value", fmt.Sprintf("import_lines: %d %q", e.Code, e.Message)) + var domain kv.KvError + expect(!errors.As(err, &domain), "an untyped failure isn't a KvError") + expect(s.Count() == 3 && s.Find("c") != nil && s.Find("d") == nil, "c was stored, d wasn't") + s.Close() +} diff --git a/conformance/go/run.sh b/conformance/go/run.sh index 63048e77..e28f1ec2 100755 --- a/conformance/go/run.sh +++ b/conformance/go/run.sh @@ -9,7 +9,7 @@ require_tools go go # Run a Go consumer in a throwaway module that requires the generated module # as-is (a `replace` points at its directory). The module path follows the -# package identity or the configured `module_path`, so it's read from the +# package identity or the configured `name`, so it's read from the # generated go.mod and substituted for the consumer's `__MODPATH__` import # sentinel. The generated module ships its C header and links # `-l`; only the library directory is supplied. @@ -24,7 +24,7 @@ go_consumer() { cp "$ROOT/conformance/go/common.go" "$moddir/common.go" cat > "$moddir/go.mod" < $GENROOT/$sample/go EOF diff --git a/conformance/kotlin/calculator.kt b/conformance/kotlin/calculator.kt index 6237f53a..06c368cb 100644 --- a/conformance/kotlin/calculator.kt +++ b/conformance/kotlin/calculator.kt @@ -2,18 +2,27 @@ // // The getting-started surface through the generated `Calculator` object: a // direct-value call (wrapping at the `Int` edges), a throwing call that -// raises the typed `CalcException.DivisionByZero`, and a string in and out -// (non-ASCII and astral text and an interior NUL survive). Loading the -// bindings checks the ABI revision and the `calculator` contract. Ends by -// asserting the producer's leak counters are zero. +// raises the typed `CalcException.DivisionByZero`, a string in and out +// (non-ASCII and astral text and an interior NUL survive), a second error +// domain whose code value collides with the first's (`ParseException`), +// an untyped `throws: any` failure (`FfiException` code -1), an optional +// scalar return (`Double?`), and typed arrays in and out. Open domains: an +// unknown positive code maps to the domain's base exception. Loading the +// bindings checks the ABI revision and the `calculator` contract (see +// load_failure.kt for a failed load). Ends by asserting the producer's leak +// counters are zero. @file:JvmName("Main") import calculator.CalcException import calculator.Calculator import calculator.FfiException import calculator.JniBridge +import calculator.NativeLibrary +import calculator.ParseException fun main() { + NativeLibrary.load() + NativeLibrary.load() // loading twice is a no-op expect(JniBridge.debug_live(-1) == 1L, "the sample counts live allocations") expect(Calculator.add(2, 3) == 5, "add(2, 3)") @@ -34,6 +43,56 @@ fun main() { expect(Calculator.greet("Wörld 🦀") == "Hello, Wörld 🦀!", "greet keeps non-ASCII and astral text") expect(Calculator.greet("a\u0000b") == "Hello, a\u0000b!", "greet keeps an interior NUL") + // A second domain: its code 1 is NotANumber, not DivisionByZero. + expect(Calculator.parse("42") == 42 && Calculator.parse(" 42 ") == 42, "parse(42)") + val nan = thrownBy { Calculator.parse("4x") } + expect(nan is ParseException.NotANumber, "parse(4x) raises NotANumber (got $nan)") + nan as ParseException.NotANumber + expect(nan.code == 1 && nan.text == "4x" && nan.message == "not a number: 4x", "NotANumber payload and message") + val empty = thrownBy { Calculator.parse("") } + expect( + empty is ParseException.NotANumber && empty.text == "" && empty.message == "not a number: ", + "parse(\"\") raises NotANumber with empty text (got $empty)", + ) + + // throws: any + expect(Calculator.sqrt(9.0) == 3.0, "sqrt(9)") + val neg = thrownBy { Calculator.sqrt(-4.0) } + expect( + neg is FfiException && neg !is CalcException && neg !is ParseException && neg.code == -1, + "sqrt(-4) raises an untyped FfiException with code -1 (got $neg)", + ) + expect(neg?.message == "cannot take the square root of -4", "sqrt(-4) message (got ${neg?.message})") + + // An optional scalar return. + expect(Calculator.mean(listOf(1.0, 2.0, 6.0)) == 3.0, "mean([1, 2, 6])") + expect(Calculator.mean(emptyList()) == null, "mean([]) is absent") + + // Typed arrays in and out. + expect(Calculator.runningTotal(listOf(1, 2, 3)) == listOf(1, 3, 6), "runningTotal([1, 2, 3])") + expect(Calculator.runningTotal(emptyList()).isEmpty(), "runningTotal([])") + expect( + Calculator.runningTotal(listOf(1, 2, 3, Int.MAX_VALUE)) == listOf(1, 3, 6, -2147483643), + "runningTotal wraps", + ) + val big = List(1000) { it } + expect(Calculator.runningTotal(big).last() == 499500, "runningTotal of a heap-copied array") + + // Open domains: a code these bindings don't know maps to the domain's + // base exception with its code and message (domains 2 and 3 are + // CalcError and ParseError, in declaration order). + val unknown = JniBridge.error(2, 77, "from the future".toByteArray(), null) + expect( + unknown is CalcException && unknown !is CalcException.DivisionByZero && unknown.code == 77, + "an unknown CalcError code is a plain CalcException (got $unknown)", + ) + expect(unknown.message == "from the future", "the unknown code keeps its message") + val unknownParse = JniBridge.error(3, 1, "no payload".toByteArray(), null) + expect( + unknownParse is ParseException && unknownParse !is ParseException.NotANumber, + "a payload-less NotANumber falls back to the base ParseException (got $unknownParse)", + ) + expectNoLeaks(JniBridge::debug_live) println("kotlin/calculator: OK") } diff --git a/conformance/kotlin/codec.kt b/conformance/kotlin/codec.kt index 4adcbb80..afba4cff 100644 --- a/conformance/kotlin/codec.kt +++ b/conformance/kotlin/codec.kt @@ -10,7 +10,9 @@ // payload, malformed input rejected as marshalling failures (through the // internal JNI bridge, which takes raw bytes), and object identity and // reference counting through buffers. Ends by asserting the producer's leak -// counters are zero. Compiled with `-Xfriend-paths`, so the bindings' +// counters are zero. Also the ABI 5 shapes: optional scalars, typed arrays +// (in, out, and as iterator items), `usize`, `char`, a custom type, and +// content equality of records holding bytes. Compiled with `-Xfriend-paths`, so the bindings' // `internal` codec and bridge are reachable. @file:JvmName("Main") @@ -28,8 +30,8 @@ import codec.Token import codec.Vector import codec.decodeBuffer import codec.encodeBuffer -import codec.packVector -import codec.unpackVector +import codec.pack_Vector +import codec.unpack_Vector /** Release the tokens a decoded vector carries (only `Objects` has any). */ fun release(v: Vector) { @@ -38,11 +40,11 @@ fun release(v: Vector) { h.primary.close() h.spare?.close() h.many.forEach { it.close() } - h.by_name.values.forEach { it.close() } + h.byName.values.forEach { it.close() } } } -fun encode(v: Vector): ByteArray = encodeBuffer { packVector(it, v) } +fun encode(v: Vector): ByteArray = encodeBuffer { pack_Vector(it, v) } /** The index of the vector named [name]. */ fun find(n: UInt, name: String): UInt { @@ -53,27 +55,46 @@ fun find(n: UInt, name: String): UInt { return 0u } -/** Push a primitive vector's value through its direct-family echo. */ +/** + * Push a primitive vector's value through its direct-family echo, and the + * ones that also cross as an optional scalar or a typed array through those. + */ fun echo(v: Vector) { when (v) { + is Vector.I32 -> { + expect(Codec.echoI32(v.value) == v.value, "echoI32 ${v.value}") + expect(Codec.echoOptI32(v.value) == v.value, "echoOptI32 ${v.value}") + expect(Codec.echoI32s(listOf(v.value, v.value)) == listOf(v.value, v.value), "echoI32s ${v.value}") + } + is Vector.U64 -> { + expect(Codec.echoU64(v.value) == v.value, "echoU64 ${v.value}") + expect(Codec.echoU64s(listOf(v.value)) == listOf(v.value), "echoU64s ${v.value}") + expect(Codec.echoUsize(v.value) == v.value, "echoUsize ${v.value}") + } + is Vector.F64 -> { + val bits = v.value.toRawBits() + expect(Codec.echoF64(v.value).toRawBits() == bits, "echoF64 ${v.value}") + expect(Codec.echoOptF64(v.value)?.toRawBits() == bits, "echoOptF64 ${v.value}") + expect(Codec.echoF64s(listOf(v.value)).single().toRawBits() == bits, "echoF64s ${v.value}") + } + is Vector.Flag -> { + expect(Codec.echoBool(v.value) == v.value, "echoBool ${v.value}") + expect(Codec.echoOptBool(v.value) == v.value, "echoOptBool ${v.value}") + } + is Vector.Hue -> { + expect(Codec.echoColor(v.value) == v.value, "echoColor ${v.value}") + expect(Codec.echoOptColor(v.value) == v.value, "echoOptColor ${v.value}") + } is Vector.I8 -> expect(Codec.echoI8(v.value) == v.value, "echoI8 ${v.value}") is Vector.U8 -> expect(Codec.echoU8(v.value) == v.value, "echoU8 ${v.value}") is Vector.I16 -> expect(Codec.echoI16(v.value) == v.value, "echoI16 ${v.value}") is Vector.U16 -> expect(Codec.echoU16(v.value) == v.value, "echoU16 ${v.value}") - is Vector.I32 -> expect(Codec.echoI32(v.value) == v.value, "echoI32 ${v.value}") is Vector.U32 -> expect(Codec.echoU32(v.value) == v.value, "echoU32 ${v.value}") is Vector.I64 -> expect(Codec.echoI64(v.value) == v.value, "echoI64 ${v.value}") - is Vector.U64 -> expect(Codec.echoU64(v.value) == v.value, "echoU64 ${v.value}") is Vector.F32 -> expect( Codec.echoF32(v.value).toRawBits() == v.value.toRawBits(), "echoF32 ${v.value}", ) - is Vector.F64 -> expect( - Codec.echoF64(v.value).toRawBits() == v.value.toRawBits(), - "echoF64 ${v.value}", - ) - is Vector.Flag -> expect(Codec.echoBool(v.value) == v.value, "echoBool ${v.value}") - is Vector.Hue -> expect(Codec.echoColor(v.value) == v.value, "echoColor ${v.value}") is Vector.Text -> expect(Codec.echoText(v.value) == v.value, "echoText") is Vector.Blob -> expect(Codec.echoBlob(v.value).contentEquals(v.value), "echoBlob") else -> {} @@ -83,7 +104,7 @@ fun echo(v: Vector) { fun everyVector(n: UInt) { for (i in 0u until n) { val raw = JniBridge.codec_vector(i.toInt()) - val v = decodeBuffer(raw) { unpackVector(it) } + val v = decodeBuffer(raw) { unpack_Vector(it) } if (!Codec.checkVector(i, v)) { expect( false, @@ -102,16 +123,16 @@ fun everyVector(n: UInt) { } fun canonicalScalars() = Scalars( - i8_value = -8, - u8_value = 200u, - i16_value = -16000, - u16_value = 60000u, - i32_value = -2000000000, - u32_value = 4000000000u, - i64_value = -9007199254740993L, - u64_value = ULong.MAX_VALUE, - f32_value = 1.5f, - f64_value = -2.25e100, + i8Value = -8, + u8Value = 200u, + i16Value = -16000, + u16Value = 60000u, + i32Value = -2000000000, + u32Value = 4000000000u, + i64Value = -9007199254740993L, + u64Value = ULong.MAX_VALUE, + f32Value = 1.5f, + f64Value = -2.25e100, flag = true, color = Color.Blue, ) @@ -120,7 +141,7 @@ fun literalVectors(n: UInt) { val canonical = canonicalScalars() expect(Codec.checkVector(find(n, "scalars canonical"), Vector.AllScalars(canonical)), "scalars canonical") expect( - !Codec.checkVector(find(n, "scalars canonical"), Vector.AllScalars(canonical.copy(u16_value = 60001u))), + !Codec.checkVector(find(n, "scalars canonical"), Vector.AllScalars(canonical.copy(u16Value = 60001u))), "a changed field no longer matches", ) expect(Codec.checkVector(find(n, "shape labeled"), Vector.Figure(Shape.Labeled("tag", 3))), "shape labeled") @@ -165,10 +186,10 @@ fun spotChecks(n: UInt) { val minimum = fetch(find(n, "scalars minimum")) expect(minimum is Vector.AllScalars, "scalars minimum is AllScalars") val m = (minimum as Vector.AllScalars).value - expect(m.i8_value == Byte.MIN_VALUE && m.i16_value == Short.MIN_VALUE, "scalars minimum i8/i16") - expect(m.i32_value == Int.MIN_VALUE && m.i64_value == Long.MIN_VALUE, "scalars minimum i32/i64") - expect(m.u8_value == UByte.MIN_VALUE && m.u64_value == 0uL, "scalars minimum unsigned") - expect(m.f32_value == Float.NEGATIVE_INFINITY && m.f64_value.isNaN(), "scalars minimum floats") + expect(m.i8Value == Byte.MIN_VALUE && m.i16Value == Short.MIN_VALUE, "scalars minimum i8/i16") + expect(m.i32Value == Int.MIN_VALUE && m.i64Value == Long.MIN_VALUE, "scalars minimum i32/i64") + expect(m.u8Value == UByte.MIN_VALUE && m.u64Value == 0uL, "scalars minimum unsigned") + expect(m.f32Value == Float.NEGATIVE_INFINITY && m.f64Value.isNaN(), "scalars minimum floats") expect(m.color == Color.Infrared && !m.flag, "scalars minimum color and flag") val deep = fetch(find(n, "composite canonical")) @@ -176,8 +197,8 @@ fun spotChecks(n: UInt) { val c: Composite = (deep as Vector.Deep).value expect(c.name == "héllo wörld ✓", "composite name (got ${c.name})") expect(c.blob.size == 6 && c.blob[5] == 255.toByte(), "composite blob") - expect(c.some_i64 == Long.MIN_VALUE && c.none_i64 == null, "composite optionals") - expect(c.some_text == "", "composite some_text is present and empty") + expect(c.someI64 == Long.MIN_VALUE && c.noneI64 == null, "composite optionals") + expect(c.someText == "", "composite someText is present and empty") expect(c.names.size == 3 && c.names[1] == "", "composite names") expect(c.matrix.size == 3 && c.matrix[1].isEmpty() && c.matrix[2][0] == -4, "composite matrix") expect( @@ -185,15 +206,15 @@ fun spotChecks(n: UInt) { "composite floats", ) expect( - c.by_name.size == 4 && c.by_id.size == 3 && c.by_color.size == 2 && c.flags.size == 2, + c.byName.size == 4 && c.byId.size == 3 && c.byColor.size == 2 && c.flags.size == 2, "composite maps", ) - expect(c.scalars.u32_value == 4000000000u, "composite scalars.u32_value") + expect(c.scalars.u32Value == 4000000000u, "composite scalars.u32Value") expect(c.shape is Shape.Labeled && (c.shape as Shape.Labeled).count == 3, "composite shape") val lastShape = c.shapes.last() expect(c.shapes.size == 6 && lastShape is Shape.Nested && lastShape.note == null, "composite shapes") - expect(c.maybe_shape is Shape.Nested, "composite maybe_shape") - expect(c.maybe_list?.size == 2, "composite maybe_list") + expect(c.maybeShape is Shape.Nested, "composite maybeShape") + expect(c.maybeList?.size == 2, "composite maybeList") expect(c.sparse.size == 3 && c.sparse[0] == true && c.sparse[1] == null, "composite sparse") expect(c.colors.size == 4 && c.colors[3] == Color.Infrared, "composite colors") } @@ -241,7 +262,7 @@ fun malformed() { expect(encode(Vector.Counts(emptyMap()))[0] == 19.toByte(), "Counts tag") // The generated decoder rejects the same inputs. - val unknown = thrownBy { decodeBuffer(byteArrayOf(0xE7.toByte(), 0x03, 0, 0)) { unpackVector(it) } } + val unknown = thrownBy { decodeBuffer(byteArrayOf(0xE7.toByte(), 0x03, 0, 0)) { unpack_Vector(it) } } expect(unknown is NativeBugException && unknown.code == -3, "an unknown tag fails to decode") // Color can't spell an undeclared value, but the raw bridge can: the @@ -258,7 +279,7 @@ fun objects(n: UInt) { val h = (full as Vector.Objects).value expect(h.primary.value() == 10L && h.spare?.value() == 11L, "holder primary and spare") expect(h.many.map { it.value() } == listOf(12L, 13L, Long.MIN_VALUE), "holder many") - expect(h.by_name.mapValues { it.value.value() } == mapOf("a" to 20L, "b" to 21L), "holder by_name") + expect(h.byName.mapValues { it.value.value() } == mapOf("a" to 20L, "b" to 21L), "holder byName") // Each encoding mints fresh references, so the holder can be sent twice. val expected = 10L + 11 + 12 + 13 + 20 + 21 + Long.MIN_VALUE expect(Codec.sumHolder(h) == expected && Codec.sumHolder(h) == expected, "sumHolder twice") @@ -287,6 +308,95 @@ fun objects(n: UInt) { expect(thrownBy { p.value() } is IllegalStateException, "a closed wrapper can't be used") } +/** The ABI 5 shapes: optional scalars, typed arrays, usize, char, a custom type, and an iterator of arrays. */ +fun abi5Shapes() { + // Optional scalars cross as a presence flag and a value. + expect(Codec.echoOptI32(null) == null, "echoOptI32(null)") + expect(Codec.echoOptI32(Int.MIN_VALUE) == Int.MIN_VALUE, "echoOptI32(MIN)") + expect(Codec.echoOptI32(0) == 0, "echoOptI32(0) is present") + val negZero = Codec.echoOptF64(-0.0) + expect(negZero != null && negZero.toRawBits() == (-0.0).toRawBits(), "echoOptF64(-0.0) keeps the sign") + expect(Codec.echoOptF64(Double.NaN)?.isNaN() == true, "echoOptF64(NaN)") + expect(Codec.echoOptF64(null) == null, "echoOptF64(null)") + expect(Codec.echoOptBool(true) == true && Codec.echoOptBool(false) == false, "echoOptBool") + expect(Codec.echoOptBool(null) == null, "echoOptBool(null)") + expect(Codec.echoOptColor(Color.Infrared) == Color.Infrared, "echoOptColor(Infrared)") + expect(Codec.echoOptColor(Color.Blue) == Color.Blue, "echoOptColor(Blue)") + expect(Codec.echoOptColor(null) == null, "echoOptColor(null)") + // The raw bridge can send an undeclared enum value: -3. + val badOpt = thrownBy { JniBridge.codec_echo_opt_color(true, 3) } + expect(badOpt is NativeBugException && badOpt.code == -3, "an undeclared Color? is rejected (got $badOpt)") + expect(JniBridge.codec_echo_opt_color(false, 3) == null, "an absent value is ignored") + + // Typed arrays. + val floats = listOf(Double.NaN, -0.0, Double.MIN_VALUE, Double.POSITIVE_INFINITY) + val echoed = Codec.echoF64s(floats) + expect( + echoed.size == 4 && echoed.zip(floats).all { (a, b) -> a.toRawBits() == b.toRawBits() }, + "echoF64s is bit-identical (got $echoed)", + ) + expect(Codec.echoF64s(emptyList()).isEmpty(), "echoF64s([])") + val ints = listOf(Int.MIN_VALUE, 0, Int.MAX_VALUE) + expect(Codec.echoI32s(ints) == ints, "echoI32s") + expect(Codec.echoI32s(emptyList()).isEmpty(), "echoI32s([])") + val longs = List(5000) { it * 7 } + expect(Codec.echoI32s(longs) == longs, "echoI32s of a heap-copied array") + val big = listOf(ULong.MAX_VALUE, 9223372036854775808uL) + expect(Codec.echoU64s(big) == big, "echoU64s keeps unsigned values") + expect(Codec.echoU64s(emptyList()).isEmpty(), "echoU64s([])") + + // usize crosses as u64. + expect(Codec.echoUsize(4294967295uL) == 4294967295uL, "echoUsize(u32::MAX)") + expect(Codec.echoUsize(ULong.MAX_VALUE) == ULong.MAX_VALUE, "echoUsize(u64::MAX)") + + // char crosses as a one-scalar string. + for (c in listOf("\uD83E\uDD80", "é", "a")) { + expect(Codec.echoChar(c) == c, "echoChar($c)") + } + val two = thrownBy { Codec.echoChar("ab") } + expect( + two is NativeBugException && two.code == -3 && two.message!!.endsWith("value: \"ab\" is not a valid char"), + "echoChar(ab) is a marshalling error (got ${two?.message})", + ) + val none = thrownBy { Codec.echoChar("") } + expect(none is NativeBugException && none.code == -3, "echoChar(\"\") is a marshalling error (got $none)") + + // A custom type (a u32 in hex) crosses as its string repr. + expect(Codec.echoHex("ff") == "ff" && Codec.echoHex("00FF") == "ff" && Codec.echoHex("0") == "0", "echoHex normalizes") + for ((input, message) in listOf( + "xyz" to "value: invalid digit found in string", + "" to "value: cannot parse integer from empty string", + "100000000" to "value: number too large to fit in target type", + )) { + val e = thrownBy { Codec.echoHex(input) } + expect( + e is NativeBugException && e.code == -3 && e.message!!.endsWith(message), + "echoHex($input) is a marshalling error (got ${e?.message})", + ) + } + + // An iterator of typed arrays. + expect( + Codec.chunks(listOf(Int.MIN_VALUE, 0, Int.MAX_VALUE), 2u).asSequence().toList() == + listOf(listOf(Int.MIN_VALUE, 0), listOf(Int.MAX_VALUE)), + "chunks of 2", + ) + expect( + Codec.chunks(listOf(1, 2, 3, 4), 2u).asSequence().toList() == listOf(listOf(1, 2), listOf(3, 4)), + "chunks([1, 2, 3, 4], 2)", + ) + expect(!Codec.chunks(listOf(1, 2), 0u).hasNext(), "chunks of 0 is empty") + expect(!Codec.chunks(emptyList(), 3u).hasNext(), "chunks of [] is empty") + expect(JniBridge.debug_live(2) == 0L, "exhausted chunk iterators are released") + + // Records holding bytes compare by content. + val blob = byteArrayOf(1, 2, 3) + expect(Vector.Blob(blob) == Vector.Blob(blob.copyOf()), "Blob compares by content") + expect(Vector.Blob(blob).hashCode() == Vector.Blob(blob.copyOf()).hashCode(), "Blob hashes by content") + expect(Vector.Blob(blob) != Vector.Blob(byteArrayOf(1, 2)), "different bytes differ") + expect(Vector.Blob(blob).toString() == "Blob(value=[1, 2, 3])", "Blob prints its bytes") +} + fun main() { expect(JniBridge.debug_live(-1) == 1L, "the sample counts live allocations") val n = Codec.vectorCount() @@ -298,6 +408,7 @@ fun main() { outOfRange(n) malformed() objects(n) + abi5Shapes() expectNoLeaks(JniBridge::debug_live) println("kotlin/codec: OK ($n vectors)") diff --git a/conformance/kotlin/kvstore.kt b/conformance/kotlin/kvstore.kt index fd06c69c..fc409629 100644 --- a/conformance/kotlin/kvstore.kt +++ b/conformance/kotlin/kvstore.kt @@ -9,8 +9,8 @@ // data classes, the `EntryKind` enum class, maps and optionals, unsigned // counts as `UInt`/`ULong`, and the logical clock; // * `KvException` subclasses with their payload fields (`KeyNotFound`, -// `Expired`, `StoreFull`, `Rejected`), and runtime failures (-4) as the -// root `FfiException`; +// `Expired`, `StoreFull`, `Rejected`, `CallbackFailed`), and untyped +// failures (-1) as the root `FfiException`; // * lazy `NativeIterator`s of strings (throwing at launch), records, and // objects, including one abandoned part-way; // * three callback interfaces implemented in Kotlin: a `Listener` @@ -18,9 +18,10 @@ // when it throws, and notified from a producer thread during // compaction), a `Policy` (a record return, a throwing method whose // `KvException.Rejected` reaches the `put` caller with its payload, an -// object parameter and object return), and a `Loader` passed as an -// optional callback (string, bytes, and optional-object returns; typed -// errors decoded by the producer or passed through); +// object parameter and object return, any other failure arriving as +// `CallbackFailed`), and a `Loader` passed as an optional callback +// (string, bytes, and optional-object returns; typed errors decoded by +// the producer or passed through); // * `Store` objects in every position: parameter, return, optional, list, // map value, record field, iterator element, async result, and callback // parameter and return; @@ -30,7 +31,15 @@ // list launched concurrently, and an async function in the nested // `kv.stats` module (`Kv.StatsModule`); // * the sibling `report` root (the shared `Entry` record and its own error -// domain). +// domain); +// * the ABI 5 shapes: optional scalars as a parameter (`ttlSeconds`), a +// return (`expiresAt`), iterator items (`expirations`), an async result +// (`versionOf`), and a callback parameter and return (`Policy.ttlFor`); +// typed arrays as a return (`valueSizes`), an async result +// (`versions`), and a callback parameter and return (`Scorer`, as a +// lambda through its `fun interface`); `usize` counts as `ULong`; a +// `throws: any` method (`importLines`); callback failures converted into +// `KvException.CallbackFailed`; and content equality of `Entry`. // // Releases of consumer callbacks are observed through the producer's // callback counter (`debug_live(1)`). Ends by asserting the producer's leak @@ -58,6 +67,7 @@ import kvstore.Loader import kvstore.Policy import kvstore.Report import kvstore.ReportException +import kvstore.Scorer import kvstore.Stats import kvstore.Store import kvstore.StoreInfo @@ -111,10 +121,22 @@ class RecordingListener(private val skip: String? = null, private val failOn: St // ── policy (consumer-implemented, rich returns, throws) ─────────────────── -/** Routes `b/` keys to [other], a reference this policy owns. */ +/** + * Routes `b/` keys to [other], a reference this policy owns, and sets TTLs: + * 1 for `short`, none for `forever`, a typed failure for `ttl-typed`, an + * untyped one for `ttl-boom`, and the requested TTL otherwise. + */ class TestPolicy(private val other: Store) : Policy { val admitted = AtomicInteger() + override fun ttlFor(key: String, requested: Long?): Long? = when (key) { + "short" -> 1L + "forever" -> null + "ttl-typed" -> throw KvException.StoreFull(7u, "no room for a TTL") + "ttl-boom" -> throw IllegalStateException("ttl exploded") + else -> requested + } + override fun admit(entry: Entry): Entry { admitted.incrementAndGet() expect(entry.version == 0u, "the store assigns the version after admission") @@ -169,7 +191,7 @@ fun basics() = Store.open("/basics").use { s -> // put returns the stored entry; the version counts puts of the key. val e1 = s.putText("alpha", "one", EntryKind.Persistent) expect(e1.key == "alpha" && text(e1.value) == "one" && e1.kind == EntryKind.Persistent, "put alpha") - expect(e1.version == 1u && e1.expires_at == null && e1.tags.isEmpty() && e1.metadata.isEmpty(), "put alpha fields") + expect(e1.version == 1u && e1.expiresAt == null && e1.tags.isEmpty() && e1.metadata.isEmpty(), "put alpha fields") val e2 = s.putText("alpha", "two", EntryKind.Volatile) expect(e2.version == 2u && e2.kind == EntryKind.Volatile, "put alpha again") @@ -180,12 +202,12 @@ fun basics() = Store.open("/basics").use { s -> expect(s.find("nope") == null, "find nope") // TTLs follow the logical clock; an expired get reports when. - expect(s.putText("ttl", "x", EntryKind.Volatile, 10).expires_at == 10L, "ttl expires_at") + expect(s.putText("ttl", "x", EntryKind.Volatile, 10).expiresAt == 10L, "ttl expiresAt") expect(s.now() == 0L, "clock starts at 0") - expect(s.tick(9) == 9L && s.count() == 2u, "tick 9") - expect(s.tick(1) == 10L && s.count() == 1u, "tick 10") + expect(s.tick(9) == 9L && s.count() == 2uL, "tick 9") + expect(s.tick(1) == 10L && s.count() == 1uL, "tick 10") val expired = expectThrows("get(ttl)") { s.get("ttl") } - expect(expired.key == "ttl" && expired.expired_at == 10L, "Expired payload") + expect(expired.key == "ttl" && expired.expiredAt == 10L, "Expired payload") expectKeyNotFound("ttl") { s.get("ttl") } // the expired read removed it // Capacity: a new key past it is StoreFull { capacity }. @@ -201,8 +223,8 @@ fun basics() = Store.open("/basics").use { s -> expect(s.delete("beta") && !s.delete("beta"), "delete twice") @Suppress("DEPRECATION") val size = s.size() - expect(size == s.count() && size == 1u, "deprecated size() == count()") - expect(s.clear() == 1u && s.count() == 0u, "clear") + expect(size.toULong() == s.count() && size == 1u, "deprecated size() == count()") + expect(s.clear() == 1u && s.count() == 0uL, "clear") } fun iterators() = Store.open("/iter").use { s -> @@ -230,7 +252,7 @@ fun iterators() = Store.open("/iter").use { s -> for (part in s.partition(prefixes)) { part.use { expect(it.address() != s.address(), "partition yields new stores") - expect(it.count() == listOf(2u, 1u, 0u)[i] && it.path() == prefixes[i], "partition $i") + expect(it.count() == listOf(2uL, 1uL, 0uL)[i] && it.path() == prefixes[i], "partition $i") } i++ } @@ -242,7 +264,7 @@ fun listeners() { val s = Store.open("/listen") val l = RecordingListener(skip = "quiet") val id = s.subscribe(l) - expect(id > 0u && s.listenerCount() == 1u && liveCallbacks() == base + 1, "subscribe") + expect(id > 0u && s.listenerCount() == 1uL && liveCallbacks() == base + 1, "subscribe") s.putText("a", "1") var put = l.puts().last() @@ -267,7 +289,7 @@ fun listeners() { // Unsubscribing releases the listener once. expect(s.unsubscribe(id) && liveCallbacks() == base, "unsubscribe releases the listener") - expect(!s.unsubscribe(id) && s.listenerCount() == 0u, "unsubscribe twice") + expect(!s.unsubscribe(id) && s.listenerCount() == 0uL, "unsubscribe twice") // A listener that fails is detached (and released); the put succeeds. val failing = RecordingListener(failOn = "boom") @@ -275,13 +297,13 @@ fun listeners() { s.putText("fine", "1") expect(failing.puts().size == 1, "the failing listener saw fine") s.putText("boom", "1") - expect(s.count() == 2u && s.listenerCount() == 0u, "a failing listener is detached") + expect(s.count() == 2uL && s.listenerCount() == 0uL, "a failing listener is detached") expect(liveCallbacks() == base, "the failing listener was released") // Closing the store releases the listeners it still holds. s.subscribe(RecordingListener()) s.subscribe(RecordingListener()) - expect(s.listenerCount() == 2u && liveCallbacks() == base + 2, "two listeners") + expect(s.listenerCount() == 2uL && liveCallbacks() == base + 2, "two listeners") s.close() expect(liveCallbacks() == base, "closing the store released its listeners") } @@ -301,18 +323,34 @@ fun policies() { // route: the object parameter and object return redirect a write. s.putText("b/x", "2", EntryKind.Volatile) - expect(s.count() == 1u && other.count() == 1u, "route redirected b/x") + expect(s.count() == 1uL && other.count() == 1uL, "route redirected b/x") // A typed error from the throwing callback reaches the caller with its - // code, message, and payload. + // code and payload, and the message the producer renders from them. val rejected = expectThrows("put(secret)") { s.putText("secret", "3") } - expect(rejected.code == 1005 && rejected.message == "secrets are not stored", "Rejected code and message") + expect( + rejected.code == 1005 && rejected.message == "write to secret rejected: no secrets", + "Rejected code and message (got ${rejected.message})", + ) expect(rejected.key == "secret" && rejected.reason == "no secrets", "Rejected payload") - // Anything else arrives as -4 with the consumer's message. - val boom = expectThrows("put(boom)") { s.putText("boom", "4") } - expect(boom !is KvException && boom.code == -4 && boom.message == "policy exploded", "-4 (got ${boom.code}: ${boom.message})") - expect(s.count() == 1u && other.count() == 1u && p.admitted.get() == 4, "failed puts changed nothing") + // Anything else arrives as CallbackFailed with the consumer's message. + val boom = expectThrows("put(boom)") { s.putText("boom", "4") } + expect(boom.code == 1006 && boom.message == "policy exploded", "CallbackFailed (got ${boom.code}: ${boom.message})") + expect(boom.message_ == "policy exploded", "CallbackFailed payload") + expect(s.count() == 1uL && other.count() == 1uL && p.admitted.get() == 4, "failed puts changed nothing") + + // ttlFor: an optional scalar parameter and return. + expect(s.putText("short", "x").expiresAt == 1L && s.expiresAt("short") == 1L, "ttlFor sets a TTL") + expect(s.putText("forever", "x", ttl = 5).expiresAt == null, "ttlFor removes a TTL") + expect(s.putText("plain", "x", ttl = 9).expiresAt == 9L, "ttlFor keeps the requested TTL") + expect(s.putText("plain2", "x").expiresAt == null, "ttlFor keeps no TTL") + val typed = expectThrows("put(ttl-typed)") { s.putText("ttl-typed", "x") } + expect(typed.capacity == 7u && typed.message == "store is full (7 entries)", "ttlFor's typed error (got ${typed.message})") + val ttlBoom = expectThrows("put(ttl-boom)") { s.putText("ttl-boom", "x") } + expect(ttlBoom.message == "ttl exploded", "ttlFor's untyped error (got ${ttlBoom.message})") + expect(s.count() == 5uL, "ttl failures stored nothing") + s.clear() // Replacing the policy releases the old one; null removes it. s.setPolicy(TestPolicy(other.share())) @@ -320,7 +358,7 @@ fun policies() { s.setPolicy(null) expect(!s.hasPolicy() && liveCallbacks() == base, "setPolicy(null) released it") s.putText("secret", "now allowed") - expect(s.count() == 2u, "no policy, no veto") + expect(s.count() == 1uL, "no policy, no veto") other.close() s.close() @@ -337,7 +375,7 @@ fun loaders() = Store.open("/load").use { s -> expect(loaded != null && text(loaded.value) == "loaded:k", "loaded value") expect(loaded!!.kind == EntryKind.Volatile && loaded.metadata == mapOf("source" to "kotlin-loader"), "loaded entry") expect(liveCallbacks() == base, "a loader is released after the call") - expect(s.count() == 1u, "the loaded entry is stored") + expect(s.count() == 1uL, "the loaded entry is stored") // A hit doesn't consult the loader. expect(s.getOrLoad("k", TestLoader())?.version == 1u, "a hit") @@ -353,10 +391,13 @@ fun loaders() = Store.open("/load").use { s -> expect(s.getOrLoad("missing", TestLoader()) == null, "KeyNotFound for the same key is none") // KeyNotFound for another key: passed through, payload intact. val other = expectThrows("getOrLoad(elsewhere)") { s.getOrLoad("elsewhere", TestLoader()) } - expect(other.key == "other" && other.message == "not in the loader", "KeyNotFound(other) passed through") - // Any other failure is -4. - val broken = expectThrows("getOrLoad(broken)") { s.getOrLoad("broken", TestLoader()) } - expect(broken.code == -4 && broken.message == "loader is broken", "-4 (got ${broken.code}: ${broken.message})") + expect( + other.key == "other" && other.message == "key not found: other", + "KeyNotFound(other) passed through (got ${other.message})", + ) + // Any other failure is CallbackFailed with the loader's message. + val broken = expectThrows("getOrLoad(broken)") { s.getOrLoad("broken", TestLoader()) } + expect(broken.code == 1006 && broken.message == "loader is broken", "CallbackFailed (got ${broken.code}: ${broken.message})") expect(liveCallbacks() == base, "every loader was released") } @@ -373,7 +414,7 @@ fun asyncCalls() = runBlocking { expect(s.compact(0u) == 2u, "compact(0) removed 2") expect(l.removed(expired = true).size == 2, "the listener saw both expirations") expect(l.offMainThread.get() == 2, "notified from a producer thread") - expect(s.count() == 1u, "one entry left") + expect(s.count() == 1uL, "one entry left") expect(s.compact(5u) == 0u, "compact(5) removed nothing") // Cancel mid-pause: the call completes at once with a cancellation, and @@ -422,13 +463,13 @@ fun objectGraph() { val s = s0.share() expect(s.address() == s0.address(), "share() is the same object") s0.close() - expect(s.count() == 1u, "alive through the shared reference") + expect(s.count() == 1uL, "alive through the shared reference") // fork(): a distinct object with a copy of the entries. val fork = s.fork() - expect(fork.address() != s.address() && fork.count() == 1u && fork.path() == "/graph", "fork") + expect(fork.address() != s.address() && fork.count() == 1uL && fork.path() == "/graph", "fork") fork.putText("k2", "v") - expect(fork.count() == 2u && s.count() == 1u, "fork is independent") + expect(fork.count() == 2uL && s.count() == 1uL, "fork is independent") // larger(): `Store?` in and out. val empty = Store.open("/empty") @@ -440,7 +481,7 @@ fun objectGraph() { val info = s.describe("main", fork) expect(info.label == "main" && info.count == 1u, "describe label and count") expect(info.store.address() == s.address() && info.mirror?.address() == fork.address(), "describe objects") - expect(info.mirror?.count() == 2u, "the mirror is live") + expect(info.mirror?.count() == 2uL, "the mirror is live") // openMany(): a list of objects; one bad path fails the whole call. val many = Store.openMany(listOf("/a", "/b")) @@ -461,7 +502,7 @@ fun objectGraph() { expect(Store.totalCount(stores, named, null) == 5u, "totalCount without extra") // Everything is still intact; release each reference once. - expect(s.count() == 1u && fork.count() == 2u && many[0].count() == 1u, "still usable") + expect(s.count() == 1uL && fork.count() == 2uL && many[0].count() == 1uL, "still usable") named.values.forEach { it.close() } info.store.close() info.mirror?.close() @@ -492,6 +533,76 @@ fun statsAndReport() = Store.open("/stats").use { s -> expect(nothing.code == 2001 && nothing.message == "nothing to report", "NothingToReport") } +// ── ABI 5 shapes ────────────────────────────────────────────────────────── + +/** Optional scalars and typed arrays as returns, iterator items, and async results. */ +fun abi5Shapes() = Store.open("/abi5").use { s -> + s.putText("b", "12", EntryKind.Persistent) + s.put("a", byteArrayOf(1, 2, 3), EntryKind.Volatile, 7) + expect(s.count() == 2uL, "count() is a usize") + expect(s.expiresAt("a") == 7L, "expiresAt(a)") + expect(s.expiresAt("b") == null && s.expiresAt("zzz") == null, "expiresAt absent") + expect(s.valueSizes() == listOf(3uL, 2uL), "valueSizes (got ${s.valueSizes()})") + val expirations = s.expirations().asSequence().toList() + expect(expirations == listOf(7L, null), "expirations (got $expirations)") + expect(JniBridge.debug_live(2) == 0L, "the expirations iterator is released") + runBlocking { + expect(s.versionOf("a") == 1u, "versionOf(a)") + expect(s.versionOf("q") == null, "versionOf(q) is absent") + s.putText("b", "x") + val versions = s.versions(listOf("b", "q", "a")) + expect(versions == listOf(2u, 0u, 1u), "versions (got $versions)") + } +} + +/** A callback interface with a typed-array parameter and return, as a lambda. */ +fun ranking() = Store.open("/rank").use { s -> + val base = liveCallbacks() + s.putText("a", "1") + s.putText("b", "333") + s.putText("c", "22") + var seen: List? = null + val ranked = s.rank { sizes -> + seen = sizes + sizes.map { it.toDouble() } + } + expect(seen == listOf(1uL, 3uL, 2uL), "the scorer saw the sizes (got $seen)") + expect(ranked == listOf("b", "c", "a"), "rank (got $ranked)") + val failed = expectThrows("rank with a failing scorer") { + s.rank { throw IllegalStateException("scorer failed") } + } + expect(failed.message == "scorer failed", "the scorer's message (got ${failed.message})") + val short = expectThrows("rank with too few scores") { s.rank { listOf(1.0) } } + expect(short.message == "expected 3 scores, got 1", "the count mismatch (got ${short.message})") + val reverse = object : Scorer { + override fun scores(sizes: List): List = sizes.map { -it.toDouble() } + } + expect(s.rank(reverse) == listOf("a", "c", "b"), "an object scorer") + expect(liveCallbacks() == base, "every scorer was released") +} + +/** A `throws: any` method returning a usize. */ +fun imports() = Store.open("/import").use { s -> + expect(s.importLines("a=1\n\nb=two\n") == 2uL, "importLines") + expect(text(s.get("b").value) == "two", "an imported value") + val e = thrownBy { s.importLines("c=3\nbroken\nd=4") } + expect( + e is FfiException && e !is KvException && e.code == -1 && e.message == "line 2: expected key=value", + "importLines fails untyped (got $e)", + ) + expect(s.count() == 3uL && s.find("c") != null && s.find("d") == null, "the lines before the failure were stored") +} + +/** Records holding bytes compare by content. */ +fun recordEquality() = Store.open("/equality").use { s -> + val e = s.putText("k", "v") + val copy = e.copy(value = bytes("v")) + expect(e == copy && e.hashCode() == copy.hashCode(), "Entry compares its bytes by content") + expect(e != copy.copy(value = bytes("w")), "different bytes differ") + expect(e.toString().contains("value=[118]"), "Entry prints its bytes (got $e)") + expect(s.get("k") == e, "a fetched entry equals the put one") +} + fun main() { expect(JniBridge.debug_live(-1) == 1L, "the sample counts live allocations") @@ -504,6 +615,10 @@ fun main() { asyncCalls() objectGraph() statsAndReport() + abi5Shapes() + ranking() + imports() + recordEquality() expectNoLeaks(JniBridge::debug_live) println("kotlin/kvstore: OK") diff --git a/conformance/kotlin/load_failure.kt b/conformance/kotlin/load_failure.kt new file mode 100644 index 00000000..ada35565 --- /dev/null +++ b/conformance/kotlin/load_failure.kt @@ -0,0 +1,28 @@ +// Conformance check: a failed load is a catchable `LinkageError`, Kotlin +// (JVM via JNI) target, calculator sample. +// +// Run with `CALCULATOR_LIBRARY` naming a library that doesn't exist. +// `NativeLibrary.load()` throws `UnsatisfiedLinkError` with the reason, and +// the same again on every later call; the first binding call throws that +// error too, and every later one `NoClassDefFoundError`. Both are +// `LinkageError`s, so an app can catch the failure and carry on. +@file:JvmName("LoadFailure") + +import calculator.Calculator +import calculator.NativeLibrary + +fun main() { + val first = thrownBy { NativeLibrary.load() } + expect(first is UnsatisfiedLinkError, "load() raises UnsatisfiedLinkError (got $first)") + expect(first?.message?.contains("does-not-exist") == true, "the message names the library (got ${first?.message})") + val second = thrownBy { NativeLibrary.load() } + expect( + second is UnsatisfiedLinkError && second.message == first?.message, + "load() raises the same error again (got $second)", + ) + val call = thrownBy { Calculator.add(1, 2) } + expect(call is UnsatisfiedLinkError, "the first call raises UnsatisfiedLinkError (got $call)") + val again = thrownBy { Calculator.add(1, 2) } + expect(again is LinkageError, "a later call raises a LinkageError (got $again)") + println("kotlin/calculator load failure: OK") +} diff --git a/conformance/kotlin/run.sh b/conformance/kotlin/run.sh index a3a4dfa7..f99fd3a3 100755 --- a/conformance/kotlin/run.sh +++ b/conformance/kotlin/run.sh @@ -45,7 +45,27 @@ kotlin_consumer() { java -Djava.library.path="$b/shim" -cp "$b/app.jar:$b/bindings.jar:$coro:$kstdlib" Main } -kotlin_calculator() { kotlin_consumer calculator calculator.kt; } +# A failed load is catchable: the calculator bindings pointed at a library +# that doesn't exist (run after kotlin_consumer, whose jars it reuses). The +# leaf name differs from the real library's: dyld searches DYLD_LIBRARY_PATH +# by leaf name even for an absolute path. +kotlin_load_failure() { + local b="$OUT/kotlin-calculator" + local kc real coro kstdlib + kc=$(command -v kotlinc) + real=$(readlink -f "$kc" 2>/dev/null || echo "$kc") + coro=$(ls "$(dirname "$real")/../libexec/lib/kotlinx-coroutines-core-jvm.jar" \ + "$(dirname "$real")/../lib/kotlinx-coroutines-core-jvm.jar" 2>/dev/null | head -1) + kstdlib=$(ls "$(dirname "$real")/../libexec/lib/kotlin-stdlib.jar" \ + "$(dirname "$real")/../lib/kotlin-stdlib.jar" 2>/dev/null | head -1) + kotlinc "$ROOT/conformance/kotlin/common.kt" "$ROOT/conformance/kotlin/load_failure.kt" \ + -cp "$b/bindings.jar:$coro" -d "$b/load_failure.jar" 2>"$b/kotlinc.log" \ + || { cat "$b/kotlinc.log" >&2; echo "kotlinc failed on load_failure.kt" >&2; return 1; } + env "$(library_env calculator)=$b/does-not-exist/libcalculator-missing.$EXT" \ + java -Djava.library.path="$b/shim" -cp "$b/load_failure.jar:$b/bindings.jar:$coro:$kstdlib" LoadFailure +} + +kotlin_calculator() { kotlin_consumer calculator calculator.kt && kotlin_load_failure; } kotlin_codec() { kotlin_consumer codec codec.kt; } kotlin_kvstore() { kotlin_consumer kvstore kvstore.kt; } diff --git a/conformance/node/calculator.mjs b/conformance/node/calculator.mjs index 2b6d9197..d982d26f 100644 --- a/conformance/node/calculator.mjs +++ b/conformance/node/calculator.mjs @@ -1,11 +1,13 @@ // Conformance consumer: calculator sample (node and wasm lanes). // -// The minimal sample: the load-time checks, i32 arithmetic that wraps, a -// throwing function whose typed error has no payload, and strings that keep -// non-ASCII text, astral characters, and interior NULs. Ends with every leak -// counter at zero. +// The minimal sample: the load-time checks, i32 arithmetic that wraps, +// range-checked integer arguments, two error domains whose codes share a +// value (dispatched by the callable's domain), a `throws: any` function, +// an optional scalar return, typed arrays in both directions, and strings +// that keep non-ASCII text, astral characters, and interior NULs. Ends with +// every leak counter at zero. -import { expect, finish, load, throws } from './harness.mjs'; +import { expect, finish, live, load, same, throws } from './harness.mjs'; const api = await load('calculator'); const { calculator, CalculatorError } = api; @@ -13,10 +15,15 @@ const { calculator, CalculatorError } = api; const I32_MAX = 2147483647; const I32_MIN = -2147483648; -expect(api.__debugLive(-1) === 1n, 'the sample counts live resources'); +expect(live(-1) === 1n, 'the sample counts live resources'); expect(calculator.add(2, 3) === 5, 'add'); expect(calculator.add(I32_MAX, 1) === I32_MIN, 'add wraps'); +throws(() => calculator.add(I32_MAX + 1, 0), (e) => e instanceof RangeError, 'an i32 past the range'); +throws(() => calculator.add(I32_MIN - 1, 0), (e) => e instanceof RangeError, 'an i32 below the range'); +throws(() => calculator.add(1.5, 1), (e) => e instanceof RangeError, 'a fractional i32'); +throws(() => calculator.add(NaN, 1), (e) => e instanceof RangeError, 'NaN as an i32'); +throws(() => calculator.add('1', 2), (e) => e instanceof TypeError, 'a string as an i32'); expect(calculator.divide(10, 2) === 5, 'divide'); expect(calculator.divide(-7, 2) === -3, 'divide rounds toward zero'); @@ -27,6 +34,7 @@ throws( e instanceof calculator.DivisionByZeroError && e instanceof calculator.CalcError && e instanceof CalculatorError && + !(e instanceof calculator.ParseError) && e.code === 1 && calculator.DivisionByZeroError.CODE === 1 && e.message === 'division by zero' && @@ -34,10 +42,56 @@ throws( 'divide by zero is the typed error, with no payload fields', ); +// A second domain whose code 1 is a different error. +expect(calculator.parse('42') === 42 && calculator.parse(' 42 ') === 42, 'parse'); +throws( + () => calculator.parse('4x'), + (e) => + e instanceof calculator.NotANumberError && + e instanceof calculator.ParseError && + !(e instanceof calculator.CalcError) && + e.code === 1 && + e.text === '4x' && + e.message === 'not a number: 4x', + 'parse("4x") is ParseError.NotANumber with its field', +); +throws( + () => calculator.parse(''), + (e) => e instanceof calculator.NotANumberError && e.text === '' && e.message === 'not a number: ', + 'parse("")', +); + +// throws: any is the root error with code -1. +expect(calculator.sqrt(9) === 3, 'sqrt'); +throws( + () => calculator.sqrt(-4), + (e) => + e instanceof CalculatorError && + !(e instanceof calculator.CalcError) && + !(e instanceof calculator.ParseError) && + e.code === -1 && + e.message === 'cannot take the square root of -4', + 'sqrt(-4) is an untyped error', +); + +// An optional scalar return and typed arrays. +expect(calculator.mean([1, 2, 6]) === 3, 'mean'); +expect(calculator.mean(Float64Array.of(1, 2, 6)) === 3, 'mean of a Float64Array'); +expect(calculator.mean([]) === null, 'the mean of nothing is null'); +same(calculator.runningTotal([1, 2, 3]), [1, 3, 6], 'runningTotal'); +same(calculator.runningTotal(Int32Array.of(1, 2, 3)), [1, 3, 6], 'runningTotal of an Int32Array'); +same(calculator.runningTotal([]), [], 'runningTotal of nothing'); +same(calculator.runningTotal([1, 2, 3, I32_MAX]), [1, 3, 6, -2147483643], 'runningTotal wraps'); +expect(Array.isArray(calculator.runningTotal([1])), 'a typed-array return is a plain array'); +throws(() => calculator.runningTotal([1, 2 ** 31]), (e) => e instanceof RangeError && e.message.includes('values[1]'), 'an element past the range'); +throws(() => calculator.runningTotal([0.5]), (e) => e instanceof RangeError, 'a fractional element'); +throws(() => calculator.runningTotal(new Float64Array(1)), (e) => e instanceof TypeError, 'the wrong typed array'); +throws(() => calculator.runningTotal('123'), (e) => e instanceof TypeError, 'a string as a list'); + expect(calculator.greet('World') === 'Hello, World!', 'greet'); expect(calculator.greet('') === 'Hello, !', 'greet an empty name'); expect(calculator.greet('Wörld 🦀') === 'Hello, Wörld 🦀!', 'non-ASCII and astral text'); expect(calculator.greet('a\0b') === 'Hello, a\0b!', 'an interior NUL'); throws(() => calculator.greet(42), (e) => e instanceof TypeError, 'a non-string is a TypeError'); -await finish(api, 'calculator'); +await finish('calculator'); diff --git a/conformance/node/codec.mjs b/conformance/node/codec.mjs index d06c65a4..31c07a87 100644 --- a/conformance/node/codec.mjs +++ b/conformance/node/codec.mjs @@ -6,20 +6,26 @@ // vectors built from literals (so a symmetric encode/decode bug can't hide), // spot checks of decoded fields (64-bit integers exact as bigints, float // bits, astral text), the typed out-of-range error and its payload, the -// marshalling failures the API can express, and object identity and -// reference counting through buffers. Ends with every leak counter at zero. +// marshalling failures the API can express, range checks of every integer +// width (direct, optional, typed-array, and buffered), optional scalars and +// typed arrays of every kind, `usize`, `char`, and a custom type, a lazy +// iterator of typed arrays, object identity and reference counting through +// buffers, and, on WebAssembly, an instance poisoned by a trap. Ends with +// every leak counter at zero. -import { expect, finish, load, same, throws } from './harness.mjs'; +import { expect, finish, isWasm, live, load, same, throws } from './harness.mjs'; const api = await load('codec'); const { codec, CodecError } = api; const { Color, Token } = codec; +const I32_MIN = -2147483648; +const I32_MAX = 2147483647; const I64_MIN = -(2n ** 63n); const I64_MAX = 2n ** 63n - 1n; const U64_MAX = 2n ** 64n - 1n; -expect(api.__debugLive(-1) === 1n, 'the sample counts live resources'); +expect(live(-1) === 1n, 'the sample counts live resources'); const n = codec.vectorCount(); expect(n >= 60, `at least 60 vectors (got ${n})`); @@ -213,8 +219,143 @@ throws(() => codec.checkVector(0, { tag: 'Flag', value: 2 }), (e) => e instanceo throws(() => codec.checkVector(0, { tag: 'Nope' }), (e) => e instanceof TypeError, 'an unknown tag'); throws(() => codec.checkVector(0, { tag: 'I64', value: 2n ** 64n }), (e) => e instanceof RangeError, 'an i64 out of range'); throws(() => codec.echoText(null), (e) => e instanceof TypeError, 'a null string'); +throws( + () => codec.checkVector(0, { tag: 'I8', value: 128 }), + (e) => e instanceof RangeError, + 'the buffer writer range-checks an i8', +); +throws( + () => codec.checkVector(0, { tag: 'U32', value: -1 }), + (e) => e instanceof RangeError, + 'the buffer writer range-checks a u32', +); +throws( + () => codec.checkVector(0, { tag: 'U16', value: 1.5 }), + (e) => e instanceof RangeError, + 'the buffer writer rejects a fractional u16', +); -// 6. Objects. +// 6. Direct integers are range-checked before the call, deterministically: +// a fractional, NaN, or out-of-range number is a RangeError (never a +// wrapped or truncated value), and 64-bit integers take a bigint or an +// integral number in range. +const range = (fn, msg) => throws(fn, (e) => e instanceof RangeError, msg); +expect(codec.echoI8(-128) === -128 && codec.echoI8(127) === 127, 'i8 bounds'); +range(() => codec.echoI8(128), 'i8 past the range'); +range(() => codec.echoI8(-129), 'i8 below the range'); +expect(codec.echoU8(255) === 255, 'u8 bound'); +range(() => codec.echoU8(256), 'u8 past the range'); +range(() => codec.echoU8(-1), 'u8 below the range'); +expect(codec.echoI16(-32768) === -32768, 'i16 bound'); +range(() => codec.echoI16(32768), 'i16 past the range'); +expect(codec.echoU16(65535) === 65535, 'u16 bound'); +range(() => codec.echoU16(65536), 'u16 past the range'); +expect(codec.echoU32(4294967295) === 4294967295, 'u32 bound'); +range(() => codec.echoU32(4294967296), 'u32 past the range'); +range(() => codec.echoI32(0.5), 'a fractional i32'); +range(() => codec.echoI32(Infinity), 'an infinite i32'); +expect(codec.echoI64(-(2 ** 63)) === I64_MIN, 'an integral number at the i64 minimum'); +expect(codec.echoI64(2 ** 53) === 2n ** 53n, 'an integral number as an i64'); +range(() => codec.echoI64(2 ** 63), '2^63 as a number is past the i64 range'); +range(() => codec.echoI64(I64_MAX + 1n), 'an i64 bigint past the range'); +range(() => codec.echoI64(NaN), 'NaN as an i64'); +range(() => codec.echoI64(1.5), 'a fractional i64'); +throws(() => codec.echoI64('1'), (e) => e instanceof TypeError, 'a string as an i64'); +expect(codec.echoU64(2 ** 63) === 2n ** 63n, '2^63 as a u64 number'); +range(() => codec.echoU64(2 ** 64), '2^64 as a number is past the u64 range'); +range(() => codec.echoU64(-1), 'a negative u64'); +range(() => codec.echoU64(-1n), 'a negative u64 bigint'); +// f32 arguments round as Float32Array does: past FLT_MAX plus half an ulp +// to infinity, below it to FLT_MAX. +const FLT_MAX = 3.4028234663852886e38; +const FLT_OVERFLOW = 3.4028235677973366e38; +expect(codec.echoF32(FLT_OVERFLOW) === Infinity, 'f32 rounds to infinity'); +expect(codec.echoF32(FLT_OVERFLOW - 2 ** 75) === FLT_MAX, 'f32 rounds to FLT_MAX'); +expect(codec.echoF32(-1e39) === -Infinity, 'f32 overflows to -infinity'); +expect(Object.is(codec.echoF32(-0), -0), 'f32 keeps the sign of zero'); + +// 7. Optional scalars: null (or undefined) for none. +expect(codec.echoOptI32(null) === null && codec.echoOptI32(undefined) === null, 'echoOptI32(none)'); +expect(codec.echoOptI32(I32_MIN) === I32_MIN && codec.echoOptI32(0) === 0, 'echoOptI32'); +range(() => codec.echoOptI32(2 ** 31), 'an optional i32 past the range'); +range(() => codec.echoOptI32(0.5), 'a fractional optional i32'); +expect(Object.is(codec.echoOptF64(-0), -0), 'echoOptF64 keeps -0'); +expect(Number.isNaN(codec.echoOptF64(NaN)), 'echoOptF64(NaN)'); +expect(codec.echoOptF64(null) === null, 'echoOptF64(none)'); +expect( + codec.echoOptBool(true) === true && codec.echoOptBool(false) === false && codec.echoOptBool(null) === null, + 'echoOptBool', +); +throws(() => codec.echoOptBool(1), (e) => e instanceof TypeError, 'a number as an optional bool'); +expect(codec.echoOptColor(Color.Infrared) === -1 && codec.echoOptColor(Color.Blue) === 7, 'echoOptColor'); +expect(codec.echoOptColor(null) === null, 'echoOptColor(none)'); +throws( + () => codec.echoOptColor(3), + (e) => e instanceof CodecError && !(e instanceof codec.CodecError) && e.code === -3, + 'an undeclared optional enum value', +); + +// 8. Typed arrays: arrays (or the matching typed array) in, arrays out. +const bits = (xs) => Array.from(new Uint8Array(Float64Array.from(xs).buffer)); +const floats = [NaN, -0, 5e-324, Infinity]; +const echoed = codec.echoF64s(floats); +expect(Array.isArray(echoed), 'echoF64s returns an array'); +same(bits(echoed), bits(floats), 'echoF64s is bit-identical'); +same(codec.echoF64s(Float64Array.from(floats)).length, 4, 'echoF64s of a Float64Array'); +same(codec.echoI32s([I32_MIN, 0, I32_MAX]), [I32_MIN, 0, I32_MAX], 'echoI32s'); +same(codec.echoI32s([]), [], 'echoI32s of nothing'); +same(codec.echoI32s(new Int32Array(0)), [], 'echoI32s of an empty Int32Array'); +same(codec.echoU64s([U64_MAX, 2n ** 63n]), [U64_MAX, 2n ** 63n], 'echoU64s'); +same(codec.echoU64s([1]), [1n], 'echoU64s of an integral number'); +same(codec.echoU64s(BigUint64Array.of(5n)), [5n], 'echoU64s of a BigUint64Array'); +same(codec.echoU64s([]), [], 'echoU64s of nothing'); +range(() => codec.echoU64s([1n, -1n]), 'a negative u64 element'); +range(() => codec.echoI32s([1, 2 ** 31]), 'an i32 element past the range'); +throws(() => codec.echoI32s(new Uint32Array(1)), (e) => e instanceof TypeError, 'the wrong typed array'); +throws(() => codec.echoF64s(null), (e) => e instanceof TypeError, 'null as a list'); + +// 9. usize (u64), char, and a custom type (Hex, a u32 written in hex). +expect(codec.echoUsize(4294967295n) === 4294967295n, 'echoUsize'); +if (isWasm(api)) { + // wasm32's usize is 32 bits: a wider value fails the conversion. + throws( + () => codec.echoUsize(U64_MAX), + (e) => e instanceof CodecError && e.code === -3 && e.message === `value: ${U64_MAX} is not a valid usize`, + 'echoUsize(u64::MAX) on a 32-bit producer', + ); +} else { + expect(codec.echoUsize(U64_MAX) === U64_MAX, 'echoUsize(u64::MAX)'); +} +for (const ch of ['\u{1F980}', 'é', 'a']) expect(codec.echoChar(ch) === ch, `echoChar(${ch})`); +const marshalling = (message) => (e) => + e instanceof CodecError && !(e instanceof codec.CodecError) && e.code === -3 && e.message === message; +throws(() => codec.echoChar('ab'), marshalling('value: "ab" is not a valid char'), 'echoChar("ab")'); +throws(() => codec.echoChar(''), marshalling('value: "" is not a valid char'), 'echoChar("")'); +expect(codec.echoHex('ff') === 'ff' && codec.echoHex('00FF') === 'ff' && codec.echoHex('0') === '0', 'echoHex'); +throws(() => codec.echoHex('xyz'), marshalling('value: invalid digit found in string'), 'echoHex("xyz")'); +throws(() => codec.echoHex(''), marshalling('value: cannot parse integer from empty string'), 'echoHex("")'); +throws( + () => codec.echoHex('100000000'), + marshalling('value: number too large to fit in target type'), + 'echoHex past u32', +); + +// 10. An iterator of typed arrays, pulled lazily and closed early. +same([...codec.chunks([I32_MIN, 0, I32_MAX], 2)], [[I32_MIN, 0], [I32_MAX]], 'chunks of 2'); +same([...codec.chunks([1, 2, 3, 4], 2)], [[1, 2], [3, 4]], 'chunks of an even split'); +same([...codec.chunks([1, 2], 0)], [], 'chunks of size 0'); +same([...codec.chunks([], 3)], [], 'chunks of nothing'); +const lazy = codec.chunks(Int32Array.of(1, 2, 3), 1); +same(lazy.next(), { done: false, value: [1] }, 'a lazy first chunk'); +expect(live(2) === 1n, 'the iterator is live while open'); +lazy.close(); +expect(live(2) === 0n && lazy.next().done, 'close() releases the iterator'); +lazy.close(); +const disposed = codec.chunks([1, 2], 1); +disposed[Symbol.dispose](); +expect(live(2) === 0n && disposed.next().done, '[Symbol.dispose]() releases the iterator'); + +// 11. Objects. const full = codec.vector(find('objects full')); expect(full.tag === 'Objects', 'objects full is Objects'); const h = full.value; @@ -241,4 +382,33 @@ for (const t of [p, twin, minus4]) t.close(); release(full); throws(() => p.value(), (e) => e instanceof CodecError && e.code === -3, 'a closed token'); -await finish(api, `codec (${n} vectors)`); +// 12. On WebAssembly, a trap poisons the instance: every later call +// throws a code -2 fault, and releases do nothing. A separate instance of +// the module is trapped on purpose (an error message read from out of +// bounds), through the transport itself. +if (isWasm(api)) { + const { $loadWasm } = await import(new URL('./node_modules/codec/linear.js', import.meta.url)); + const m = await $loadWasm('codec', undefined, 'codec', 'CODEC_LIBRARY', null); + expect(m.x.codec_abi_version() === 5, 'a second instance works'); + let trapped = null; + try { + m.x.codec_error_set(m.err, 1, 0x7ffffff0, 64); + } catch (e) { + trapped = e; + } + expect(trapped instanceof WebAssembly.RuntimeError, `the bad read traps (got ${trapped})`); + let after = null; + try { + m.x.codec_abi_version(); + } catch (e) { + after = e; + } + expect( + after !== null && after.code === -2 && after.message.includes('trapped earlier'), + `a call after the trap throws a code -2 fault (got ${after && after.message})`, + ); + expect(m.q.codec_free_bytes(0, 0) === undefined, 'a release after the trap does nothing'); + expect(codec.echoI32(7) === 7, "the bindings' own instance is unaffected"); +} + +await finish(`codec (${n} vectors)`); diff --git a/conformance/node/harness.mjs b/conformance/node/harness.mjs index b79225e7..354c6b4a 100644 --- a/conformance/node/harness.mjs +++ b/conformance/node/harness.mjs @@ -46,17 +46,31 @@ export async function rejects(promise, check, msg) { } } +let debug = null; + /** * Import the package installed next to the consumer (by path, since a * sample may share its name with a Node.js builtin such as `events`) and, - * on WebAssembly, initialize it. + * on WebAssembly, initialize it. Also loads the package's `./debug` + * export, whose leak counters `live` reads. */ export async function load(pkg) { const api = await import(new URL(`./node_modules/${pkg}/index.js`, import.meta.url)); if (typeof api.init === 'function') await api.init(); + debug = await import(new URL(`./node_modules/${pkg}/debug.js`, import.meta.url)); + expect(!('__debugLive' in api) && !('debugLive' in api), 'the leak counters are not part of the API'); return api; } +/** + * The native library's live-resource counter of `kind` (0 objects, + * 1 callbacks, 2 iterators, 3 cancel tokens, 4 byte runs; -1 is `1n` when + * the library counts). + */ +export function live(kind) { + return debug.debugLive(kind); +} + /** `true` when running against the WebAssembly bindings. */ export function isWasm(api) { return typeof api.init === 'function'; @@ -68,15 +82,15 @@ const KINDS = ['objects', 'callbacks', 'iterators', 'cancel tokens', 'returned b * Force garbage collection until every wrapper the consumer dropped has been * finalized, require every native leak counter back at zero, and exit. */ -export async function finish(api, label) { - let live = []; +export async function finish(label) { + let counts = []; for (let round = 0; round < 100; round++) { globalThis.gc(); await new Promise((resolve) => setTimeout(resolve, 5)); - live = KINDS.map((_, kind) => api.__debugLive(kind)); - if (live.every((n) => n === 0n)) break; + counts = KINDS.map((_, kind) => live(kind)); + if (counts.every((n) => n === 0n)) break; } - live.forEach((n, kind) => expect(n === 0n, `${n} ${KINDS[kind]} still live at exit`)); + counts.forEach((n, kind) => expect(n === 0n, `${n} ${KINDS[kind]} still live at exit`)); if (failures > 0) { console.error(`${label}: ${failures} failure(s)`); process.exit(1); diff --git a/conformance/node/kvstore.mjs b/conformance/node/kvstore.mjs index 9750c318..e7d25e43 100644 --- a/conformance/node/kvstore.mjs +++ b/conformance/node/kvstore.mjs @@ -3,22 +3,28 @@ // // The `Store` class (a throwing factory, the `new` constructor, methods, // statics, the deprecated `size`, close()), typed `KvError`s with their -// payload fields, records with bytes, optional, list, and map fields, lazy -// iterators of strings, records, and objects, three callback interfaces -// implemented in JavaScript (a `Listener` that is retained, filtered, and -// detached when it throws; a `Policy` with a record return, a typed error -// raised back through `put`, and object parameters and returns; a `Loader` -// passed as an optional callback with string, bytes, and optional-object -// returns), objects in every buffered position, async calls with `AbortSignal` +// payload fields, records with bytes, optional, list, and map fields, +// optional scalars and typed arrays in every position (an optional TTL +// argument, `expiresAt`, `valueSizes`, an iterator of optional expiries, +// async `versionOf`/`versions`, a `Policy.ttlFor` taking and returning an +// optional scalar, a `Scorer` taking and returning typed arrays), `u64` +// counts (Rust `usize`), a `throws: any` method, lazy iterators of strings, +// records, objects, and optional scalars (closed early too), four callback +// interfaces implemented in JavaScript (a `Listener` that is retained, +// filtered, and detached when it throws; a `Policy` whose typed errors reach +// the caller typed and whose other failures become `CallbackFailed`, with +// record and object parameters and returns; a `Scorer`; a `Loader` passed as +// an optional callback with string, bytes, and optional-object returns), +// objects in every buffered position, async calls with `AbortSignal` // cancellation, the nested `kv.stats` module, the sibling `report` root, and -// leak-free teardown. `__debugLive(1)` (live callbacks) shows when the -// producer released an implementation. +// leak-free teardown. `live(1)` (live callbacks) shows when the producer +// released an implementation. // // On wasm32 the producer has no threads: compaction pauses for no time and // notifies inline, so the mid-pause cancellation and the off-thread delivery // checks run on Node.js only. -import { expect, finish, isWasm, load, rejects, same, throws } from './harness.mjs'; +import { expect, finish, isWasm, live, load, rejects, same, throws } from './harness.mjs'; const api = await load('kvstore'); const { kv, report, KvstoreError, CancelledError } = api; @@ -27,7 +33,7 @@ const wasm = isWasm(api); const enc = (s) => new TextEncoder().encode(s); const dec = (b) => new TextDecoder().decode(b); -const callbacks = () => api.__debugLive(1); +const callbacks = () => live(1); function put(store, key, value, kind = EntryKind.Persistent, ttl = null) { return store.put(key, enc(value), kind, ttl); @@ -37,7 +43,7 @@ const keyNotFound = (key) => (e) => e instanceof kv.KeyNotFoundError && e instanceof kv.KvError && e.code === 1001 && e.key === key; // 1. Load: the import checked the ABI revision and both contract tables. -expect(api.__debugLive(-1) === 1n, 'the sample counts live resources'); +expect(live(-1) === 1n, 'the sample counts live resources'); async function constructors() { throws( @@ -80,8 +86,8 @@ function basics() { expect(put(s, 'ttl', 'x', EntryKind.Volatile, 10n).expires_at === 10n, 'a ttl sets expires_at'); expect(s.now() === 0n, 'the clock starts at zero'); - expect(s.tick(9n) === 9n && s.count() === 2, 'tick(9)'); - expect(s.tick(1) === 10n && s.count() === 1, 'tick(1) expires the entry'); + expect(s.tick(9n) === 9n && s.count() === 2n, 'tick(9)'); + expect(s.tick(1) === 10n && s.count() === 1n, 'tick(1) expires the entry'); throws( () => s.get('ttl'), (e) => @@ -104,11 +110,14 @@ function basics() { (e) => e instanceof KvstoreError && !(e instanceof kv.KvError) && e.code === -3, 'an undeclared enum value is a marshalling failure', ); + throws(() => put(s, 'k', 'v', EntryKind.Volatile, 2n ** 63n), (e) => e instanceof RangeError, 'a TTL past i64'); + throws(() => put(s, 'k', 'v', EntryKind.Volatile, 1.5), (e) => e instanceof RangeError, 'a fractional TTL'); + throws(() => s.setCapacity(-1), (e) => e instanceof RangeError, 'a negative u32'); put(s, 'beta', 'b'); expect(s.delete('beta') === true && s.delete('beta') === false, 'delete'); - expect(s.size() === s.count() && s.count() === 1, 'the deprecated size'); - expect(s.clear() === 1 && s.count() === 0, 'clear'); + expect(s.size() === 1 && s.count() === 1n, 'the deprecated size'); + expect(s.clear() === 1 && s.count() === 0n, 'clear'); s.close(); throws(() => s.count(), (e) => e instanceof KvstoreError && e.code === -3, 'a closed store'); } @@ -124,9 +133,13 @@ function iterators() { const partial = s.keys('user.'); expect(partial.next().value === 'user.alice', 'a lazy first key'); partial.return(); - expect(partial.next().done && api.__debugLive(2) === 0n, 'abandoning an iterator releases it'); + expect(partial.next().done && live(2) === 0n, 'abandoning an iterator releases it'); const entries = [...s.entries('sys.')]; + const early = s.entries(null); + expect(early.next().value.key === 'sys.x' && live(2) === 1n, 'an open iterator is live'); + early.close(); + expect(live(2) === 0n && early.next().done, 'close() releases an iterator early'); expect(entries.length === 1 && entries[0].key === 'sys.x' && dec(entries[0].value) === 'xx', 'entries'); const prefixes = ['user.', 'sys.', 'none.']; @@ -134,7 +147,7 @@ function iterators() { expect(parts.length === 3, 'one store per prefix'); parts.forEach((p, i) => { expect(p instanceof Store && p.path() === prefixes[i], `partition ${i} path`); - expect(p.count() === [2, 1, 0][i], `partition ${i} count`); + expect(p.count() === [2n, 1n, 0n][i], `partition ${i} count`); p.close(); }); s.close(); @@ -166,7 +179,7 @@ function listeners() { const s = Store.open('/listen'); const l = new Listener({ skip: 'quiet' }); const id = s.subscribe(l); - expect(id > 0 && s.listenerCount() === 1 && callbacks() === base + 1n, 'subscribe'); + expect(id > 0 && s.listenerCount() === 1n && callbacks() === base + 1n, 'subscribe'); // Synchronous calls notify before they return. const last = () => l.changes.at(-1); @@ -186,7 +199,7 @@ function listeners() { same(l.changes.at(-1), { tag: 'Cleared', count: 1 }, 'a clear'); expect(s.unsubscribe(id) === true && callbacks() === base, 'unsubscribe releases the listener'); - expect(s.unsubscribe(id) === false && s.listenerCount() === 0, 'a second unsubscribe'); + expect(s.unsubscribe(id) === false && s.listenerCount() === 0n, 'a second unsubscribe'); // A listener that throws is detached (and released); the put succeeds. const failing = new Listener({ failOn: 'boom' }); @@ -194,12 +207,12 @@ function listeners() { put(s, 'fine', '1'); expect(failing.count('Put') === 1, 'the failing listener sees fine'); put(s, 'boom', '1'); - expect(s.count() === 2 && s.listenerCount() === 0 && callbacks() === base, 'a throwing listener is detached'); + expect(s.count() === 2n && s.listenerCount() === 0n && callbacks() === base, 'a throwing listener is detached'); // Releasing the store releases the listeners it still holds. s.subscribe(new Listener()); s.subscribe(new Listener()); - expect(s.listenerCount() === 2 && callbacks() === base + 2n, 'two more listeners'); + expect(s.listenerCount() === 2n && callbacks() === base + 2n, 'two more listeners'); s.close(); expect(callbacks() === base, 'closing the store releases its listeners'); } @@ -210,6 +223,14 @@ class Policy { this.admitted = 0; } + ttlFor(key, requested) { + if (key === 'short') return 1n; + if (key === 'forever') return null; + if (key === 'badttl') throw new kv.InvalidPathError('no ttl for you'); + if (key === 'ttlboom') throw new Error('ttl exploded'); + return requested; + } + admit(entry) { this.admitted++; expect(entry.version === 0, 'admit sees version 0'); @@ -218,6 +239,8 @@ class Policy { } if (entry.key.startsWith('boom')) throw new Error('policy exploded'); if (entry.key.startsWith('garbage')) return { key: entry.key }; + if (entry.key.startsWith('undeclared')) throw new kv.KvError(4242, 'an undeclared code'); + if (entry.key === 'short' || entry.key === 'forever' || entry.key === 'plain') return entry; return { ...entry, key: 'renamed', kind: EntryKind.Encrypted, tags: ['admitted'] }; } @@ -246,7 +269,17 @@ function policies() { expect(e.key === 'a' && e.version === 1 && e.kind === EntryKind.Encrypted, "admit's rewrite is stored"); same(e.tags, ['admitted'], "admit's tags"); put(s, 'b/x', '2', EntryKind.Volatile); - expect(s.count() === 1 && other.count() === 1, 'route redirects a write'); + expect(s.count() === 1n && other.count() === 1n, 'route redirects a write'); + + // ttlFor: an optional scalar in and out. + expect(put(s, 'short', 'x').expires_at === 1n && s.expiresAt('short') === 1n, 'ttlFor sets a TTL'); + expect(put(s, 'forever', 'x', EntryKind.Volatile, 5n).expires_at === null, 'ttlFor drops a TTL'); + expect(put(s, 'plain', 'x', EntryKind.Volatile, 9n).expires_at === 9n, 'ttlFor keeps the requested TTL'); + throws( + () => put(s, 'badttl', 'x'), + (err) => err instanceof kv.InvalidPathError && err.code === 1004, + 'a typed error from ttlFor reaches the caller typed', + ); throws( () => put(s, 'secret', '3'), @@ -255,24 +288,32 @@ function policies() { err.code === 1005 && err.key === 'secret' && err.reason === 'no secrets' && - err.message === 'secrets are not stored', - 'a typed error from admit reaches the caller with its payload', + err.message === 'write to secret rejected: no secrets', + "a typed error from admit reaches the caller with its fields and the domain's message", + ); + const callbackFailed = (message) => (err) => + err instanceof kv.CallbackFailedError && + err.code === 1006 && + err.message.includes(message); + throws(() => put(s, 'boom', '4'), callbackFailed('policy exploded'), 'any other exception is CallbackFailed'); + throws(() => put(s, 'ttlboom', '4'), callbackFailed('ttl exploded'), 'a failing ttlFor is CallbackFailed'); + throws( + () => put(s, 'undeclared', '4'), + callbackFailed('an undeclared code'), + 'a code the domain does not declare is CallbackFailed', ); - const foreign = (message) => (err) => - err instanceof KvstoreError && !(err instanceof kv.KvError) && err.code === -4 && err.message.includes(message); - throws(() => put(s, 'boom', '4'), foreign('policy exploded'), 'any other exception is -4 with its message'); // The binding checks returns, so a malformed record or a null object - // never reaches the producer: each fails the callback (-4) instead. - throws(() => put(s, 'garbage', '5'), foreign('expected'), 'a malformed admit return'); - throws(() => put(s, 'null/x', '6'), foreign('expected a Store'), 'a null route return'); - expect(s.count() === 1 && other.count() === 1 && p.admitted === 6, 'failed puts change nothing'); + // never reaches the producer: each fails the callback instead. + throws(() => put(s, 'garbage', '5'), callbackFailed('expected'), 'a malformed admit return'); + throws(() => put(s, 'null/x', '6'), callbackFailed('expected a Store'), 'a null route return'); + expect(s.count() === 4n && other.count() === 1n && p.admitted === 10, 'failed puts change nothing'); s.setPolicy(new Policy(other)); expect(callbacks() === base + 1n, 'replacing the policy releases the old one'); s.setPolicy(null); expect(!s.hasPolicy() && callbacks() === base, 'setPolicy(null) releases it'); put(s, 'secret', 'now allowed'); - expect(s.count() === 2, 'no policy, no veto'); + expect(s.count() === 5n, 'no policy, no veto'); other.close(); s.close(); } @@ -317,13 +358,13 @@ function loaders() { expect(s.getOrLoad('missing', new Loader()) === null, 'KeyNotFound for this key is none'); throws( () => s.getOrLoad('elsewhere', new Loader()), - (e) => keyNotFound('other')(e) && e.message === 'not in the loader', - 'KeyNotFound for another key passes through', + (e) => keyNotFound('other')(e) && e.message === 'key not found: other', + 'KeyNotFound for another key passes through typed', ); throws( () => s.getOrLoad('broken', new Loader()), - (e) => e instanceof KvstoreError && e.code === -4 && e.message === 'loader is broken', - 'any other loader failure is -4', + (e) => e instanceof kv.CallbackFailedError && e.code === 1006 && e.message === 'loader is broken', + 'any other loader failure is CallbackFailed', ); expect(callbacks() === base, 'every loader is released'); s.close(); @@ -345,7 +386,7 @@ async function asyncCalls() { const pending = s.compact(0, { signal: new AbortController().signal }); if (!wasm) expect(expired() === 0, 'off-thread notifications wait for the event loop'); expect((await pending) === 2, 'compact removes the expired entries'); - expect(expired() === 2 && s.count() === 1, 'the listener saw both removals'); + expect(expired() === 2 && s.count() === 1n, 'the listener saw both removals'); expect((await s.compact(5)) === 0, 'compact without a signal'); await rejects(s.compact(0, { signal: AbortSignal.abort() }), (e) => e instanceof CancelledError, 'an aborted signal'); @@ -366,6 +407,8 @@ async function asyncCalls() { expect(stopped, 'the cancelled pause stopped cooperatively'); } + expect((await s.versionOf('keep')) === 1 && (await s.versionOf('gone')) === null, 'versionOf'); + same(await s.versions(['keep', 'gone']), [1, 0], 'versions'); const many = await Promise.all(Array.from({ length: 32 }, () => s.getMany(['keep', 'gone', 'keep']))); expect( many.every((r) => r.length === 3 && r[0]?.key === 'keep' && r[1] === null && r[2]?.key === 'keep'), @@ -392,29 +435,29 @@ function objectGraph() { expect(shared.find('via-original') !== null, 'share() is the same object'); s.delete('via-original'); s.close(); - expect(shared.count() === 1, 'still alive through the shared reference'); + expect(shared.count() === 1n, 'still alive through the shared reference'); const fork = shared.fork(); - expect(fork.count() === 1 && fork.path() === '/graph', 'fork copies the entries'); + expect(fork.count() === 1n && fork.path() === '/graph', 'fork copies the entries'); put(fork, 'k2', 'v'); - expect(fork.count() === 2 && shared.count() === 1, 'fork is distinct'); + expect(fork.count() === 2n && shared.count() === 1n, 'fork is distinct'); const empty = Store.open('/empty'); expect(empty.larger(null) === null, 'larger(null) on an empty store'); const bigger = empty.larger(fork); - expect(bigger.count() === 2, 'larger picks the fork'); + expect(bigger.count() === 2n, 'larger picks the fork'); bigger.close(); const self = shared.larger(null); put(self, 'via-larger', 'v'); - expect(shared.count() === 2, 'larger(null) on a non-empty store is itself'); + expect(shared.count() === 2n, 'larger(null) on a non-empty store is itself'); self.delete('via-larger'); self.close(); const info = shared.describe('main', fork); expect(info.label === 'main' && info.count === 1, 'describe'); - expect(info.store.count() === 1 && info.mirror.count() === 2, 'describe carries both stores'); + expect(info.store.count() === 1n && info.mirror.count() === 2n, 'describe carries both stores'); put(info.store, 'via-info', 'v'); - expect(shared.count() === 2, 'describe carries this store itself'); + expect(shared.count() === 2n, 'describe carries this store itself'); shared.delete('via-info'); const opened = Store.openMany(['/a', '/b']); @@ -423,16 +466,90 @@ function objectGraph() { const named = Store.byLabel([info, { label: 'first', store: opened[0], mirror: null, count: 0 }]); same(Object.keys(named).sort(), ['first', 'main'], 'byLabel keys'); - expect(named.main.count() === 1 && named.first.path() === '/a', 'byLabel values'); + expect(named.main.count() === 1n && named.first.path() === '/a', 'byLabel values'); put(opened[0], 'm', '1'); expect(Store.totalCount([opened[0], opened[1], fork], named, info) === 6, 'totalCount with extra'); expect(Store.totalCount([opened[0], opened[1], fork], named, null) === 5, 'totalCount without extra'); - expect(shared.count() === 1 && fork.count() === 2 && opened[0].count() === 1, 'still usable after buffers'); + expect(shared.count() === 1n && fork.count() === 2n && opened[0].count() === 1n, 'still usable after buffers'); for (const o of [info.store, info.mirror, named.main, named.first, ...opened, empty, fork, shared]) o.close(); } +class Scorer { + constructor(mode = 'size') { + this.mode = mode; + this.seen = []; + } + + scores(sizes) { + this.seen.push(sizes); + if (this.mode === 'fail') throw new Error('scorer is broken'); + if (this.mode === 'short') return [1]; + if (this.mode === 'typed') return Float64Array.from(sizes, Number); + return sizes.map((n) => Number(n) * 1.0); + } +} + +function directShapes() { + const base = callbacks(); + const s = Store.open('/shapes'); + put(s, 'b', '12', EntryKind.Persistent, null); + put(s, 'a', 'abc', EntryKind.Volatile, 7n); + expect(s.count() === 2n, 'count is a u64'); + expect(s.expiresAt('a') === 7n && s.expiresAt('b') === null && s.expiresAt('zzz') === null, 'expiresAt'); + same(s.valueSizes(), [3n, 2n], 'valueSizes is an array of bigints'); + same([...s.expirations()], [7n, null], 'an iterator of optional scalars'); + + s.clear(); + put(s, 'a', '1'); + put(s, 'b', '333'); + put(s, 'c', '22'); + const scorer = new Scorer(); + same(s.rank(scorer), ['b', 'c', 'a'], 'rank'); + same(scorer.seen, [[1n, 3n, 2n]], 'the scorer received the sizes as an array'); + same(s.rank(new Scorer('typed')), ['b', 'c', 'a'], 'a scorer may return a typed array'); + throws( + () => s.rank(new Scorer('fail')), + (e) => e instanceof kv.CallbackFailedError && e.code === 1006 && e.message === 'scorer is broken', + 'a failing scorer is CallbackFailed', + ); + throws( + () => s.rank(new Scorer('short')), + (e) => e instanceof kv.CallbackFailedError && e.message === 'expected 3 scores, got 1', + 'a scorer returning too few scores', + ); + expect(callbacks() === base, 'every scorer is released'); + + s.clear(); + expect(s.importLines('a=1\n\nb=two\n') === 2n, 'importLines'); + expect(dec(s.get('b').value) === 'two', 'importLines stored b'); + throws( + () => s.importLines('c=3\nbroken\nd=4'), + (e) => + e instanceof KvstoreError && + !(e instanceof kv.KvError) && + e.code === -1 && + e.message === 'line 2: expected key=value', + 'a throws: any method fails with code -1', + ); + expect(s.count() === 3n && s.find('c') !== null && s.find('d') === null, 'importLines stopped at the bad line'); + s.close(); +} + +async function asyncShapes() { + const s = Store.open('/async-shapes'); + put(s, 'a', '1'); + expect((await s.versionOf('a')) === 1, 'versionOf'); + expect((await s.versionOf('q')) === null, 'versionOf a missing key'); + put(s, 'b', 'x'); + put(s, 'b', 'x'); + const versions = await s.versions(['b', 'q', 'a']); + expect(Array.isArray(versions), 'an async typed-array result is an array'); + same(versions, [2, 0, 1], 'versions'); + s.close(); +} + function statsAndReport() { const s = Store.open('/stats'); put(s, 'b', '12'); @@ -469,8 +586,10 @@ listeners(); policies(); loaders(); await asyncCalls(); +directShapes(); +await asyncShapes(); objectGraph(); statsAndReport(); expect(callbacks() === 0n, 'every callback implementation was released'); -await finish(api, 'kvstore'); +await finish('kvstore'); diff --git a/conformance/python/calculator_consumer.py b/conformance/python/calculator_consumer.py index e77d3f5d..2f37bd4c 100644 --- a/conformance/python/calculator_consumer.py +++ b/conformance/python/calculator_consumer.py @@ -1,12 +1,20 @@ -"""Conformance consumer: calculator sample, Python target (ABI revision 4). +"""Conformance consumer: calculator sample, Python target (ABI revision 5). The getting-started surface: direct `i32` arguments and returns (with -wrapping), a throwing call raising the typed `CalcError.DivisionByZero`, and -a string round trip (non-ASCII and astral text and an interior NUL survive, -since strings cross as pointer + length). Importing the package already -checked the ABI revision and the `calculator` contract table. Ends with the -leak check (see harness.py). +wrapping, and range-checked before ctypes could truncate them), two error +domains in one module whose codes share a value (`CalcError.DivisionByZero` +and `ParseError.NotANumber` are both 1, told apart by the callable's +domain), a `throws: any` function raising the root error with code -1, a +string round trip (non-ASCII and astral text and an interior NUL survive, +since strings cross as pointer + length), an optional `f64` return (crossing +as a flag plus a value), and typed arrays in both directions (any sequence +of numbers in; a list out). Importing the package already checked the ABI +revision and the `calculator` contract table. Ends with the leak check (see +harness.py). """ +import array +import ctypes + import calculator as calc from harness import Consumer @@ -15,9 +23,11 @@ consumer = Consumer("calculator", calc) check = consumer.check +raises = consumer.raises +impl = consumer.impl -def main() -> None: +def arithmetic() -> None: check(calc.add(2, 3) == 5, "add") check(calc.add(-7, 7) == 0, "add to zero") check(calc.add(INT32_MAX, 1) == INT32_MIN, "add wraps") @@ -26,12 +36,22 @@ def main() -> None: check(calc.divide(-7, 2) == -3, "divide rounds toward zero") check(calc.divide(INT32_MIN, -1) == INT32_MIN, "divide MIN by -1") - exc = consumer.raises(calc.CalcError.DivisionByZero, lambda: calc.divide(1, 0), "divide(1, 0)") + exc = raises(calc.CalcError.DivisionByZero, lambda: calc.divide(1, 0), "divide(1, 0)") + check(type(exc) is calc.DivisionByZeroError, f"DivisionByZero class {exc!r}") check(exc.code == 1 and exc.CODE == 1, f"DivisionByZero code {exc.code}") check(exc.message == "division by zero", f"DivisionByZero message {exc.message!r}") check(isinstance(exc, calc.CalcError) and isinstance(exc, calc.Error), "error hierarchy") - check(calc.DivisionByZero is calc.CalcError.DivisionByZero, "scoped alias") + check(calc.DivisionByZeroError is calc.CalcError.DivisionByZero, "scoped alias") + + # ctypes would truncate these silently; the bindings refuse them first. + exc2 = raises(OverflowError, lambda: calc.add(INT32_MAX + 1, 0), "add(2^31, 0)") + check(str(exc2) == "a: 2147483648 is out of range for i32", str(exc2)) + raises(OverflowError, lambda: calc.add(0, INT32_MIN - 1), "add(0, -2^31 - 1)") + exc3 = raises(TypeError, lambda: calc.add(1.5, 2), "add(1.5, 2)") # type: ignore[arg-type] + check(str(exc3) == "a: expected an integer, got float", str(exc3)) + +def strings() -> None: for name, expected in [ ("World", "Hello, World!"), ("", "Hello, !"), @@ -41,6 +61,67 @@ def main() -> None: got = calc.greet(name) check(got == expected, f"greet({name!r}) == {got!r}") + +def domains() -> None: + check(calc.parse("42") == 42 and calc.parse(" 42 ") == 42, "parse") + for text in ["4x", ""]: + exc = raises(calc.ParseError.NotANumber, lambda: calc.parse(text), f"parse({text!r})") + check(type(exc) is calc.NotANumberError, f"NotANumber class {exc!r}") + check(exc.code == 1 and exc.text == text, f"NotANumber payload {exc!r}") + check(exc.message == f"not a number: {text}", f"NotANumber message {exc.message!r}") + # Code 1 of the other domain is not this one. + check(not isinstance(exc, calc.CalcError), "dispatched by the callable's domain") + + check(calc.sqrt(9.0) == 3.0, "sqrt(9)") + exc = raises(calc.Error, lambda: calc.sqrt(-4.0), "sqrt(-4)") + check(type(exc) is calc.Error, f"throws any raises the root error {exc!r}") + check(exc.code == calc.Error.GENERIC_ERROR_CODE == -1, f"sqrt code {exc.code}") + check(exc.message == "cannot take the square root of -4", f"sqrt message {exc.message!r}") + + # A domain is open: a code a newer library added maps to its base class. + unknown = impl._calc_error_from(77, "from the future", b"") + check(type(unknown) is calc.CalcError and unknown.code == 77 + and unknown.message == "from the future", f"unknown code {unknown!r}") + runtime = impl._parse_error_from(-2, "panicked", b"") + check(type(runtime) is calc.Error and runtime.code == -2, f"runtime code {runtime!r}") + + +def slices() -> None: + check(calc.mean([1.0, 2.0, 6.0]) == 3.0, "mean present") + check(calc.mean([]) is None, "mean absent") + check(calc.mean((1, 2)) == 1.5, "mean of a tuple of ints") + check(calc.mean(array.array("d", [2.0, 4.0])) == 3.0, "mean of an array lent as is") + check(calc.mean(x / 2 for x in range(1, 4)) == 1.0, "mean of a generator") # type: ignore[arg-type] + + check(calc.running_total([1, 2, 3]) == [1, 3, 6], "running_total") + check(calc.running_total([]) == [], "running_total empty") + check(calc.running_total([1, 2, 3, INT32_MAX]) == [1, 3, 6, -2147483643], + "running_total wraps") + check(calc.running_total(range(4)) == [0, 1, 3, 6], "running_total of a range") + + exc = raises(OverflowError, lambda: calc.running_total([1, INT32_MAX + 1]), "element 2^31") + check(str(exc) == "values: an element is out of range for i32", str(exc)) + exc2 = raises(TypeError, lambda: calc.running_total([1, "2"]), # type: ignore[list-item] + "string element") + check(str(exc2).startswith("values: "), str(exc2)) + raises(TypeError, lambda: calc.running_total(b"\x01\x02"), "bytes") + raises(TypeError, lambda: calc.mean("12"), "str") # type: ignore[arg-type] + + # A typed array that isn't aligned for its element type is -3. + raw = (ctypes.c_uint8 * 16)() + has = ctypes.c_double() + err = impl._ErrorStruct() + present = impl._c_calculator_mean(ctypes.addressof(raw) + 1, 1, ctypes.byref(has), + ctypes.byref(err)) + code, message, _ = impl._read_error(err) + check(not present and code == -3, f"misaligned array: {code} {message!r}") + + +def main() -> None: + arithmetic() + strings() + domains() + slices() consumer.finish() diff --git a/conformance/python/codec_consumer.py b/conformance/python/codec_consumer.py index c4a3e3d4..9bb85c4a 100644 --- a/conformance/python/codec_consumer.py +++ b/conformance/python/codec_consumer.py @@ -1,19 +1,24 @@ -"""Conformance consumer: codec sample, Python target (ABI revision 4). +"""Conformance consumer: codec sample, Python target (ABI revision 5). The shared-vector loop: for every vector the producer serves, decode it with the generated codecs, pass it back to `check_vector` (which re-decodes the generated encoding and compares), confirm it never matches its neighbor, and push each primitive vector's value through the matching direct-family -`echo_*`. Then vectors built from literals (so a symmetric encode/decode bug -can't hide), spot checks of decoded values, the typed out-of-range error -with its payload, malformed input rejected on both sides, and object -identity and reference counting through buffers. Ends with the leak check -(see harness.py). +`echo_*`, its optional (flag plus value) echo, and its typed-array echo. +Then vectors built from literals (so a symmetric encode/decode bug can't +hide), spot checks of decoded values, the typed out-of-range error with its +payload, malformed input rejected on both sides, object identity and +reference counting through buffers, the direct optionals and typed arrays at +their edges, `usize`, `char`, and custom-type (`Hex`) strings with their +marshalling errors, and an iterator of typed arrays. Ends with the leak +check (see harness.py). """ +import array import ctypes +import dataclasses import math import struct -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List import codec from harness import Consumer @@ -63,6 +68,22 @@ def same_f64(a: float, b: float) -> bool: } +def same_f64s(a: List[float], b: List[float]) -> bool: + return len(a) == len(b) and all(same_f64(x, y) for x, y in zip(a, b)) + + +# The optional (OptDirect) and typed-array (Slice) echoes each primitive +# vector variant also goes through. +DIRECT_ECHOES: Dict[type, Callable[[Any], bool]] = { + codec.VectorI32: lambda v: codec.echo_opt_i32(v) == v and codec.echo_i32s([v, v]) == [v, v], + codec.VectorU64: lambda v: codec.echo_u64s([v]) == [v] and codec.echo_usize(v) == v, + codec.VectorF64: lambda v: (same_f64(codec.echo_opt_f64(v), v) # type: ignore[arg-type] + and same_f64s(codec.echo_f64s([v]), [v])), + codec.VectorFlag: lambda v: codec.echo_opt_bool(v) is v, + codec.VectorHue: lambda v: codec.echo_opt_color(v) is v, +} + + def find(n: int, name: str) -> int: for i in range(n): if codec.vector_name(i) == name: @@ -81,6 +102,9 @@ def every_vector(n: int) -> None: echo = ECHOES.get(type(v)) if echo is not None: check(echo(v.value), f"echo of vector {i} ({codec.vector_name(i)})") + direct = DIRECT_ECHOES.get(type(v)) + if direct is not None: + check(direct(v.value), f"direct echoes of vector {i} ({codec.vector_name(i)})") del v # releases any tokens inside @@ -97,9 +121,14 @@ def literal_vectors(n: int) -> None: s = canonical_scalars() check(codec.check_vector(find(n, "scalars canonical"), codec.VectorAllScalars(s)), "scalars canonical") - s.u16_value = 60001 - check(not codec.check_vector(find(n, "scalars canonical"), codec.VectorAllScalars(s)), + changed = dataclasses.replace(s, u16_value=60001) + check(not codec.check_vector(find(n, "scalars canonical"), codec.VectorAllScalars(changed)), "a changed field no longer matches") + # Records are frozen and hashable by value. + raises_frozen = consumer.raises(dataclasses.FrozenInstanceError, + lambda: setattr(s, "u16_value", 1), "frozen record") + check("u16_value" in str(raises_frozen), str(raises_frozen)) + check(hash(s) == hash(canonical_scalars()) and s == canonical_scalars(), "record hash") check(codec.check_vector(find(n, "shape labeled"), codec.VectorFigure(codec.ShapeLabeled(label="tag", count=3))), @@ -189,8 +218,9 @@ def out_of_range(n: int) -> None: check(exc.code == 1 and exc.index == n and exc.count == n, f"OutOfRange payload {exc!r}") check(exc.message == f"vector {n} is out of range (count {n})", f"message {exc.message!r}") check(isinstance(exc, codec.CodecError) and isinstance(exc, codec.Error), "hierarchy") + check(type(exc) is codec.OutOfRangeError, f"code class {exc!r}") - exc = consumer.raises(codec.OutOfRange, lambda: codec.vector_name(n + 5), "vector_name") + exc = consumer.raises(codec.OutOfRangeError, lambda: codec.vector_name(n + 5), "vector_name") check(exc.index == n + 5 and exc.count == n, "vector_name payload") check(not codec.check_vector(n, codec.VectorBlank()), "check_vector past the end") @@ -268,6 +298,103 @@ def objects(n: int) -> None: p.close() +INT32_MIN = -(2**31) +INT32_MAX = 2**31 - 1 + + +def optionals() -> None: + check(codec.echo_opt_i32(None) is None, "echo_opt_i32(None)") + check(codec.echo_opt_i32(INT32_MIN) == INT32_MIN, "echo_opt_i32(MIN)") + check(codec.echo_opt_i32(0) == 0, "echo_opt_i32(0) is present") + consumer.raises(OverflowError, lambda: codec.echo_opt_i32(INT32_MAX + 1), "i32? range") + + z = codec.echo_opt_f64(-0.0) + check(z is not None and f64_bits(z) == f64_bits(-0.0), "echo_opt_f64(-0.0) keeps the sign") + nan = codec.echo_opt_f64(math.nan) + check(nan is not None and math.isnan(nan), "echo_opt_f64(NaN)") + check(codec.echo_opt_f64(None) is None, "echo_opt_f64(None)") + + check(codec.echo_opt_bool(True) is True and codec.echo_opt_bool(False) is False, + "echo_opt_bool present") + check(codec.echo_opt_bool(None) is None, "echo_opt_bool(None)") + + for color in [codec.Color.Infrared, codec.Color.Blue]: + got = codec.echo_opt_color(color) + check(got is color and isinstance(got, codec.Color), f"echo_opt_color({color!r})") + check(codec.echo_opt_color(None) is None, "echo_opt_color(None)") + # An undeclared enum value is a marshalling failure; the call declares + # no errors, so it traps. Absent, the value slot is ignored. + exc = consumer.raises(codec.InternalError, lambda: codec.echo_opt_color(3), # type: ignore[arg-type] + "echo_opt_color(3)") + check(exc.code == -3, f"echo_opt_color(3) code {exc.code}") + out = ctypes.c_int32() + err = impl._ErrorStruct() + present = impl._c_codec_echo_opt_color(False, 3, ctypes.byref(out), ctypes.byref(err)) + check(not present and err.code == 0, "absent ignores the value slot") + + +def typed_arrays() -> None: + specials = [math.nan, -0.0, 5e-324, math.inf] + got = codec.echo_f64s(specials) + check([f64_bits(x) for x in got] == [f64_bits(x) for x in specials], f"echo_f64s {got!r}") + check(codec.echo_f64s([]) == [], "echo_f64s([])") + check(codec.echo_f64s(array.array("d", [1.5, 2.5])) == [1.5, 2.5], "an array lent as is") + + extremes = [INT32_MIN, 0, INT32_MAX] + check(codec.echo_i32s(extremes) == extremes, "echo_i32s extremes") + check(codec.echo_i32s(()) == [], "echo_i32s(())") + exc = consumer.raises(OverflowError, lambda: codec.echo_i32s([0, INT32_MIN - 1]), + "i32 element range") + check(str(exc) == "values: an element is out of range for i32", str(exc)) + + big = [UINT64_MAX, 2**63] + check(codec.echo_u64s(big) == big, f"echo_u64s {codec.echo_u64s(big)!r}") + check(codec.echo_u64s([]) == [], "echo_u64s([])") + consumer.raises(OverflowError, lambda: codec.echo_u64s([-1]), "u64 element range") + consumer.raises(OverflowError, lambda: codec.echo_u64s([2**64]), "u64 element range") + + check(codec.echo_usize(4294967295) == 4294967295, "echo_usize(2^32 - 1)") + check(codec.echo_usize(UINT64_MAX) == UINT64_MAX, "echo_usize(u64::MAX)") + exc = consumer.raises(OverflowError, lambda: codec.echo_usize(-1), "echo_usize(-1)") + check(str(exc) == "value: -1 is out of range for u64", str(exc)) + + def pull(values: List[int], size: int) -> List[List[int]]: + with codec.chunks(values, size) as it: + check(isinstance(it, codec.NativeIterator), "chunks is a NativeIterator") + return list(it) + + check(pull(extremes, 2) == [[INT32_MIN, 0], [INT32_MAX]], "chunks(extremes, 2)") + check(pull([1, 2, 3, 4], 2) == [[1, 2], [3, 4]], "chunks([1, 2, 3, 4], 2)") + check(pull([1, 2], 0) == [], "chunks(.., 0)") + check(pull([], 3) == [], "chunks([], 3)") + # Closing part-way releases the handle; a closed iterator is exhausted. + it = codec.chunks([1, 2, 3], 1) + check(next(it) == [1] and impl._debug_live(2) == 1, "one live iterator") + it.close() + check(impl._debug_live(2) == 0 and list(it) == [], "closed part-way") + + +def strings() -> None: + for text in ["\U0001F980", "é", "a"]: + check(codec.echo_char(text) == text, f"echo_char({text!r})") + for text in ["ab", ""]: + exc = consumer.raises(codec.InternalError, lambda: codec.echo_char(text), + f"echo_char({text!r})") + check(exc.code == -3 and exc.message == f"value: \"{text}\" is not a valid char", + f"echo_char({text!r}) {exc!r}") + + for text, expected in [("ff", "ff"), ("00FF", "ff"), ("0", "0")]: + check(codec.echo_hex(text) == expected, f"echo_hex({text!r})") + for text, message in [ + ("xyz", "value: invalid digit found in string"), + ("", "value: cannot parse integer from empty string"), + ("100000000", "value: number too large to fit in target type"), + ]: + exc = consumer.raises(codec.InternalError, lambda: codec.echo_hex(text), + f"echo_hex({text!r})") + check(exc.code == -3 and exc.message == message, f"echo_hex({text!r}) {exc!r}") + + def main() -> None: n = codec.vector_count() check(n >= 60, f"vector_count {n}") @@ -277,6 +404,9 @@ def main() -> None: out_of_range(n) malformed() objects(n) + optionals() + typed_arrays() + strings() consumer.finish() diff --git a/conformance/python/kvstore_consumer.py b/conformance/python/kvstore_consumer.py index 1ff6322c..d664270a 100644 --- a/conformance/python/kvstore_consumer.py +++ b/conformance/python/kvstore_consumer.py @@ -1,23 +1,31 @@ -"""Conformance consumer: kvstore sample, Python target (ABI revision 4). +"""Conformance consumer: kvstore sample, Python target (ABI revision 5). Drives the feature-complete producer through the generated package: * the load-time checks (importing checked the ABI revision and both - modules' contract tables; a doctored table is refused by name); + modules' contract tables; a doctored table is refused by name; a + library that can't be loaded, or isn't this one, raises + `LibraryLoadError`, an `ImportError`); * the `Store` interface: fallible and infallible constructors, methods, statics, the deprecated `size`, records (`Entry`, `StoreInfo`), the `EntryKind` enum, maps and optionals, and the logical clock; * `KvError` codes with their payload fields (`KeyNotFound`, `Expired`, - `StoreFull`, `Rejected`) and runtime codes on throwing calls; + `StoreFull`, `Rejected`, `CallbackFailed`), runtime codes on throwing + calls, and `throws: any` (`import_lines`) raising the root error; * lazy iterators of strings (throwing), records, and objects; * three callback interfaces implemented by subclassing the generated ABCs: a `Listener` (retained, filtered by `accepts`, told about every `Change`, detached when it raises, and notified from a producer thread - during compaction), a `Policy` (a record return, a typed `KvError` raised - back through `put` with its fields, an object parameter and object - return), and a `Loader` passed as an optional callback (string, bytes, - and optional-object returns; typed errors decoded by the producer or - passed through); + during compaction), a `Policy` (an optional `i64` parameter and return, + a record return, a typed `KvError` raised back through `put` with its + fields, an object parameter and object return, and any other failure + arriving as `CallbackFailed`), a `Loader` passed as an optional callback + (string, bytes, and optional-object returns; typed errors decoded by the + producer or passed through), and a `Scorer` taking and returning typed + arrays; + * the direct transports: an optional `i64` return, a `u64` typed-array + return, an iterator of optional `i64`s, and async optional `u32` and + `u32`-array results; * `Store` objects in every position, compared by native identity; * coroutines: an async free function returning an object, a cancellable method cancelled mid-pause, an async list, an async function in the @@ -27,10 +35,14 @@ Ends with every callback released and the leak check (see harness.py). """ import asyncio +import ctypes.util +import dataclasses +import importlib.util +import os import threading import time import warnings -from typing import List, Optional +from typing import List, Optional, Sequence import kvstore as kv from harness import Consumer @@ -56,12 +68,54 @@ def put(store: kv.Store, key: str, value: str, kind: kv.EntryKind = P, return store.put(key, value.encode(), kind, ttl) -def expect_key_not_found(fn, key: str, what: str) -> kv.KeyNotFound: +def expect_key_not_found(fn, key: str, what: str) -> kv.KeyNotFoundError: exc = raises(kv.KvError.KeyNotFound, fn, what) + check(type(exc) is kv.KeyNotFoundError, f"{what}: class {exc!r}") check(exc.code == 1001 and exc.key == key, f"{what}: KeyNotFound {exc.key!r}") return exc +def expect_callback_failed(fn, message: str, what: str) -> None: + exc = raises(kv.KvError.CallbackFailed, fn, what) + check(type(exc) is kv.CallbackFailedError and exc.code == 1006, f"{what}: {exc!r}") + check(exc.message == message and exc.message_ == message, + f"{what}: message {exc.message!r}") + + +def load_failures() -> None: + """Load the implementation module afresh with the library override set + to something that isn't the library: it must raise `LibraryLoadError` + (an `ImportError`) naming what went wrong.""" + var = "KVSTORE_LIBRARY" + saved = os.environ[var] + + def load(path: str) -> ImportError: + os.environ[var] = path + spec = importlib.util.spec_from_file_location("kvstore_reloaded", impl.__file__) + assert spec is not None and spec.loader is not None + try: + spec.loader.exec_module(importlib.util.module_from_spec(spec)) + except ImportError as exc: + check(type(exc).__name__ == "LibraryLoadError", f"{path}: {exc!r}") + check(exc.path == path, f"{path}: path {exc.path!r}") + return exc + finally: + os.environ[var] = saved + check(False, f"loading {path} succeeded") + raise AssertionError # unreachable + + # Not `libkvstore.dylib`: dyld searches DYLD_LIBRARY_PATH by leaf name + # even for an absolute path, and would find the real library. + missing = "/nonexistent/libkvstore-missing.dylib" + exc = load(missing) + check(str(exc).startswith(f"cannot load the kvstore native library from {var}='{missing}'"), + str(exc)) + libc = ctypes.util.find_library("c") + if libc: + exc = load(libc) + check(str(exc).startswith("the library doesn't export kvstore_abi_version"), str(exc)) + + def load_checks() -> None: # A table entry the library lacks, or one whose signature changed, is # refused by its path. @@ -69,8 +123,9 @@ def load_checks() -> None: symbol, entries = next(iter(saved.items())) try: impl._CONTRACTS[symbol] = entries + [(1, 2, "kv.Store.vanished")] - exc = raises(ImportError, impl._check_contract, "missing entry") + exc = raises(kv.LibraryLoadError, impl._check_contract, "missing entry") check(str(exc).startswith("kv.Store.vanished is missing from the library"), str(exc)) + check(isinstance(exc, ImportError) and exc.path == impl._lib._name, "an ImportError") entry_id, entry_hash, path = entries[0] impl._CONTRACTS[symbol] = [(entry_id, entry_hash ^ 1, path)] exc = raises(ImportError, impl._check_contract, "changed entry") @@ -80,6 +135,7 @@ def load_checks() -> None: impl._CONTRACTS.clear() impl._CONTRACTS.update(saved) impl._check_contract() + load_failures() def constructors() -> None: @@ -99,7 +155,7 @@ async def run() -> None: try: await kv.open_store("") check(False, "open_store('') returned") - except kv.InvalidPath as exc: + except kv.InvalidPathError as exc: check(exc.code == 1004 and exc.message == "invalid path", f"open_store {exc!r}") asyncio.run(run()) @@ -124,7 +180,7 @@ def basics() -> None: check(s.now() == 0, "now") check(s.tick(9) == 9 and s.count() == 2, "tick 9") check(s.tick(1) == 10 and s.count() == 1, "tick 1") - exc = raises(kv.Expired, lambda: s.get("ttl"), "get expired") + exc = raises(kv.ExpiredError, lambda: s.get("ttl"), "get expired") check(exc.code == 1002 and exc.key == "ttl" and exc.expired_at == 10, f"Expired {exc!r}") expect_key_not_found(lambda: s.get("ttl"), "ttl", "the expired read removed it") @@ -132,7 +188,7 @@ def basics() -> None: s.set_capacity(1) check(s.capacity() == 1, "set_capacity") put(s, "alpha", "three") - exc = raises(kv.StoreFull, lambda: put(s, "beta", "b"), "StoreFull") + exc = raises(kv.StoreFullError, lambda: put(s, "beta", "b"), "StoreFull") check(exc.capacity == 1, "StoreFull payload") s.set_capacity(100) @@ -141,6 +197,9 @@ def basics() -> None: exc = raises(kv.Error, lambda: s.put("k", b"v", 9, None), "undeclared EntryKind") check(exc.code == kv.Error.MARSHAL_ERROR_CODE and not isinstance(exc, kv.KvError), f"marshal error {exc!r}") + # A TTL out of the i64 range never reaches ctypes (which would truncate). + exc2 = raises(OverflowError, lambda: put(s, "k", "v", P, 2**63), "ttl past i64") + check(str(exc2) == "ttl_seconds: 9223372036854775808 is out of range for i64", str(exc2)) put(s, "beta", "b") check(s.delete("beta") is True and s.delete("beta") is False, "delete") @@ -164,11 +223,15 @@ def iterators() -> None: # Abandoning an iterator part-way releases it. it = s.keys("user.") + check(isinstance(it, kv.NativeIterator), "keys returns a NativeIterator") check(next(it) == "user.alice", "first key") check(impl._debug_live(2) == 1, "one live iterator") - it.close() # type: ignore[attr-defined] + it.close() check(impl._debug_live(2) == 0, "abandoned iterator released") check(list(it) == [], "a closed iterator is exhausted") + with s.keys(None) as scoped: + check(next(scoped) == "sys.x" and impl._debug_live(2) == 1, "with block") + check(impl._debug_live(2) == 0, "the with block released the iterator") entries = list(s.entries("sys.")) check(len(entries) == 1 and entries[0].key == "sys.x" and entries[0].value == b"xx", @@ -230,7 +293,7 @@ def listeners() -> None: # An expired read removes the entry and says so. put(s, "short", "x", V, 1) s.tick(1) - raises(kv.Expired, lambda: s.get("short"), "get short") + raises(kv.ExpiredError, lambda: s.get("short"), "get short") check(listener.changes[-1] == kv.ChangeRemoved(key="short", expired=True), "expired Removed") check(s.clear() == 1, "clear") check(listener.changes[-1] == kv.ChangeCleared(count=1), "Cleared") @@ -263,13 +326,24 @@ def listeners() -> None: class Policy(kv.Policy): - """Tags and encrypts admitted entries, routes `b/` keys to `other`, and - fails in various ways for some keys.""" + """Tags and encrypts admitted entries, routes `b/` keys to `other`, + decides some TTLs, and fails in various ways for some keys.""" def __init__(self, other: kv.Store) -> None: self.other = other self.admitted = 0 + def ttl_for(self, key: str, requested: Optional[int]) -> Optional[int]: + if key == "short": + return 1 + if key == "forever": + return None + if key == "ttl-fail": + raise kv.KvError.InvalidPath() + if key == "ttl-huge": + return 2**63 # out of range for i64: the callback fails + return requested + def admit(self, entry: kv.Entry) -> kv.Entry: self.admitted += 1 check(entry.version == 0, "the store assigns the version after admission") @@ -278,10 +352,11 @@ def admit(self, entry: kv.Entry) -> kv.Entry: message="secrets are not stored") if entry.key.startswith("boom"): raise RuntimeError("policy exploded") - entry.tags = ["admitted"] - entry.kind = E - entry.key = "renamed" # ignored by the store - return entry + if entry.key.startswith("odd"): + raise kv.KvError(4242, "a code KvError doesn't declare") + # Records are frozen: derive the admitted copy. + return dataclasses.replace(entry, tags=["admitted"], kind=E, + key="renamed") # the key is ignored by the store def route(self, key: str, home: kv.Store) -> kv.Store: if key.startswith("b/"): @@ -306,21 +381,35 @@ def policies() -> None: put(s, "b/x", "2", V) check(s.count() == 1 and other.count() == 1, "route redirected the write") - exc = raises(kv.Rejected, lambda: put(s, "secret", "3", V), "Rejected") + # A typed error the policy raises reaches the caller typed, with the + # domain's own message rendered from its fields. + exc = raises(kv.RejectedError, lambda: put(s, "secret", "3", V), "Rejected") check(exc.code == 1005 and exc.key == "secret" and exc.reason == "no secrets", f"Rejected payload {exc!r}") - check(exc.message == "secrets are not stored", f"Rejected message {exc.message!r}") - - exc = raises(kv.Error, lambda: put(s, "boom", "4", V), "non-domain failure") - check(exc.code == kv.Error.FOREIGN_ERROR_CODE and exc.message == "policy exploded", - f"foreign error {exc!r}") - check(not isinstance(exc, kv.KvError), "a foreign error isn't a domain error") - + check(exc.message == "write to secret rejected: no secrets", + f"Rejected message {exc.message!r}") + + # Anything else is CallbackFailed with the implementation's message. + expect_callback_failed(lambda: put(s, "boom", "4", V), "policy exploded", + "non-domain failure") + expect_callback_failed(lambda: put(s, "odd", "4", V), "a code KvError doesn't declare", + "undeclared code") # A None where an object is required is the implementation's failure. - exc = raises(kv.Error, lambda: put(s, "null/x", "6", V), "None route") - check(exc.code == kv.Error.FOREIGN_ERROR_CODE, f"None route {exc!r}") + expect_callback_failed(lambda: put(s, "null/x", "6", V), "expected Store, got NoneType", + "None route") check(s.count() == 1 and other.count() == 1, "failed puts stored nothing") - check(policy.admitted == 5, f"admitted {policy.admitted}") + check(policy.admitted == 6, f"admitted {policy.admitted}") + + # ttl_for: an optional scalar in and out, consulted before admit. + check(put(s, "short", "x", V).expires_at == 1, "ttl_for decided 1") + check(put(s, "forever", "x", V, 5).expires_at is None, "ttl_for decided none") + check(put(s, "kept", "x", V, 5).expires_at == 5, "ttl_for kept the request") + exc = raises(kv.InvalidPathError, lambda: put(s, "ttl-fail", "x", V), "ttl_for fails") + check(exc.message == "invalid path", f"ttl_for failure {exc!r}") + expect_callback_failed(lambda: put(s, "ttl-huge", "x", V), + "Policy.ttl_for() result: 9223372036854775808 is out of range for i64", + "ttl_for out of range") + check(s.count() == 4 and policy.admitted == 9, f"ttl puts {s.count()} {policy.admitted}") # Replacing the policy releases the old one; None removes it. s.set_policy(Policy(other)) @@ -328,7 +417,7 @@ def policies() -> None: s.set_policy(None) check(not s.has_policy() and held_callbacks() == base, "set_policy(None) released it") put(s, "secret", "now allowed") - check(s.count() == 2, "no policy") + check(s.count() == 5, "no policy") other.close() s.close() @@ -345,9 +434,9 @@ def fallback(self, key: str) -> Optional[kv.Store]: def load(self, key: str) -> bytes: if key == "missing": - raise kv.KeyNotFound(key="missing", message="not in the loader") + raise kv.KeyNotFoundError(key="missing", message="not in the loader") if key == "elsewhere": - raise kv.KeyNotFound(key="other", message="not in the loader") + raise kv.KeyNotFoundError(key="other", message="not in the loader") if key == "broken": raise RuntimeError("loader is broken") return f"loaded:{key}".encode() @@ -374,10 +463,9 @@ def loaders() -> None: check(s.get_or_load("missing", Loader()) is None, "KeyNotFound for this key") exc = expect_key_not_found(lambda: s.get_or_load("elsewhere", Loader()), "other", "KeyNotFound for another key") - check(exc.message == "not in the loader", f"passed-through message {exc.message!r}") - exc = raises(kv.Error, lambda: s.get_or_load("broken", Loader()), "broken loader") - check(exc.code == kv.Error.FOREIGN_ERROR_CODE and exc.message == "loader is broken", - f"broken loader {exc!r}") + check(exc.message == "key not found: other", f"passed-through message {exc.message!r}") + expect_callback_failed(lambda: s.get_or_load("broken", Loader()), "loader is broken", + "broken loader") check(held_callbacks() == base, "every loader released") s.close() @@ -460,7 +548,7 @@ def object_graph() -> None: many = kv.Store.open_many(["/a", "/b"]) check([m.path() for m in many] == ["/a", "/b"], "open_many") - raises(kv.InvalidPath, lambda: kv.Store.open_many(["/a", ""]), "open_many with ''") + raises(kv.InvalidPathError, lambda: kv.Store.open_many(["/a", ""]), "open_many with ''") named = kv.Store.by_label([info, kv.StoreInfo(label="first", store=many[0], mirror=None, count=0)]) @@ -497,6 +585,74 @@ def stats_and_report() -> None: s.close() +class Scorer(kv.Scorer): + """Scores each size as itself, fails, or returns too few scores.""" + + def __init__(self, mode: str) -> None: + self.mode = mode + self.seen: List[List[int]] = [] + + def scores(self, sizes: List[int]) -> Sequence[float]: + self.seen.append(sizes) + if self.mode == "fail": + raise ValueError("scorer is out of order") + if self.mode == "short": + return [1.0] + return tuple(size * 1.0 for size in sizes) + + +def abi5_shapes() -> None: + s = kv.Store.open("/abi5") + put(s, "b", "12") + put(s, "a", "\x01\x02\x03", V, 7) + check(s.count() == 2, "count") + + # An optional scalar return. + check(s.expires_at("a") == 7, "expires_at present") + check(s.expires_at("b") is None and s.expires_at("zzz") is None, "expires_at absent") + # A typed-array return, in key order. + check(s.value_sizes() == [3, 2], f"value_sizes {s.value_sizes()!r}") + # Optional scalar iterator items. + check(list(s.expirations()) == [7, None], "expirations") + + async def run() -> None: + # Optional scalar and typed-array async results. + check(await s.version_of("a") == 1, "version_of present") + check(await s.version_of("q") is None, "version_of absent") + put(s, "b", "x") + check(await s.versions(["b", "q", "a"]) == [2, 0, 1], "versions") + check(await s.versions([]) == [], "versions of nothing") + + asyncio.run(run()) + s.close() + + # A callback taking and returning typed arrays. + s = kv.Store.open("/rank") + put(s, "a", "1") + put(s, "b", "333") + put(s, "c", "22") + base = held_callbacks() + scorer = Scorer("sizes") + check(s.rank(scorer) == ["b", "c", "a"], "rank") + check(scorer.seen == [[1, 3, 2]], f"scores received {scorer.seen!r}") + expect_callback_failed(lambda: s.rank(Scorer("fail")), "scorer is out of order", + "a failing scorer") + expect_callback_failed(lambda: s.rank(Scorer("short")), "expected 3 scores, got 1", + "a short scorer") + check(held_callbacks() == base, "scorers released") + s.close() + + # `throws: any`: the root error with code -1 and the message. + s = kv.Store.open("/import") + check(s.import_lines("a=1\n\nb=two\n") == 2, "import_lines") + check(s.get("b").value == b"two", "imported value") + exc = raises(kv.Error, lambda: s.import_lines("c=3\nbroken\nd=4"), "import broken") + check(type(exc) is kv.Error and exc.code == -1, f"untyped error {exc!r}") + check(exc.message == "line 2: expected key=value", f"untyped message {exc.message!r}") + check(s.count() == 3, "c was stored, d wasn't") + s.close() + + def main() -> None: load_checks() constructors() @@ -508,6 +664,7 @@ def main() -> None: asyncio.run(async_calls()) object_graph() stats_and_report() + abi5_shapes() check(held_callbacks() == 0, "every callback implementation released") consumer.finish() diff --git a/conformance/python/run.sh b/conformance/python/run.sh index 6aa25e05..16a8ed5c 100755 --- a/conformance/python/run.sh +++ b/conformance/python/run.sh @@ -5,7 +5,9 @@ # against one sample and must exit 0. set -uo pipefail . "$(dirname "$0")/../lib.sh" -require_tools python python3 +. "$ROOT/scripts/toolchains.sh" +PYTHON=$(find_python) || require_tools python python3.10 +export PYTHON # Run a Python consumer against the generated package as-is: its directory # goes on PYTHONPATH, and the producer cdylib is selected through the @@ -15,7 +17,7 @@ py_consumer() { local sample="$1" script="$2" env PYTHONPATH="$GENROOT/$sample/python" \ "$(library_env "$sample")=$(sample_lib "$sample")" \ - python3 -X dev "$ROOT/conformance/python/$script" + "$PYTHON" -X dev "$ROOT/conformance/python/$script" } python_calculator() { py_consumer calculator calculator_consumer.py; } diff --git a/conformance/ruby/calculator.rb b/conformance/ruby/calculator.rb index 383d3505..6039a013 100644 --- a/conformance/ruby/calculator.rb +++ b/conformance/ruby/calculator.rb @@ -4,10 +4,13 @@ # # The minimal surface: the load-time checks (requiring the gem checked the # ABI revision and the contract table; here a stale entry is shown to fail -# with the declaration's path), i32 arithmetic that wraps, a throwing -# function whose typed error has no payload, and strings that keep -# non-ASCII text, astral characters, and interior NULs. The gem is installed -# from the generated tree; the cdylib is selected through CALCULATOR_LIBRARY. +# with the declaration's path), the private boundary (no raw C function on +# the public module), i32 arithmetic that wraps, two error domains in one +# module whose codes share a value (dispatched by the callable's domain), +# a `throws: any` function, an optional scalar return, typed arrays in and +# out, and strings that keep non-ASCII text, astral characters, and +# interior NULs. The gem is installed from the generated tree; the cdylib +# is selected through CALCULATOR_LIBRARY. require_relative 'support' require 'calculator' @@ -16,19 +19,24 @@ I32_MIN = -(2**31) run_and_check_leaks(Calculator) do - expect(Calculator::ABI_VERSION == 4, 'bindings target ABI revision 4') - missing = expect_raise(LoadError, 'an entry the library lacks') do - Calculator._wv_check_contract!(calculator_calculator_contract: [[1, 2, 'calculator.gone']]) + expect(Calculator::ABI_VERSION == 5, 'bindings target ABI revision 5') + expect(Calculator::LoadError < ::LoadError, 'load failures are LoadErrors') + missing = expect_raise(Calculator::LoadError, 'an entry the library lacks') do + bridge(Calculator).check_contract!(calculator_calculator_contract: [[1, 2, 'calculator.gone']]) end expect(missing.message.end_with?('calculator.gone is missing from the library'), missing.message) - stale = expect_raise(LoadError, 'an entry whose signature changed') do - Calculator._wv_check_contract!(calculator_calculator_contract: [[fnv1a64('calculator.add'), 0, 'calculator.add']]) + stale = expect_raise(Calculator::LoadError, 'an entry whose signature changed') do + bridge(Calculator).check_contract!(calculator_calculator_contract: [[fnv1a64('calculator.add'), 0, 'calculator.add']]) end expect(stale.message.end_with?('calculator.add changed since these bindings were generated'), stale.message) + expect(!Calculator.respond_to?(:calculator_calculator_add), 'raw C functions are private') + expect_raise(NameError, 'the native module is private') { Calculator::Native } + expect_raise(NameError, 'the runtime module is private') { Calculator::Bridge } expect(Calculator.add(2, 3) == 5, 'add') expect(Calculator.add(I32_MAX, 1) == I32_MIN, 'add wraps') expect_raise(RangeError, 'an i32 out of range') { Calculator.add(I32_MAX + 1, 0) } + expect_raise(TypeError, 'a Float for an i32') { Calculator.add(1.5, 0) } expect(Calculator.divide(10, 2) == 5, 'divide') expect(Calculator.divide(-7, 2) == -3, 'divide rounds toward zero') @@ -39,12 +47,39 @@ expect(e.message == 'division by zero', "message (got #{e.message.inspect})") expect(e.instance_variables == [:@code], 'DivisionByZero carries no payload fields') + # A second domain whose code 1 is a different error. + expect(Calculator.parse('42') == 42 && Calculator.parse(' 42 ') == 42, 'parse') + e = expect_raise(Calculator::ParseError::NotANumber, 'parse("4x")') { Calculator.parse('4x') } + expect(e.is_a?(Calculator::ParseError) && !e.is_a?(Calculator::CalcError), 'NotANumber is a ParseError') + expect(e.code == 1 && e.text == '4x', "NotANumber payload (got #{e.code}, #{e.text.inspect})") + expect(e.message == 'not a number: 4x', "NotANumber message (got #{e.message.inspect})") + e = expect_raise(Calculator::ParseError::NotANumber, 'parse("")') { Calculator.parse('') } + expect(e.text == '' && e.message == 'not a number: ', "an empty text (got #{e.message.inspect})") + + # throws: any. + expect(Calculator.sqrt(9.0) == 3.0, 'sqrt') + e = expect_raise(Calculator::Error, 'sqrt(-4)') { Calculator.sqrt(-4.0) } + expect(e.instance_of?(Calculator::Error) && e.code == -1, "an untyped error (got #{e.class}, #{e.code})") + expect(e.message == 'cannot take the square root of -4', "untyped message (got #{e.message.inspect})") + + # An optional scalar return and typed arrays. + expect(Calculator.mean([1.0, 2.0, 6.0]) == 3.0, 'mean') + expect(Calculator.mean([1, 2]) == 1.5, 'mean of Integers') + expect(Calculator.mean([]).nil?, 'mean([]) is nil') + expect_raise(TypeError, 'a non-array') { Calculator.mean(1.0) } + expect_raise(TypeError, 'a non-numeric element') { Calculator.mean(['1']) } + expect(Calculator.running_total([1, 2, 3]) == [1, 3, 6], 'running_total') + expect(Calculator.running_total([]) == [], 'running_total([])') + expect(Calculator.running_total([1, 2, 3, I32_MAX]) == [1, 3, 6, -2_147_483_643], 'running_total wraps') + expect_raise(RangeError, 'an element out of range') { Calculator.running_total([I32_MAX + 1]) } + expect(Calculator.greet('World') == 'Hello, World!', 'greet') expect(Calculator.greet('') == 'Hello, !', 'greet an empty name') crab = Calculator.greet('Wörld 🦀') expect(crab == 'Hello, Wörld 🦀!', "non-ASCII and astral text (got #{crab.inspect})") expect(crab.encoding == Encoding::UTF_8, 'greet returns UTF-8') expect(Calculator.greet("a\0b") == "Hello, a\0b!", 'an interior NUL') + expect(Calculator.greet('Wörld'.encode(Encoding::ISO_8859_1)) == 'Hello, Wörld!', 'another encoding converts') expect_raise(TypeError, 'a non-string') { Calculator.greet(42) } expect_raise(ArgumentError, 'invalid UTF-8') { Calculator.greet((+"\xC3\x28").force_encoding(Encoding::UTF_8)) } end diff --git a/conformance/ruby/codec.rb b/conformance/ruby/codec.rb index e12e94ec..ada2a3b3 100644 --- a/conformance/ruby/codec.rb +++ b/conformance/ruby/codec.rb @@ -10,7 +10,11 @@ # astral text), the typed out-of-range error and its payload, malformed # buffers sent through the raw attached function (each rejected with -3) # and the encoder's own range checks, and object identity and reference -# counting through buffers. Ends with every leak counter at zero. +# counting through buffers. Then the scalar optionals (a flag and a value, +# no buffer), typed arrays in and out (range-checked, bit-exact floats, a +# misaligned raw array rejected), `usize` and `char`, a custom type crossing +# as a string, and an iterator of typed arrays. Ends with every leak counter +# at zero. require_relative 'support' require 'codec' @@ -70,11 +74,24 @@ def holder(primary, spare: nil, many: [], by_name: {}) # Sends a raw (malformed) Vector encoding to `check_vector` through the # attached C function, which must reject it as a marshalling failure. def reject(bytes, what) - err = Codec::ErrorStruct.new - ok = Codec.codec_codec_check_vector(0, bytes, bytes.bytesize, err) + raw(what) { |err| native(Codec).codec_codec_check_vector(0, bytes, bytes.bytesize, err) } +end + +# Runs a raw C call through the private Native module, which must fail with +# -3; the error is cleared. +def raw(what) + err = native(Codec)::ErrorStruct.new + yield err code = err[:code] - Codec.codec_error_clear(err) - expect(!ok && code == -3, "#{what} is rejected with -3 (got #{code})") + native(Codec).codec_error_clear(err) + expect(code == -3, "#{what} is rejected with -3 (got #{code})") +end + +# Asserts the block raises the marshalling trap (code -3) whose message +# names `detail`. +def marshal_error(what, detail) + e = expect_raise(Codec::NativeBugError, what) { yield } + expect(e.code == -3 && e.message.end_with?(detail), "#{what}: #{e.message}") end def run_codec @@ -216,9 +233,66 @@ def run_codec release(full) closed = expect_raise(Codec::Error, 'a closed token') { p.value } expect(closed.message.include?('used after close'), closed.message) + run_direct_families n end +# The ABI 5 transports: scalar optionals, typed arrays, usize, char, a +# custom type, and an iterator of typed arrays. +def run_direct_families + i32_min = -(2**31) + i32_max = (2**31) - 1 + expect(Codec.echo_opt_i32.nil? && Codec.echo_opt_i32(nil).nil?, 'echo_opt_i32(nil)') + expect(Codec.echo_opt_i32(i32_min) == i32_min && Codec.echo_opt_i32(0).zero?, 'echo_opt_i32') + expect_raise(RangeError, 'an optional i32 out of range') { Codec.echo_opt_i32(i32_max + 1) } + expect(f64_bits(Codec.echo_opt_f64(-0.0)) == f64_bits(-0.0), 'echo_opt_f64 keeps the sign of zero') + expect(Codec.echo_opt_f64(Float::NAN).nan? && Codec.echo_opt_f64(nil).nil?, 'echo_opt_f64') + expect(Codec.echo_opt_bool(true) == true && Codec.echo_opt_bool(false) == false, 'echo_opt_bool') + expect(Codec.echo_opt_bool(nil).nil?, 'echo_opt_bool(nil)') + expect(Codec.echo_opt_color(Codec::Color::INFRARED) == -1 && Codec.echo_opt_color(Codec::Color::BLUE) == 7, + 'echo_opt_color') + expect(Codec.echo_opt_color(nil).nil?, 'echo_opt_color(nil)') + out = FFI::MemoryPointer.new(:int32) + raw('a present undeclared Color') { |err| native(Codec).codec_codec_echo_opt_color(true, 3, out, err) } + marshal_error('an undeclared Color through the wrapper', '3 is not a valid Color') { Codec.echo_opt_color(3) } + + floats = Codec.echo_f64s([Float::NAN, -0.0, 5e-324, Float::INFINITY]) + expect(floats.length == 4 && floats[0].nan?, 'echo_f64s keeps NaN') + expect(floats[1..].map { |f| f64_bits(f) } == [-0.0, 5e-324, Float::INFINITY].map { |f| f64_bits(f) }, + 'echo_f64s is bit-exact') + expect(Codec.echo_i32s([i32_min, 0, i32_max]) == [i32_min, 0, i32_max] && Codec.echo_i32s([]) == [], + 'echo_i32s') + expect_raise(RangeError, 'an i32 element out of range') { Codec.echo_i32s([i32_max + 1]) } + expect(Codec.echo_u64s([U64_MAX, 2**63]) == [18_446_744_073_709_551_615, 9_223_372_036_854_775_808], + 'echo_u64s') + expect(Codec.echo_u64s([]) == [], 'echo_u64s([])') + expect_raise(RangeError, 'a negative u64 element') { Codec.echo_u64s([-1]) } + misaligned = FFI::MemoryPointer.new(:uint8, 16) + 1 + out_len = FFI::MemoryPointer.new(:size_t) + raw('a misaligned i32 array') { |err| native(Codec).codec_codec_echo_i32s(misaligned, 2, out_len, err) } + raw('a null array with a length') { |err| native(Codec).codec_codec_echo_i32s(nil, 2, out_len, err) } + + expect(Codec.echo_usize(4_294_967_295) == 4_294_967_295 && Codec.echo_usize(U64_MAX) == U64_MAX, 'echo_usize') + expect(Codec.echo_char('🦀') == '🦀' && Codec.echo_char('é') == 'é' && Codec.echo_char('a') == 'a', 'echo_char') + marshal_error('echo_char("ab")', 'value: "ab" is not a valid char') { Codec.echo_char('ab') } + marshal_error('echo_char("")', 'value: "" is not a valid char') { Codec.echo_char('') } + expect(Codec.echo_hex('ff') == 'ff' && Codec.echo_hex('00FF') == 'ff' && Codec.echo_hex('0') == '0', 'echo_hex') + marshal_error('echo_hex("xyz")', 'value: invalid digit found in string') { Codec.echo_hex('xyz') } + marshal_error('echo_hex("")', 'value: cannot parse integer from empty string') { Codec.echo_hex('') } + marshal_error('echo_hex("100000000")', 'value: number too large to fit in target type') do + Codec.echo_hex('100000000') + end + + chunks = Codec.chunks([i32_min, 0, i32_max], 2) + expect(chunks.is_a?(Enumerator), 'chunks is an Enumerator') + expect(chunks.to_a == [[i32_min, 0], [i32_max]], 'chunks of an odd count') + expect(chunks.to_a == [[i32_min, 0], [i32_max]], 'an enumerator enumerates again') + expect(Codec.chunks([1, 2, 3, 4], 2).to_a == [[1, 2], [3, 4]], 'chunks') + expect(Codec.chunks([1, 2], 0).to_a == [] && Codec.chunks([], 3).to_a == [], 'empty chunks') + expect(Codec.chunks([1, 2, 3, 4], 1).first(2) == [[1], [2]], 'an abandoned enumeration') + expect(bridge(Codec).debug_live(2).zero?, 'abandoning an enumeration releases its iterator') +end + count = nil run_and_check_leaks(Codec) { count = run_codec } diff --git a/conformance/ruby/kvstore.rb b/conformance/ruby/kvstore.rb index f991d8f9..8c00c2b1 100644 --- a/conformance/ruby/kvstore.rb +++ b/conformance/ruby/kvstore.rb @@ -9,13 +9,18 @@ # iterators of strings, records, and objects, three callback interfaces # implemented in Ruby (a `Listener` that is retained, filtered, detached # when it raises, and called from a producer thread during compaction; a -# `Policy` with a record return, a typed error raised back through `put`, -# object parameters and returns, and malformed returns; a `Loader` passed as -# an optional callback with string, bytes, and optional-object returns), -# objects in every buffered position, blocking async calls with `cancel:` -# tokens, the nested `kv.stats` module, the sibling `report` root, and -# leak-free teardown. `_wv_debug_live(1)` (live callbacks) shows when the -# producer released an implementation. +# `Policy` with an optional-scalar parameter and return, a record return, a +# typed error raised back through `put` with the domain's message, object +# parameters and returns, and malformed returns that become +# `CallbackFailed`; a `Loader` passed as an optional callback with string, +# bytes, and optional-object returns; a `Scorer` taking and returning typed +# arrays), objects in every buffered position, blocking async calls with +# `cancel:` tokens (also under a Fiber scheduler, where the waits overlap), +# optional scalars and typed arrays as returns, async results, and iterator +# elements, a `throws: any` method, the nested `kv.stats` module, the +# sibling `report` root, and leak-free teardown (including an external +# enumeration abandoned midway, released at GC). `debug_live(1)` (live +# callbacks) shows when the producer released an implementation. require_relative 'support' require 'kvstore' @@ -25,7 +30,7 @@ KvError = Kvstore::KvError def callbacks - Kvstore._wv_debug_live(1) + bridge(Kvstore).debug_live(1) end def put(store, key, value, kind = EntryKind::PERSISTENT, ttl = nil) @@ -37,6 +42,15 @@ def entry(key, value, kind: EntryKind::PERSISTENT, version: 1) metadata: {}) end +# A consumer callback's failure, which the sample converts into +# `CallbackFailed` with the consumer's message (any message when nil). +def callback_failed(message, msg) + e = expect_raise(KvError::CallbackFailed, msg) { yield } + expect(e.code == 1006 && !e.message_.empty?, "#{msg}: CallbackFailed (got #{e.code})") + expect(e.message == e.message_ && (message.nil? || e.message == message), "#{msg}: message (got #{e.message})") + e +end + def key_not_found(key, msg) e = expect_raise(KvError::KeyNotFound, msg) { yield } expect(e.is_a?(KvError) && e.code == 1001 && e.key == key, "#{msg}: KeyNotFound for #{key} (got #{e.key})") @@ -90,8 +104,8 @@ def count(cls) end end -# A policy that rewrites admitted entries, vetoes secrets with a typed -# error, and routes `b/` keys to another store. +# A policy that picks TTLs, rewrites admitted entries, vetoes secrets with +# a typed error, and routes `b/` keys to another store. class Rewriter include Kvstore::Policy @@ -102,6 +116,17 @@ def initialize(other) @admitted = 0 end + # "short" lives one tick, "forever" never expires, "ttl-fail" fails with + # a typed error, and every other key keeps the requested TTL. + def ttl_for(key, requested) + case key + when 'short' then 1 + when 'forever' then nil + when 'ttl-fail' then raise KvError::InvalidPath, 'no ttl for you' + else requested + end + end + def admit(entry) @admitted += 1 raise "admit saw version #{entry.version}" unless entry.version.zero? @@ -112,7 +137,7 @@ def admit(entry) when /\Aboom/ raise 'policy exploded' when /\Agarbage/ - # An undeclared EntryKind: the producer rejects the record with -3. + # An undeclared EntryKind: the producer rejects the record. Kvstore::Entry.new(key: entry.key, value: entry.value, kind: 9, version: 0, expires_at: nil, tags: [], metadata: {}) else @@ -124,7 +149,7 @@ def admit(entry) def route(key, home) case key when %r{\Ab/} then @other - when %r{\Anull/} then nil # a required Store: the producer rejects it with -3 + when %r{\Anull/} then nil # a required Store: the producer rejects it else home end end @@ -207,6 +232,16 @@ def basics expect_raise(TypeError, 'a store of the wrong type') { Kvstore.summarize('not a store') } end +# Drives an iterator externally (`next`) on its own thread and abandons it +# midway, so no live stack still references the suspended enumeration. +def abandon_external(store) + Thread.new do + keys = store.keys('user.') + expect(keys.next == 'user.alice', 'an external next') + expect(bridge(Kvstore).debug_live(2) == 1, 'a suspended external enumeration holds its iterator') + end.join +end + def iterators s = Store.open('/iter') put(s, 'user.bob', 'b') @@ -216,7 +251,21 @@ def iterators expect(s.keys.to_a == ['sys.x', 'user.alice', 'user.bob'], 'keys in order') key_not_found('zzz', 'a prefix that matches nothing') { s.keys('zzz').to_a } expect(s.keys('user.').first == 'user.alice', 'a lazy first key') - expect(Kvstore._wv_debug_live(2).zero?, 'abandoning an iterator releases it') + expect(bridge(Kvstore).debug_live(2).zero?, 'abandoning an iterator releases it') + + keys = s.keys('sys.') + expect(keys.next == 'sys.x', 'an external first key') + expect_raise(StopIteration, 'an exhausted external enumeration') { keys.next } + expect(bridge(Kvstore).debug_live(2).zero?, 'exhausting an external enumeration releases it') + expect(keys.to_a == ['sys.x'], 'an external enumeration can be enumerated again') + + abandon_external(s) + GC.compact if GC.respond_to?(:compact) + released = eventually do + GC.start(full_mark: true, immediate_sweep: true) + bridge(Kvstore).debug_live(2).zero? + end + expect(released, 'collecting an abandoned external enumeration releases its iterator') entries = s.entries('sys.').to_a expect(entries.length == 1 && entries[0].key == 'sys.x' && entries[0].value == 'xx'.b, 'entries') @@ -293,15 +342,23 @@ def policies e = expect_raise(KvError::Rejected, 'a typed error from admit') { put(s, 'secret', '3') } expect(e.code == 1005 && e.key == 'secret' && e.reason == 'no secrets', 'Rejected payload') - expect(e.message == 'secrets are not stored', "the policy's message (got #{e.message})") - e = expect_raise(Kvstore::Error, 'any other exception') { put(s, 'boom', '4') } - expect(!e.is_a?(KvError) && e.code == -4 && e.message == 'policy exploded', "-4 (got #{e.code}: #{e.message})") - e = expect_raise(Kvstore::Error, 'a malformed admit return') { put(s, 'garbage', '5') } - expect(!e.is_a?(KvError) && e.code == -3, "a malformed record is -3 (got #{e.code}: #{e.message})") - e = expect_raise(Kvstore::Error, 'a nil route return') { put(s, 'null/x', '6') } - expect(!e.is_a?(KvError) && e.code == -3, "a null Store is -3 (got #{e.code}: #{e.message})") + expect(e.message == 'write to secret rejected: no secrets', "the domain's message (got #{e.message})") + callback_failed('policy exploded', 'any other exception') { put(s, 'boom', '4') } + callback_failed(nil, 'a malformed admit return') { put(s, 'garbage', '5') } + callback_failed(nil, 'a nil route return') { put(s, 'null/x', '6') } expect(s.count == 1 && other.count == 1 && p.admitted == 6, 'failed puts change nothing') + # ttl_for: an optional scalar in and out, consulted before admit. + expect(put(s, 'short', 'x', EntryKind::VOLATILE).expires_at == 1, 'ttl_for gives short a TTL') + expect(put(s, 'forever', 'x', EntryKind::VOLATILE, 5).expires_at.nil?, 'ttl_for drops the TTL of forever') + expect(put(s, 'kept', 'x', EntryKind::VOLATILE, 5).expires_at == 5, 'ttl_for keeps a requested TTL') + e = expect_raise(KvError::InvalidPath, 'a typed error from ttl_for') { put(s, 'ttl-fail', 'x') } + expect(e.message == 'invalid path', "the domain's message (got #{e.message})") + expect(s.count == 4 && p.admitted == 9, 'ttl_for runs before admit') + s.delete('short') + s.delete('forever') + s.delete('kept') + s.set_policy(Rewriter.new(other)) expect(callbacks == base + 1, 'replacing the policy releases the old one') s.set_policy(nil) @@ -330,9 +387,8 @@ def loaders expect(s.get_or_load('missing', Source.new).nil?, 'KeyNotFound for this key is none') e = key_not_found('other', 'KeyNotFound for another key passes through') { s.get_or_load('elsewhere', Source.new) } - expect(e.message == 'not in the loader', "the loader's message (got #{e.message})") - e = expect_raise(Kvstore::Error, 'any other loader failure') { s.get_or_load('broken', Source.new) } - expect(!e.is_a?(KvError) && e.code == -4 && e.message == 'loader is broken', "-4 (got #{e.code}: #{e.message})") + expect(e.message == 'key not found: other', "the domain's message (got #{e.message})") + callback_failed('loader is broken', 'any other loader failure') { s.get_or_load('broken', Source.new) } expect(callbacks == base, 'every loader is released') s.close end @@ -455,9 +511,84 @@ def stats_and_report s.close end +# A scorer returning each size as its score, failing, or returning too few. +class Sizer + include Kvstore::Scorer + + attr_reader :seen + + def initialize(mode = :sizes) + @mode = mode + end + + def scores(sizes) + @seen = sizes + case @mode + when :sizes then sizes.map(&:to_f) + when :fail then raise 'scorer is out of order' + else [1.0] + end + end +end + +def abi5_shapes + s = Store.open('/abi5') + put(s, 'b', '12') + put(s, 'a', "\x01\x02\x03", EntryKind::VOLATILE, 7) + expect(s.count == 2, 'two entries') + + # An optional scalar return, a typed-array return, and optional scalar + # iterator elements. + expect(s.expires_at('a') == 7 && s.expires_at('b').nil? && s.expires_at('zzz').nil?, 'expires_at') + expect(s.value_sizes == [3, 2], "value_sizes in key order (got #{s.value_sizes})") + expect(s.expirations.to_a == [7, nil], 'expirations') + + # An optional scalar and a typed array as async results. + expect(s.version_of('a') == 1 && s.version_of('q').nil?, 'version_of') + put(s, 'b', 'x') + expect(s.versions(%w[b q a]) == [2, 0, 1], 'versions') + s.close + + # A callback taking and returning typed arrays. + s = Store.open('/rank') + put(s, 'a', '1') + put(s, 'b', '333') + put(s, 'c', '22') + sizer = Sizer.new + expect(s.rank(sizer) == %w[b c a] && sizer.seen == [1, 3, 2], 'rank by the scorer') + callback_failed('scorer is out of order', 'a failing scorer') { s.rank(Sizer.new(:fail)) } + callback_failed('expected 3 scores, got 1', 'a short scorer') { s.rank(Sizer.new(:short)) } + s.close + + # throws: any, and a usize return. + s = Store.open('/import') + expect(s.import_lines("a=1\n\nb=two\n") == 2 && s.get('b').value == 'two'.b, 'import_lines') + e = expect_raise(Kvstore::Error, 'a malformed line') { s.import_lines("c=3\nbroken\nd=4") } + expect(e.instance_of?(Kvstore::Error) && e.code == -1, "an untyped error (got #{e.class}, #{e.code})") + expect(e.message == 'line 2: expected key=value', "the untyped message (got #{e.message})") + expect(s.count == 3, 'the lines before the failure were stored') + s.close +end + +# Two async calls waiting in two Fibers of one thread under a Fiber +# scheduler overlap: each wait yields to the scheduler. +def fibers + s = Store.open('/fibers') + results = [] + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Thread.new do + Fiber.set_scheduler(MiniScheduler.new) + 2.times { Fiber.schedule { results << s.compact(300) } } + end.join + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started + expect(results == [0, 0], "both compactions completed (got #{results})") + expect(elapsed < 0.55, "the waits overlapped (took #{elapsed.round(3)}s)") + s.close +end + run_and_check_leaks(Kvstore) do main = Thread.current - expect(Kvstore::ABI_VERSION == 4, 'bindings target ABI revision 4') + expect(Kvstore::ABI_VERSION == 5, 'bindings target ABI revision 5') constructors basics iterators @@ -467,6 +598,8 @@ def stats_and_report async_calls(main) object_graph stats_and_report + abi5_shapes + fibers expect(callbacks.zero?, 'every callback implementation was released') end diff --git a/conformance/ruby/run.sh b/conformance/ruby/run.sh index 70cc195b..21b0b1a9 100755 --- a/conformance/ruby/run.sh +++ b/conformance/ruby/run.sh @@ -5,6 +5,8 @@ # one generated gem and runs one consumer against it, which must exit 0. set -uo pipefail . "$(dirname "$0")/../lib.sh" +. "$ROOT/scripts/toolchains.sh" +use_ruby || require_tools ruby ruby3.2 require_tools ruby ruby gem # Build the sample's generated gem with `gem build`, install it into a diff --git a/conformance/ruby/support.rb b/conformance/ruby/support.rb index c7e66653..6d03fa16 100644 --- a/conformance/ruby/support.rb +++ b/conformance/ruby/support.rb @@ -30,19 +30,31 @@ def fnv1a64(text) text.bytes.reduce(0xcbf29ce484222325) { |h, b| ((h ^ b) * 0x100000001b3) & 0xffff_ffff_ffff_ffff } end +# The bindings' private runtime module (Bridge), which the consumers use +# for the leak counters and the contract check. +def bridge(mod) + mod.const_get(:Bridge) +end + +# The bindings' private module of attached C functions (Native), which the +# consumers use to send malformed input past the idiomatic wrappers. +def native(mod) + mod.const_get(:Native) +end + # Runs the consumer body on its own thread, so none of its locals survive on # a live stack once it returns, then forces collection (and the # AutoPointer finalizers that release unclosed wrappers) and asserts the # producer reports no live native resources: objects, callbacks, iterators, # cancel tokens, or byte runs. def run_and_check_leaks(mod) - expect(mod._wv_debug_live(-1) == 1, 'the library counts live resources') + expect(bridge(mod).debug_live(-1) == 1, 'the library counts live resources') Thread.new { yield }.join kinds = %w[objects callbacks iterators tokens runs] live = nil 100.times do GC.start(full_mark: true, immediate_sweep: true) - live = (0..4).map { |k| mod._wv_debug_live(k) } + live = (0..4).map { |k| bridge(mod).debug_live(k) } break if live.all?(&:zero?) sleep 0.02 @@ -50,3 +62,69 @@ def run_and_check_leaks(mod) report = kinds.zip(live).map { |k, n| "#{k}=#{n}" }.join(' ') expect(live.all?(&:zero?), "no live native resources at exit (#{report})") end + +# A minimal Fiber scheduler (Fiber.set_scheduler): enough for the blocking +# operations the bindings use while a call waits (Thread::Queue#pop and +# Mutex, through block/unblock, and sleep). It shows that an async call +# waiting in one non-blocking Fiber lets the thread's other Fibers run. +class MiniScheduler + def initialize + @ready = Thread::Queue.new + @lock = Mutex.new + @blocked = 0 + @sleeping = {} + end + + def fiber(&block) + fiber = Fiber.new(blocking: false, &block) + fiber.resume + fiber + end + + def block(_blocker, _timeout = nil) + @lock.synchronize { @blocked += 1 } + Fiber.yield + end + + # May be called from any thread. + def unblock(_blocker, fiber) + @ready << fiber + end + + def kernel_sleep(duration = nil) + @lock.synchronize { @sleeping[Fiber.current] = now + (duration || 0) } + Fiber.yield + end + + def io_wait(_io, _events, _timeout) + raise NotImplementedError, 'MiniScheduler does no I/O' + end + + # Runs every Fiber to completion (called when the thread's scheduler is + # cleared, or the thread ends). + def close + loop do + due = @lock.synchronize do + ready = @sleeping.select { |_, at| at <= now }.keys + ready.each { |f| @sleeping.delete(f) } + ready + end + due.each { |f| f.resume if f.alive? } + until @ready.empty? + fiber = @ready.pop + @lock.synchronize { @blocked -= 1 } + fiber.resume if fiber.alive? + end + break if @lock.synchronize { @blocked.zero? && @sleeping.empty? } + + Thread.pass + sleep(0.001) + end + end + + private + + def now + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end +end diff --git a/conformance/swift/calculator.swift b/conformance/swift/calculator.swift index c1fb68b3..c4a422f6 100644 --- a/conformance/swift/calculator.swift +++ b/conformance/swift/calculator.swift @@ -1,24 +1,36 @@ -// Conformance consumer: calculator sample, Swift target (ABI revision 4). +// Conformance consumer: calculator sample, Swift target (ABI revision 5). +// +// The getting-started surface through the generated `Calculator` module: +// +// * the load check: `CalculatorLibrary.check()` passes, and the contract +// comparison it runs reports a missing or changed declaration as a +// `LoadError` (driven directly through the runtime, since a matching +// library can't produce one); +// * a direct-value call (wrapping on overflow) and a string in and out +// (non-ASCII text, an astral character, and an interior NUL survive); +// * two error domains whose codes coincide: `CalcError.divisionByZero` and +// `ParseError.notANumber` with its `text` field, dispatched by the +// callable's domain; an unknown positive code of a domain (from a newer +// library) decodes to the domain's `unknown` case; +// * `throws: any`: `sqrt` fails with the runtime error, code -1, and the +// producer's message; +// * an optional scalar return (`mean -> Double?`) and typed arrays in and +// out (`runningTotal`), including empty arrays and wrapping. // -// The getting-started surface through the generated `Calculator` module: a -// direct-value call (wrapping on overflow), a throwing call whose failure is -// the typed `CalcError.divisionByZero`, and a string in and out (non-ASCII -// text, an astral character, and an interior NUL survive). The bindings check -// the ABI revision and the `calculator` contract table before the first call. // Ends by asserting the producer's leak counters are zero. Exits non-zero on // any mismatch. import CCalculator -import Calculator +@testable import Calculator import Foundation -func fail(_ msg: String) -> Never { - FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) +func fail(_ msg: String, line: UInt = #line) -> Never { + FileHandle.standardError.write(Data("assertion failed (line \(line)): \(msg)\n".utf8)) exit(1) } -func expect(_ cond: Bool, _ msg: @autoclosure () -> String) { - if !cond { fail(msg()) } +func expect(_ cond: Bool, _ msg: @autoclosure () -> String, line: UInt = #line) { + if !cond { fail(msg(), line: line) } } /// Every live-resource counter must settle at zero: 0 objects, 1 callbacks, @@ -41,11 +53,29 @@ func greet(_ name: String, _ expected: String) { expect(Array(got.utf8) == Array(expected.utf8), "greet(\(name.debugDescription)) == \(got.debugDescription)") } -func run() { - expect(Calculator.add(a: 2, b: 3) == 5, "add(2, 3)") - expect(Calculator.add(a: -7, b: 7) == 0, "add(-7, 7)") - expect(Calculator.add(a: .max, b: 1) == .min, "add wraps") +func loadCheck() { + do { + try CalculatorLibrary.check() + try CalculatorLibrary.check() + } catch { + fail("check() threw \(error)") + } + expect(CalculatorLibrary.abiVersion == 5, "abiVersion") + + // The comparison `check()` runs, against the real table: an unknown id + // is missing, a known id with another hash changed. + let missing = wvCheckContract(calculator_calculator_contract, [(0x1, 0x2, "calculator.nope")]) + expect(missing == .missing(declaration: "calculator.nope"), "missing row (got \(String(describing: missing)))") + var len = 0 + guard let first = calculator_calculator_contract(&len)?.pointee, len > 0 else { fail("empty contract table") } + let changed = wvCheckContract(calculator_calculator_contract, [(first.id, first.hash &+ 1, "calculator.x")]) + expect(changed == .changed(declaration: "calculator.x"), "changed row (got \(String(describing: changed)))") + expect(wvCheckContract(calculator_calculator_contract, [(first.id, first.hash, "calculator.x")]) == nil, "matching row") + let description = CalculatorLibrary.LoadError.missing(declaration: "calculator.nope").localizedDescription + expect(description == "Calculator: calculator.nope is missing from the library 'calculator'; regenerate the bindings or rebuild the library", description) +} +func domains() { do { expect(try Calculator.divide(a: 10, b: 2) == 5, "divide(10, 2)") expect(try Calculator.divide(a: -7, b: 2) == -3, "divide rounds toward zero") @@ -58,14 +88,77 @@ func run() { _ = try Calculator.divide(a: 1, b: 0) fail("divide(1, 0) returned") } catch let error as CalcError { - guard case let .divisionByZero(message) = error else { fail("unexpected \(error)") } - expect(error.errorCode == 1, "DivisionByZero code (got \(error.errorCode))") - expect(message == "division by zero", "DivisionByZero message (got \(message))") + expect(error == .divisionByZero(message: "division by zero"), "divide(1, 0) threw \(error)") + expect(error.errorCode == 1 && error.message == "division by zero", "DivisionByZero code and message") expect(error.localizedDescription == "division by zero", "localizedDescription") } catch { fail("divide(1, 0) threw \(error), not CalcError") } + // A second domain with a payload; its code 1 is CalcError's too. + do { + expect(try Calculator.parse(text: "42") == 42, "parse(42)") + expect(try Calculator.parse(text: " 42 ") == 42, "parse( 42 )") + } catch { + fail("parse threw \(error)") + } + for (text, message) in [("4x", "not a number: 4x"), ("", "not a number: ")] { + do { + _ = try Calculator.parse(text: text) + fail("parse(\(text.debugDescription)) returned") + } catch let error as ParseError { + expect(error == .notANumber(message: message, text: text), "parse(\(text.debugDescription)) threw \(error)") + expect(error.errorCode == 1, "NotANumber code") + } catch { + fail("parse(\(text.debugDescription)) threw \(error), not ParseError") + } + } + + // Domains are open: a code these bindings don't know keeps its code and + // message in the domain's `unknown` case. + var raw = calculator_error() + let message = Array("from a newer library".utf8) + calculator_error_set(&raw, 77, message, message.count) + let unknown = CalcError(wvError: raw) + expect(unknown == .unknown(code: 77, message: "from a newer library"), "unknown code (got \(unknown))") + expect(unknown.errorCode == 77, "unknown errorCode") + calculator_error_clear(&raw) +} + +func untyped() { + do { + expect(try Calculator.sqrt(x: 9) == 3, "sqrt(9)") + _ = try Calculator.sqrt(x: -4) + fail("sqrt(-4) returned") + } catch let error as CalculatorRuntimeError { + expect(error.errorCode == CalculatorRuntimeError.untypedCode, "sqrt(-4) code \(error.errorCode)") + expect(error.message == "cannot take the square root of -4", "sqrt(-4) message \(error.message)") + } catch { + fail("sqrt threw \(error)") + } +} + +func scalarsAndArrays() { + expect(Calculator.add(a: 2, b: 3) == 5, "add(2, 3)") + expect(Calculator.add(a: -7, b: 7) == 0, "add(-7, 7)") + expect(Calculator.add(a: .max, b: 1) == .min, "add wraps") + + expect(Calculator.mean(values: [1, 2, 6]) == 3, "mean([1, 2, 6])") + expect(Calculator.mean(values: []) == nil, "mean([]) is nil") + + expect(Calculator.runningTotal(values: [1, 2, 3]) == [1, 3, 6], "runningTotal([1, 2, 3])") + expect(Calculator.runningTotal(values: []) == [], "runningTotal([])") + expect(Calculator.runningTotal(values: [1, 2, 3, .max]) == [1, 3, 6, -2_147_483_643], "runningTotal wraps") + // A slice of an array lends its own (offset) storage. + let longer: [Int32] = [9, 1, 2, 3] + expect(Calculator.runningTotal(values: Array(longer[1...])) == [1, 3, 6], "runningTotal of a slice") +} + +func run() { + loadCheck() + domains() + untyped() + scalarsAndArrays() greet("World", "Hello, World!") greet("", "Hello, !") greet("Wörld 🦀", "Hello, Wörld 🦀!") @@ -73,7 +166,7 @@ func run() { } run() -expect(calculator_abi_version() == 4, "ABI revision 4") +expect(calculator_abi_version() == 5, "ABI revision 5") expect(calculator_debug_live(-1) == 1, "the sample counts live resources") assertNoLeaks() print("swift/calculator: OK") diff --git a/conformance/swift/codec.swift b/conformance/swift/codec.swift index f682cbc5..a06d7321 100644 --- a/conformance/swift/codec.swift +++ b/conformance/swift/codec.swift @@ -1,27 +1,33 @@ -// Conformance consumer: codec sample, Swift target (ABI revision 4). +// Conformance consumer: codec sample, Swift target (ABI revision 5). // // The shared-vector loop through the generated `Codec` module: every vector // the producer serves is decoded by the Swift codec, re-encoded on the way // back into `checkVector` (which must accept it, and reject it for the // neighboring index), and, for primitive vectors, pushed through the -// matching direct-family `echo*`. Then vectors built from Swift literals (so -// a symmetric encode/decode bug can't hide), spot checks of decoded values, -// the typed `CodecError.outOfRange` with its payload, malformed buffers sent -// through the raw C entry points (the Swift encoder can't produce them), and -// object identity and reference counting through buffers. Ends by asserting -// the producer's leak counters are zero. Exits non-zero on any mismatch. +// matching direct-family `echo*`, optional-scalar `echoOpt*`, and +// typed-array `echo*s`. Then vectors built from Swift literals (so a +// symmetric encode/decode bug can't hide), spot checks of decoded values, +// the typed `CodecError.outOfRange` with its payload, optional scalars and +// typed arrays at their edges (NaN, -0.0, extremes, empty), `usize`, `char`, +// and a custom type crossing as strings, a `NativeSequence` of typed-array +// chunks, malformed buffers, invalid optional enums, misaligned arrays, and +// rejected conversions sent through the raw C entry points (the Swift +// wrappers can't produce them, and the conversions trap there), and object +// identity (`==` and hashing by native object) and reference counting +// through buffers. Ends by asserting the producer's leak counters are zero. +// Exits non-zero on any mismatch. import CCodec import Codec import Foundation -func fail(_ msg: String) -> Never { - FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) +func fail(_ msg: String, line: UInt = #line) -> Never { + FileHandle.standardError.write(Data("assertion failed (line \(line)): \(msg)\n".utf8)) exit(1) } -func expect(_ cond: Bool, _ msg: @autoclosure () -> String) { - if !cond { fail(msg()) } +func expect(_ cond: Bool, _ msg: @autoclosure () -> String, line: UInt = #line) { + if !cond { fail(msg(), line: line) } } /// Every live-resource counter must settle at zero: 0 objects, 1 callbacks, @@ -65,25 +71,43 @@ func vectorIndex(_ n: UInt32) -> [String: UInt32] { return names } -/// Push a primitive vector's value through its direct-family echo. +/// Push a primitive vector's value through its direct-family echo, and, +/// where one exists, its optional-scalar and typed-array echoes. func echo(_ v: Vector) { switch v { case let .i8(x): expect(Codec.echoI8(value: x) == x, "echoI8(\(x))") case let .u8(x): expect(Codec.echoU8(value: x) == x, "echoU8(\(x))") case let .i16(x): expect(Codec.echoI16(value: x) == x, "echoI16(\(x))") case let .u16(x): expect(Codec.echoU16(value: x) == x, "echoU16(\(x))") - case let .i32(x): expect(Codec.echoI32(value: x) == x, "echoI32(\(x))") + case let .i32(x): + expect(Codec.echoI32(value: x) == x, "echoI32(\(x))") + expect(Codec.echoOptI32(value: x) == x, "echoOptI32(\(x))") + expect(Codec.echoI32s(values: [x, x]) == [x, x], "echoI32s([\(x)])") case let .u32(x): expect(Codec.echoU32(value: x) == x, "echoU32(\(x))") case let .i64(x): expect(Codec.echoI64(value: x) == x, "echoI64(\(x))") - case let .u64(x): expect(Codec.echoU64(value: x) == x, "echoU64(\(x))") + case let .u64(x): + expect(Codec.echoU64(value: x) == x, "echoU64(\(x))") + expect(Codec.echoUsize(value: x) == x, "echoUsize(\(x))") + expect(Codec.echoU64s(values: [x]) == [x], "echoU64s([\(x)])") case let .f32(x): expect(Codec.echoF32(value: x).bitPattern == x.bitPattern, "echoF32(\(x)) bit for bit") case let .f64(x): expect(Codec.echoF64(value: x).bitPattern == x.bitPattern, "echoF64(\(x)) bit for bit") - case let .flag(x): expect(Codec.echoBool(value: x) == x, "echoBool(\(x))") + expect(Codec.echoOptF64(value: x).map { $0.bitPattern == x.bitPattern || ($0.isNaN && x.isNaN) } == true, "echoOptF64(\(x))") + expect(Codec.echoF64s(values: [x]).map { $0.isNaN ? 0 : $0.bitPattern } == [x.isNaN ? 0 : x.bitPattern], "echoF64s([\(x)])") + case let .flag(x): + expect(Codec.echoBool(value: x) == x, "echoBool(\(x))") + expect(Codec.echoOptBool(value: x) == x, "echoOptBool(\(x))") case let .text(x): expect(same(Codec.echoText(value: x), x), "echoText(\(x.debugDescription))") case let .blob(x): expect(Codec.echoBlob(value: x) == x, "echoBlob(\(x as NSData))") - case let .hue(x): expect(Codec.echoColor(value: x) == x, "echoColor(\(x))") + case let .hue(x): + expect(Codec.echoColor(value: x) == x, "echoColor(\(x))") + expect(Codec.echoOptColor(value: x) == x, "echoOptColor(\(x))") + case let .maybeI64(x): + // An `i64?` in a buffer; through the direct `i32?` echo when it fits. + if let x = x, let small = Int32(exactly: x) { + expect(Codec.echoOptI32(value: small) == small, "echoOptI32(\(small))") + } default: break } } @@ -232,7 +256,7 @@ func malformed() { // Direct families: an undeclared enum value (which Swift's `Color` can't // even spell) and invalid UTF-8. var err = codec_error() - _ = codec_codec_echo_color(codec_codec_Color(rawValue: 3), &err) + _ = codec_codec_echo_color(3, &err) expect(err.code == -3, "echo_color(3) is rejected (got \(err.code))") codec_error_clear(&err) var len = 0 @@ -251,9 +275,15 @@ func objects(_ index: [String: UInt32]) { let expected = [10, 11, 12, 13, 20, 21, Int64.min].reduce(0, &+) expect(Codec.sumHolder(holder: h) == expected && Codec.sumHolder(holder: h) == expected, "sumHolder twice") - // primaryOf returns the very same native object (a new wrapper). + // primaryOf returns the very same native object (a new wrapper), which + // compares and hashes equal to the holder's. let p = Codec.primaryOf(holder: h) expect(p.value() == 10, "primaryOf value") + expect(p == h.primary && p.hashValue == h.primary.hashValue && Set([p, h.primary]).count == 1, "primaryOf is == the primary") + expect(p != Token(value: 10), "an equal value is a different object") + // Records carrying objects are Hashable too. + let again = Holder(primary: p, spare: h.spare, many: h.many, byName: h.byName) + expect(again == h && Set([again, h]).count == 1, "holders of the same objects are equal") expect(Codec.samePrimary(a: h, b: Holder(primary: p, spare: nil, many: [], byName: [:])), "samePrimary identity") let twin = Token(value: 10) expect(!Codec.samePrimary(a: h, b: Holder(primary: twin, spare: nil, many: [], byName: [:])), "an equal value is not the same object") @@ -264,7 +294,115 @@ func objects(_ index: [String: UInt32]) { expect(twin.value() == 10, "the twin is still usable") } +func optScalars() { + expect(Codec.echoOptI32(value: nil) == nil, "echoOptI32(nil)") + expect(Codec.echoOptI32(value: .min) == .min, "echoOptI32(min)") + expect(Codec.echoOptI32(value: 0) == 0, "echoOptI32(0)") + guard let zero = Codec.echoOptF64(value: -0.0) else { fail("echoOptF64(-0.0)") } + expect(zero == 0 && zero.sign == .minus, "echoOptF64 keeps the sign of zero") + expect(Codec.echoOptF64(value: .nan)?.isNaN == true, "echoOptF64(nan)") + expect(Codec.echoOptF64(value: nil) == nil, "echoOptF64(nil)") + expect(Codec.echoOptBool(value: true) == true && Codec.echoOptBool(value: false) == false, "echoOptBool") + expect(Codec.echoOptBool(value: nil) == nil, "echoOptBool(nil)") + expect(Codec.echoOptColor(value: .infrared) == .infrared && Codec.echoOptColor(value: .blue) == .blue, "echoOptColor") + expect(Codec.echoOptColor(value: nil) == nil, "echoOptColor(nil)") + + // A present raw value Swift's `Color` can't spell is rejected; an + // absent one is ignored. + var err = codec_error() + var out: Int32 = 0 + expect(!codec_codec_echo_opt_color(true, 3, &out, &err) && err.code == -3, "echo_opt_color(3) is rejected (got \(err.code))") + codec_error_clear(&err) + expect(!codec_codec_echo_opt_color(false, 3, &out, &err) && err.code == 0, "an absent color is ignored") +} + +func typedArrays() { + let f64s = [Double(bitPattern: 0x7ff8_0000_0000_0001), -0.0, .leastNonzeroMagnitude, .infinity] + let back = Codec.echoF64s(values: f64s) + expect(back.count == 4 && back[0].isNaN, "echoF64s NaN") + expect(back[1...].map(\.bitPattern) == f64s[1...].map(\.bitPattern), "echoF64s bit for bit") + expect(Codec.echoI32s(values: [.min, 0, .max]) == [.min, 0, .max], "echoI32s extremes") + expect(Codec.echoI32s(values: []) == [], "echoI32s([])") + expect(Codec.echoU64s(values: [.max, 1 << 63]) == [18_446_744_073_709_551_615, 9_223_372_036_854_775_808], "echoU64s extremes") + expect(Codec.echoU64s(values: []) == [], "echoU64s([])") + // An array slice copied out lends its own storage. + let wide: [UInt64] = [7, 8, 9] + expect(Codec.echoU64s(values: Array(wide.dropFirst())) == [8, 9], "echoU64s of a slice") + + // A null array with a length, and one not aligned for its element. + var err = codec_error() + var len = 0 + expect(codec_codec_echo_u64s(nil, 2, &len, &err) == nil && err.code == -3, "a null array with a length is rejected") + codec_error_clear(&err) + let raw = UnsafeMutableRawPointer.allocate(byteCount: 24, alignment: 8) + defer { raw.deallocate() } + raw.initializeMemory(as: UInt8.self, repeating: 0, count: 24) + let misaligned = UnsafePointer(OpaquePointer(raw + 4)) + expect(codec_codec_echo_u64s(misaligned, 2, &len, &err) == nil && err.code == -3, "a misaligned array is rejected") + codec_error_clear(&err) +} + +func chunks() { + let extremes = Array(Codec.chunks(values: [.min, 0, .max], size: 2)) + expect(extremes == [[-2_147_483_648, 0], [2_147_483_647]], "chunks of extremes (got \(extremes))") + let seq = Codec.chunks(values: [1, 2, 3, 4], size: 2) + do { + expect(try seq.collect() == [[1, 2], [3, 4]], "chunks of four") + } catch { + fail("collect threw \(error)") + } + expect(seq.next() == nil && seq.error == nil, "an exhausted sequence stays at the end") + expect(Array(Codec.chunks(values: [1, 2], size: 0)).isEmpty, "chunks of size 0") + expect(Array(Codec.chunks(values: [], size: 3)).isEmpty, "chunks of nothing") + // Abandoning a sequence part-way releases the native iterator. + do { + let partial = Codec.chunks(values: [1, 2, 3], size: 1) + withExtendedLifetime(partial) { + expect(partial.next() == [1], "first chunk") + expect(codec_debug_live(2) == 1, "one live iterator") + } + } + expect(codec_debug_live(2) == 0, "the abandoned iterator was released") +} + +/// Echo `input` through the raw C `f`, expecting a -3 with `message`. +func rejected( + _ f: (UnsafePointer?, Int, UnsafeMutablePointer?, UnsafeMutablePointer?) -> UnsafePointer?, + _ input: String, _ message: String +) { + var err = codec_error() + var len = 0 + let bytes = Array(input.utf8) + let out = bytes.withUnsafeBufferPointer { f($0.baseAddress, $0.count, &len, &err) } + let got = err.message_ptr.map { String(decoding: UnsafeBufferPointer(start: $0, count: err.message_len), as: UTF8.self) } ?? "" + expect(out == nil && err.code == -3 && got == message, "\(input.debugDescription) rejected with \(err.code) \(got.debugDescription)") + codec_error_clear(&err) +} + +func conversions() { + expect(Codec.echoUsize(value: 4_294_967_295) == 4_294_967_295, "echoUsize(u32 max)") + expect(Codec.echoUsize(value: .max) == .max, "echoUsize(u64 max)") + + for c in ["🦀", "é", "a"] { + expect(same(Codec.echoChar(value: c), c), "echoChar(\(c))") + } + rejected(codec_codec_echo_char, "ab", "value: \"ab\" is not a valid char") + rejected(codec_codec_echo_char, "", "value: \"\" is not a valid char") + + expect(Codec.echoHex(value: "ff") == "ff", "echoHex(ff)") + expect(Codec.echoHex(value: "00FF") == "ff", "echoHex(00FF) normalizes") + expect(Codec.echoHex(value: "0") == "0", "echoHex(0)") + rejected(codec_codec_echo_hex, "xyz", "value: invalid digit found in string") + rejected(codec_codec_echo_hex, "", "value: cannot parse integer from empty string") + rejected(codec_codec_echo_hex, "100000000", "value: number too large to fit in target type") +} + func run() { + do { + try CodecLibrary.check() + } catch { + fail("check() threw \(error)") + } let n = Codec.vectorCount() expect(n >= 60, "vectorCount (got \(n))") let index = vectorIndex(n) @@ -275,11 +413,15 @@ func run() { outOfRange(n) malformed() objects(index) + optScalars() + typedArrays() + chunks() + conversions() print("swift/codec: \(n) vectors") } run() -expect(codec_abi_version() == 4, "ABI revision 4") +expect(codec_abi_version() == 5, "ABI revision 5") expect(codec_debug_live(-1) == 1, "the sample counts live resources") assertNoLeaks() print("swift/codec: OK") diff --git a/conformance/swift/kvstore.swift b/conformance/swift/kvstore.swift index a38bb9da..ebcadd24 100644 --- a/conformance/swift/kvstore.swift +++ b/conformance/swift/kvstore.swift @@ -1,24 +1,27 @@ -// Conformance consumer: kvstore sample, Swift target (ABI revision 4). +// Conformance consumer: kvstore sample, Swift target (ABI revision 5). // // Drives the feature-complete producer through the generated `Kvstore` // module: // -// * the load-time checks (ABI revision, the `kv` and `report` contract -// tables), run by the bindings before the first call; +// * the load check (ABI revision, the `kv` and `report` contract tables), +// through `KvstoreLibrary.check()`; // * the `Store` class: a throwing static factory and an `init`, methods, // statics, the deprecated `size()`, records (`Entry`, `StoreInfo`), the // C-style `EntryKind`, maps and optionals, and the logical clock; // * `KvError` cases with their payload fields (`keyNotFound`, `expired`, // `storeFull`, `rejected`, `invalidPath`); // * lazy sequences of strings (throwing), records, and objects; -// * three callback protocols implemented in Swift: a `Listener` (retained, +// * four callback protocols implemented in Swift: a `Listener` (retained, // filtered by `accepts`, told about every `Change`, detached when it // throws, and notified from a producer thread during compaction), a -// `Policy` (a record return, a throwing method whose `KvError` and fields -// reach the `put` caller, an object parameter and object return), and a -// `Loader` passed as an optional callback (string, bytes, and -// optional-object returns; `KvError`s decoded by the producer or passed -// through); +// `Policy` (an optional-scalar parameter and return in `ttlFor`, a record +// return, a throwing method whose `KvError` and fields reach the `put` +// caller with the domain's message, any other error, including a code +// `KvError` doesn't declare, arriving as `callbackFailed`, an object +// parameter and object return), a `Loader` passed as an optional +// callback (string, bytes, and optional-object returns; `KvError`s +// decoded by the producer or passed through), and a `Scorer` (typed +// arrays in and out); // * `Store` objects in every position: parameter, return, optional, list, // map value, record field, iterator element, async result, and callback // parameter and return; @@ -27,7 +30,13 @@ // `CancellationError` while its background work stops, shown by // `activeJobs()`), an async list, an async function in the nested // `kv.stats` module, and concurrent calls; -// * the nested `kv.stats` module and the sibling `report` root. +// * the nested `kv.stats` module and the sibling `report` root; +// * the ABI 5 shapes: an optional scalar return (`expiresAt`), iterator +// item (`expirations`), and async result (`versionOf`); typed arrays as +// a return (`valueSizes`) and an async result (`versions`); a `usize` +// return and `throws: any` (`importLines`); +// * object identity: wrappers of one native object are `==` and hash +// alike, so records carrying objects are `Hashable` too. // // Ends by asserting the producer's leak counters are zero and that the // library released every callback implementation. Exits non-zero on any @@ -187,7 +196,7 @@ final class RecordingListener: Listener, @unchecked Sendable { return key != skip } - func onChange(change: Change) throws { + func onChange(change: Change) { if pthread_equal(pthread_self(), tally.origin) == 0 { tally.add("offMain") } switch change { case let .put(entry, replaced): @@ -220,6 +229,17 @@ final class TestPolicy: Policy, @unchecked Sendable { Released.tally.add("policy") } + /// "short" lives one tick, "forever" never expires, "ttl-fail" fails + /// with a typed error, and every other key keeps the requested TTL. + func ttlFor(key: String, requested: Int64?) throws -> Int64? { + switch key { + case "short": return 1 + case "forever": return nil + case "ttl-fail": throw KvError.invalidPath(message: "no ttl for you") + default: return requested + } + } + func admit(entry: Entry) throws -> Entry { tally.add("admitted") expect(entry.version == 0, "admit sees version 0") @@ -229,6 +249,10 @@ final class TestPolicy: Policy, @unchecked Sendable { if entry.key.hasPrefix("boom") { throw Refused("policy exploded") } + if entry.key.hasPrefix("unknown") { + // A code the domain doesn't declare is a plain callback failure. + throw KvError.unknown(code: 4242, message: "no such code") + } var admitted = entry admitted.tags = ["admitted"] admitted.kind = .encrypted @@ -272,6 +296,40 @@ final class TestLoader: Loader, @unchecked Sendable { } } +/// Scores each value size as itself, or fails, or returns too few scores. +final class TestScorer: Scorer, @unchecked Sendable { + enum Mode { case sizes, fail, short } + let mode: Mode + + init(_ mode: Mode) { + self.mode = mode + } + + deinit { + Released.tally.add("scorer") + } + + func scores(sizes: [UInt64]) throws -> [Double] { + switch mode { + case .sizes: + expect(sizes == [1, 3, 2], "scores receives the sizes in key order (got \(sizes))") + return sizes.map(Double.init) + case .fail: + throw Refused("scorer is out of order") + case .short: + return [Double(sizes[0])] + } + } +} + +/// `true` when `error` is `KvError.callbackFailed` with `message`. +func isCallbackFailed(_ message: String) -> (KvError) -> Bool { + { error in + guard case let .callbackFailed(rendered, field) = error else { return false } + return field == message && rendered == message && error.errorCode == 1006 + } +} + /// Calls the deprecated `size()` through a protocol witness, which doesn't /// warn at the call site. protocol Sized { @@ -343,7 +401,7 @@ func basics() { put(s, "beta", "b") expect(s.delete(key: "beta") && !s.delete(key: "beta"), "delete") - expect((s as Sized).size() == s.count() && s.count() == 1, "deprecated size") + expect(UInt64((s as Sized).size()) == s.count() && s.count() == 1, "deprecated size") expect(s.clear() == 1 && s.count() == 0, "clear") } @@ -452,17 +510,25 @@ func policies() { expect(s.count() == 1 && other.count() == 1, "route redirected b/x") // A typed error from the throwing callback reaches the caller with its - // code, message, and fields. + // code and fields, and the domain's own message for them. expectKvError("put(secret)", { try s.put(key: "secret", value: Data(), kind: .volatile, ttlSeconds: nil) }) { error in - guard case let .rejected(message, key, reason) = error else { return false } - return message == "secrets are not stored" && key == "secret" && reason == "no secrets" && error.errorCode == 1005 - } - // Any other error arrives as -4 with the consumer's message. - expectRuntimeError("put(boom)", code: -4, message: "policy exploded") { - try s.put(key: "boom", value: Data(), kind: .volatile, ttlSeconds: nil) + error == .rejected(message: "write to secret rejected: no secrets", key: "secret", reason: "no secrets") && error.errorCode == 1005 } + // Any other error, and a code the domain doesn't declare, arrive as + // `callbackFailed` with the consumer's message. + expectKvError("put(boom)", { try s.put(key: "boom", value: Data(), kind: .volatile, ttlSeconds: nil) }, isCallbackFailed("policy exploded")) + expectKvError("put(unknown)", { try s.put(key: "unknown", value: Data(), kind: .volatile, ttlSeconds: nil) }, isCallbackFailed("no such code")) expect(s.count() == 1 && other.count() == 1, "failed puts stored nothing") - expect(tally["admitted"] == 4, "admit calls") + expect(tally["admitted"] == 5, "admit calls") + + // ttlFor: an optional scalar in and out, consulted before admit. + expect(put(s, "short", "x", .volatile).expiresAt == 1, "ttlFor(short)") + expect(put(s, "forever", "x", .volatile, ttl: 5).expiresAt == nil, "ttlFor(forever)") + expect(put(s, "kept", "x", .volatile, ttl: 5).expiresAt == 5, "ttlFor keeps the requested TTL") + expectKvError("put(ttl-fail)", { try s.put(key: "ttl-fail", value: Data(), kind: .volatile, ttlSeconds: nil) }) { error in + error == .invalidPath(message: "invalid path") + } + expect(s.count() == 4 && tally["admitted"] == 8, "ttlFor puts") // Replacing the policy releases the old one; nil removes it. s.setPolicy(policy: TestPolicy(tally, other: other)) @@ -470,7 +536,7 @@ func policies() { s.setPolicy(policy: nil) expect(Released.tally["policy"] == 2 && !s.hasPolicy(), "removed policy released") put(s, "secret", "now allowed") - expect(s.count() == 2, "no policy, no veto") + expect(s.count() == 5, "no policy, no veto") } func loaders() { @@ -507,14 +573,13 @@ func loaders() { // KeyNotFound for this key: the producer decoded the payload and // answers nil. expect(loadOK("missing", TestLoader()) == nil, "missing") - // KeyNotFound for another key: passed through, payload intact. + // KeyNotFound for another key: passed through, payload intact, with the + // domain's message. expectKvError("getOrLoad(elsewhere)", { try load("elsewhere", TestLoader()) }) { error in - error.localizedDescription == "not in the loader" && isKeyNotFound("other")(error) - } - // Any other failure is -4. - expectRuntimeError("getOrLoad(broken)", code: -4, message: "loader is broken") { - try load("broken", TestLoader()) + error.localizedDescription == "key not found: other" && isKeyNotFound("other")(error) } + // Any other failure is `callbackFailed` with the loader's message. + expectKvError("getOrLoad(broken)", { try load("broken", TestLoader()) }, isCallbackFailed("loader is broken")) expect(Released.tally["loader"] == 6, "every loader released (got \(Released.tally["loader"]))") } @@ -585,29 +650,33 @@ func objectGraph() { var original: Store? = open("/graph") put(original!, "k", "v") - // share(): the same object; the original reference can go. + // share(): the same object (so `==`); the original reference can go. let s = original!.share() - expect(isSame(s, original!), "share is the same store") + expect(isSame(s, original!) && s == original! && s.hashValue == original!.hashValue, "share is the same store") original = nil _ = original expect(s.count() == 1, "alive through the shared reference") // fork(): a distinct object with a copy of the entries. let fork = s.fork() - expect(!isSame(fork, s) && fork.count() == 1 && fork.path() == "/graph", "fork") + expect(!isSame(fork, s) && fork != s && fork.count() == 1 && fork.path() == "/graph", "fork") put(fork, "k2", "v") expect(fork.count() == 2 && s.count() == 1, "fork is independent") // larger(): `Store?` in and out. let empty = open("/empty") expect(empty.larger(other: nil) == nil, "larger(nil) on an empty store") - expect(empty.larger(other: fork).map { isSame($0, fork) } == true, "larger(fork)") - expect(s.larger(other: nil).map { isSame($0, s) } == true, "larger(nil) is self") + expect(empty.larger(other: fork) == fork, "larger(fork)") + expect(s.larger(other: nil) == s, "larger(nil) is self") + expect(Set([s, s.share(), fork, s.larger(other: nil)!]).count == 2, "stores hash by identity") // describe(): a record whose fields carry objects. let info = s.describe(label: "main", mirror: fork) - expect(info.label == "main" && isSame(info.store, s) && info.count == 1, "describe") - expect(info.mirror.map { isSame($0, fork) } == true && info.mirror?.count() == 2, "describe mirror") + expect(info.label == "main" && info.store == s && info.count == 1, "describe") + expect(info.mirror == fork && info.mirror?.count() == 2, "describe mirror") + // A record carrying objects compares by their identity. + expect(info == StoreInfo(label: "main", store: s.share(), mirror: fork, count: 1), "StoreInfo ==") + expect(info != StoreInfo(label: "main", store: s.fork(), mirror: fork, count: 1), "StoreInfo != for another store") // openMany(): a list of objects; one bad path fails the whole call. guard let many = try? Store.openMany(paths: ["/a", "/b"]) else { fail("openMany") } @@ -620,8 +689,8 @@ func objectGraph() { // byLabel(): records with objects in, a map with object values out. let named = Store.byLabel(infos: [info, StoreInfo(label: "first", store: many[0], mirror: nil, count: 0)]) expect(named.count == 2, "byLabel count") - expect(named["main"].map { isSame($0, s) } == true, "byLabel main") - expect(named["first"].map { isSame($0, many[0]) } == true, "byLabel first") + expect(named["main"] == s, "byLabel main") + expect(named["first"] == many[0], "byLabel first") // totalCount(): a list, a map, and an optional record, all carrying // objects (each encoded as a fresh reference the producer adopts). @@ -669,6 +738,66 @@ func statsAndReport() { } } +@MainActor +func abi5Shapes() async { + let s = open("/abi5") + put(s, "b", "12") + _ = try! s.put(key: "a", value: Data([1, 2, 3]), kind: .volatile, ttlSeconds: 7) + expect(s.count() == 2, "count") + + // An optional scalar return. + expect(s.expiresAt(key: "a") == 7, "expiresAt(a)") + expect(s.expiresAt(key: "b") == nil && s.expiresAt(key: "zzz") == nil, "expiresAt absent") + + // A typed-array return, in key order. + expect(s.valueSizes() == [3, 2], "valueSizes") + + // Optional scalar iterator items. + let expirations = s.expirations() + expect(Array(expirations) == [7, nil], "expirations") + expect(expirations.error == nil && expirations.next() == nil, "expirations ended") + + // An optional scalar and a typed-array async result. + let a = await s.versionOf(key: "a") + let q = await s.versionOf(key: "q") + expect(a == 1 && q == nil, "versionOf (got \(String(describing: a)), \(String(describing: q)))") + put(s, "b", "x") + let versions = await s.versions(keys: ["b", "q", "a"]) + expect(versions == [2, 0, 1], "versions (got \(versions))") + + // A callback taking and returning typed arrays. + let r = open("/rank") + put(r, "a", "1") + put(r, "b", "333") + put(r, "c", "22") + do { + expect(try r.rank(scorer: TestScorer(.sizes)) == ["b", "c", "a"], "rank") + } catch { + fail("rank threw \(error)") + } + expectKvError("rank(fail)", { try r.rank(scorer: TestScorer(.fail)) }, isCallbackFailed("scorer is out of order")) + expectKvError("rank(short)", { try r.rank(scorer: TestScorer(.short)) }, isCallbackFailed("expected 3 scores, got 1")) + expect(Released.tally["scorer"] == 3, "every scorer released") + + // `throws: any` and a `usize` return: the runtime error, code -1. + let i = open("/import") + do { + expect(try i.importLines(text: "a=1\n\nb=two\n") == 2, "importLines") + expect(try i.get(key: "b").value == Data("two".utf8), "imported b") + } catch { + fail("importLines threw \(error)") + } + expectRuntimeError("importLines(broken)", code: KvstoreRuntimeError.untypedCode, message: "line 2: expected key=value") { + try i.importLines(text: "c=3\nbroken\nd=4") + } + expect(i.count() == 3, "c was stored, d wasn't") +} + +do { + try KvstoreLibrary.check() +} catch { + fail("check() threw \(error)") +} await constructors() basics() iterators() @@ -678,8 +807,9 @@ loaders() await asyncCalls() objectGraph() statsAndReport() +await abi5Shapes() -expect(kvstore_abi_version() == 4, "ABI revision 4") +expect(kvstore_abi_version() == 5, "ABI revision 5") expect(kvstore_debug_live(-1) == 1, "the sample counts live resources") assertNoLeaks() expect(Released.tally["listener"] == 5, "every listener released (got \(Released.tally["listener"]))") diff --git a/crates/weaveffi-cli/Cargo.toml b/crates/weaveffi-cli/Cargo.toml index 609510f0..e92f22fd 100644 --- a/crates/weaveffi-cli/Cargo.toml +++ b/crates/weaveffi-cli/Cargo.toml @@ -37,14 +37,13 @@ pkg-fmt = "zip" clap = { workspace = true } clap_complete = { workspace = true } miette = { workspace = true } -anyhow = { workspace = true } camino = { workspace = true } serde = { workspace = true } toml = { workspace = true } weaveffi-model = { workspace = true, features = ["idl"] } rayon = { workspace = true } heck = { workspace = true } -serde_yaml = { workspace = true } +serde-saphyr = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } tempfile = { workspace = true } @@ -58,7 +57,7 @@ object = { version = "0.37", default-features = false, features = ["read", "wasm [dev-dependencies] assert_cmd = "2" predicates = "3" -serde_yaml = { workspace = true } +serde-saphyr = { workspace = true } serde_json = { workspace = true } toml = { workspace = true } insta = { workspace = true } diff --git a/crates/weaveffi-cli/benches/codegen_bench.rs b/crates/weaveffi-cli/benches/codegen_bench.rs index 0be03a27..86e0c2bc 100644 --- a/crates/weaveffi-cli/benches/codegen_bench.rs +++ b/crates/weaveffi-cli/benches/codegen_bench.rs @@ -1,6 +1,5 @@ use std::path::Path; -use camino::Utf8Path; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use weaveffi_model::ir::{ Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, @@ -18,7 +17,7 @@ fn model(api: &Api) -> Model { fn calculator_api() -> Api { Api { - version: "0.11.0".to_string(), + version: "0.12.0".to_string(), modules: vec![Module { name: "calculator".to_string(), doc: None, @@ -41,7 +40,7 @@ fn calculator_api() -> Api { returns: Some(TypeRef::Prim(Prim::I32)), r#async: false, cancellable: false, - throws: false, + throws: None, deprecated: None, }, Function { @@ -62,7 +61,7 @@ fn calculator_api() -> Api { returns: Some(TypeRef::Prim(Prim::I32)), r#async: false, cancellable: false, - throws: false, + throws: None, deprecated: None, }, Function { @@ -83,7 +82,7 @@ fn calculator_api() -> Api { returns: Some(TypeRef::Prim(Prim::I32)), r#async: false, cancellable: false, - throws: false, + throws: None, deprecated: None, }, Function { @@ -97,14 +96,14 @@ fn calculator_api() -> Api { returns: Some(TypeRef::Prim(Prim::String)), r#async: false, cancellable: false, - throws: false, + throws: None, deprecated: None, }, ], structs: vec![], enums: vec![], callback_interfaces: vec![], - errors: None, + errors: Vec::new(), interfaces: vec![], modules: vec![], }], @@ -196,7 +195,7 @@ fn large_api() -> Api { ))))), r#async: false, cancellable: false, - throws: false, + throws: None, deprecated: None, }) .collect(); @@ -208,7 +207,7 @@ fn large_api() -> Api { structs, enums, callback_interfaces: vec![], - errors: None, + errors: Vec::new(), interfaces: vec![], modules: vec![], } @@ -216,7 +215,7 @@ fn large_api() -> Api { .collect(); Api { - version: "0.11.0".to_string(), + version: "0.12.0".to_string(), modules, } } @@ -262,12 +261,14 @@ fn bench_validate_kitchen_sink(c: &mut Criterion) { /// Every target rendering the kitchen-sink fixture in memory (no I/O). fn bench_render_kitchen_sink(c: &mut Criterion) { let model = model(&load_kitchen_sink_unvalidated()); - let targets = weaveffi_cli::targets::all_default(); - let out_dir = Utf8Path::new("out"); + let targets: Vec<_> = weaveffi_cli::targets::REGISTRY + .iter() + .map(|d| d.build_default()) + .collect(); c.bench_function("render_kitchen_sink", |b| { b.iter(|| { for t in &targets { - black_box(t.render(black_box(&model), out_dir)); + black_box(t.render(black_box(&model))); } }); }); diff --git a/crates/weaveffi-cli/src/backend/mod.rs b/crates/weaveffi-cli/src/backend/mod.rs deleted file mode 100644 index 3f364a0b..00000000 --- a/crates/weaveffi-cli/src/backend/mod.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! The language-backend framework. -//! -//! [`LanguageBackend`] is the one trait a target implements: a stable name, -//! a pure [`files`](LanguageBackend::files) that renders the complete output -//! set from the [`Model`], and an optional -//! [`package`](LanguageBackend::package) for `weaveffi package`. -//! [`ConfiguredBackend`](crate::codegen::ConfiguredBackend) exposes the -//! backend to the orchestrator through the object-safe -//! [`Target`](crate::codegen::Target) trait. A backend owns *only* -//! language-specific rendering: type mapping, marshalling, file layout, and -//! the exact text of each declaration. Model construction, erasure, and every -//! write live elsewhere, once. -//! -//! [`Model`]: weaveffi_model::model::Model - -use camino::{Utf8Path, Utf8PathBuf}; - -use crate::package::{Artifact, PackageContext}; -use weaveffi_model::model::Model; - -/// A single generated file: its full path (under the output directory) and the -/// rendered contents. Backends return these from [`LanguageBackend::files`]; -/// the driver creates parent directories and writes them. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OutputFile { - /// Full path to write, under (or anchored at) the output directory. - pub path: Utf8PathBuf, - /// The rendered file contents. - pub contents: String, -} - -impl OutputFile { - /// Pair a destination path with its rendered contents. - /// - /// The path is normalized to `/` separators, which every platform's file - /// APIs accept, so listings, cache records, and tests see the same path - /// on Windows as elsewhere. - pub fn new(path: impl Into, contents: impl Into) -> Self { - let path: Utf8PathBuf = path.into(); - let path = if path.as_str().contains('\\') { - Utf8PathBuf::from(path.as_str().replace('\\', "/")) - } else { - path - }; - Self { - path, - contents: contents.into(), - } - } -} - -/// An idiomatic language backend over the shared [`Model`]. -/// -/// The required methods are [`name`](Self::name) and [`files`](Self::files), -/// which assembles the complete output set; wrap the type in -/// [`ConfiguredBackend`](crate::codegen::ConfiguredBackend) to hand it to the -/// orchestrator. Rendering is pure: a backend returns [`OutputFile`]s and the -/// orchestrator does the I/O. Each backend walks the model in whatever order -/// its language needs (C and C++ order declarations by dependency, Swift -/// splits types from the namespaced module body, Kotlin, Node.js, and Wasm -/// render parallel files) and emits its own doc comments, sharing -/// [`emit_doc`](crate::codegen::common::emit_doc) for the common line and -/// block flavors. -pub trait LanguageBackend: Send + Sync { - /// Per-target, fully typed configuration (the `[generators.]` - /// table). - type Config: Default + Clone + Send + Sync; - - /// Stable short name (`"swift"`, `"python"`, ...): the `--target` token - /// and the generation-record file basename. - fn name(&self) -> &'static str; - - /// Assemble the complete output set from the validated `model`, which - /// carries the library's identity alongside every symbol and signature. - /// Most backends render a primary source file from `model.modules`, then - /// append package manifests (`package.json`, `pyproject.toml`, `go.mod`, - /// ...) as additional [`OutputFile`]s. - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec; - - /// Assemble the installable artifacts that bundle the per-platform builds - /// in `ctx.binaries`, returning `None` when this target has no packaging. - /// - /// This is the `weaveffi package` analogue of [`files`](Self::files): it - /// returns [`Artifact`]s (wheels, npm tarballs, gems, directory trees) - /// with paths relative to the dist directory, and the - /// [`write_artifact`](crate::package::write_artifact) driver does the - /// I/O. A target skips platforms its ecosystem has no slot for and - /// returns an empty list when none of `ctx.binaries` fits. The default - /// returns `None`. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { - let _ = (model, ctx, config); - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_model::ir::{Api, Function, Module, Param, TypeRef}; - use weaveffi_model::pkg::Identity; - use weaveffi_model::ty::Prim; - - #[derive(Default, Clone)] - struct FakeConfig; - - /// A trivial backend that lists each function's C symbol, so we can - /// assert the model carries the identity's prefix. - struct FakeBackend; - - impl LanguageBackend for FakeBackend { - type Config = FakeConfig; - - fn name(&self) -> &'static str { - "fake" - } - - fn files( - &self, - model: &Model, - out_dir: &Utf8Path, - _config: &Self::Config, - ) -> Vec { - let mut out = String::new(); - for m in &model.modules { - out.push_str(&format!("module {}\n", m.path)); - for f in &m.functions { - out.push_str(&format!("fn {} {}\n", f.name, f.c_base)); - } - } - vec![OutputFile::new(out_dir.join("fake/out.txt"), out)] - } - } - - fn model(name: &str) -> Model { - let api = Api { - version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: "math".into(), - doc: None, - functions: vec![Function { - name: "add".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::Prim(Prim::I32), - doc: None, - }], - returns: Some(TypeRef::Prim(Prim::I32)), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callback_interfaces: vec![], - errors: None, - modules: vec![], - }], - }; - weaveffi_model::validate::validate(&api, &Identity::named(name), None).unwrap() - } - - #[test] - fn backends_render_with_the_identity_prefix() { - use crate::codegen::{ConfiguredBackend, Target}; - let out_dir = Utf8Path::new("out"); - let render = |n: &str| { - let files = ConfiguredBackend::new(FakeBackend, FakeConfig).render(&model(n), out_dir); - assert_eq!(files.len(), 1); - assert_eq!(files[0].path, out_dir.join("fake/out.txt")); - files[0].contents.clone() - }; - assert_eq!( - render("weaveffi"), - "module math\nfn add weaveffi_math_add\n" - ); - assert_eq!(render("acme"), "module math\nfn add acme_math_add\n"); - } -} diff --git a/crates/weaveffi-cli/src/build/glue.rs b/crates/weaveffi-cli/src/build/glue.rs index c5cb4ef1..57feada8 100644 --- a/crates/weaveffi-cli/src/build/glue.rs +++ b/crates/weaveffi-cli/src/build/glue.rs @@ -8,21 +8,126 @@ use std::process::Command; -use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use crate::build::ndk::Ndk; -use crate::build::BuildSettings; -use crate::platform::{jni_shim_name, node_addon_name, Os, Platform}; +use crate::build::{BuildSettings, Builder}; +use crate::package::Skips; +use crate::platform::{jni_shim_name, node_addon_name, BinarySet, Os, Platform}; +use crate::targets::{Glue, GlueKind}; + +/// Prebuild `glue` for every platform in `binaries` it targets (macOS and +/// Linux for a Node.js addon, and Android too for a JNI shim; Windows glue is +/// never prebuilt, since the packages build it there), staging its sources under `{dir}/.glue/` and recording each library in +/// `binaries`. A platform this host can't compile it for, or whose +/// compiler, headers, JDK, or NDK is missing, is skipped (recorded in +/// `skips`); the packages then fall back to compiling the glue at install or +/// build time. +pub(crate) fn prebuild( + glue: &Glue, + dir: &Utf8Path, + binaries: &mut BinarySet, + builder: &Builder<'_>, + settings: &BuildSettings, + skips: &Skips, +) -> Result<()> { + let platforms: Vec = binaries + .platforms() + .filter(|p| match glue.kind { + GlueKind::NodeAddon => matches!(p.os(), Os::MacOs | Os::Linux), + GlueKind::JniShim => matches!(p.os(), Os::MacOs | Os::Linux | Os::Android), + }) + .collect(); + if platforms.is_empty() { + return Ok(()); + } + let (what, staging) = match glue.kind { + GlueKind::NodeAddon => ("the Node.js addon", dir.join(".glue/node-addon")), + GlueKind::JniShim => ("the JNI shim", dir.join(".glue/jni-shim")), + }; + std::fs::create_dir_all(staging.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create {staging}"))?; + for file in [&glue.source, &glue.header] { + let name = file.path.file_name().unwrap_or_default(); + std::fs::write(staging.join(name).as_std_path(), &file.contents) + .into_diagnostic() + .wrap_err_with(|| format!("failed to stage {name}"))?; + } + let source = GlueSource { + source: staging.join(glue.source.path.file_name().unwrap_or_default()), + include_dir: staging, + }; + let library = binaries.lib_name.clone(); + let node_include = match glue.kind { + GlueKind::NodeAddon => match node_headers() { + Ok(headers) => Some(headers), + Err(e) => { + for p in &platforms { + skips.skip(format!("{what} for {}", p.id()), one_line(&e)); + } + return Ok(()); + } + }, + GlueKind::JniShim => None, + }; + for platform in platforms { + let platform_dir = dir.join(platform.id()); + let built = match (&glue.kind, &node_include) { + (GlueKind::NodeAddon, Some(include)) => compile_node_addon( + &source, + platform, + &platform_dir, + &library, + include, + settings, + ), + _ => { + let ndk = match platform.os() { + Os::Android => builder.ndk().map(Some), + _ => Ok(None), + }; + ndk.and_then(|ndk| { + compile_jni_shim(&source, platform, &platform_dir, &library, ndk, settings) + }) + } + }; + match built { + Ok(path) => { + if let Some(nb) = binaries + .binaries + .iter_mut() + .find(|b| b.platform == platform) + { + match glue.kind { + GlueKind::NodeAddon => nb.node_addon = Some(path), + GlueKind::JniShim => nb.jni_shim = Some(path), + } + } + } + Err(e) => skips.skip(format!("{what} for {}", platform.id()), one_line(&e)), + } + } + Ok(()) +} + +/// An error and its causes on one line, for a skip warning. +pub(crate) fn one_line(e: &miette::Report) -> String { + e.chain() + .map(ToString::to_string) + .collect::>() + .join(": ") +} /// One glue library to compile: its C source and the directory holding the /// header it includes. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct GlueSource { +pub(crate) struct GlueSource { /// The C source file. - pub source: Utf8PathBuf, + pub(crate) source: Utf8PathBuf, /// The directory holding the producer's C header. - pub include_dir: Utf8PathBuf, + pub(crate) include_dir: Utf8PathBuf, } /// Check that this host can compile desktop glue for `platform`: macOS @@ -31,7 +136,7 @@ pub struct GlueSource { /// # Errors /// /// Returns an error explaining which host can compile it instead. -pub fn check_desktop_host(platform: Platform) -> Result<()> { +pub(crate) fn check_desktop_host(platform: Platform) -> Result<()> { let ok = match platform.os() { Os::MacOs => cfg!(target_os = "macos"), Os::Linux => cfg!(target_os = "linux") && Platform::host() == Some(platform), @@ -76,7 +181,7 @@ fn apple_flags(platform: Platform, settings: &BuildSettings) -> Vec { /// /// Returns an error explaining how to make the headers available when none /// are found. -pub fn node_headers() -> Result { +pub(crate) fn node_headers() -> Result { let has_headers = |dir: &Utf8Path| dir.join("node_api.h").is_file() && dir.join("uv.h").is_file(); if let Ok(nodedir) = std::env::var("npm_config_nodedir") { @@ -121,7 +226,7 @@ pub fn node_headers() -> Result { .into_iter() .find(|d| has_headers(d)) .ok_or_else(|| { - anyhow::anyhow!( + miette::miette!( "Node.js's C headers (node_api.h) weren't found next to {exec}; install them with \ `npx node-gyp install` or point npm_config_nodedir at a Node.js source or headers \ directory" @@ -136,7 +241,7 @@ pub fn node_headers() -> Result { /// /// Returns an error when this host can't compile for `platform`, the /// headers are missing, or the compiler fails. -pub fn compile_node_addon( +pub(crate) fn compile_node_addon( glue: &GlueSource, platform: Platform, dir: &Utf8Path, @@ -182,7 +287,7 @@ pub fn compile_node_addon( /// # Errors /// /// Returns an error when no JDK with `jni.h` is found. -pub fn jdk_includes() -> Result> { +pub(crate) fn jdk_includes() -> Result> { let mut homes = Vec::new(); if let Some(home) = std::env::var("JAVA_HOME").ok().filter(|h| !h.is_empty()) { homes.push(Utf8PathBuf::from(home)); @@ -217,7 +322,9 @@ pub fn jdk_includes() -> Result> { return Ok(vec![include.clone(), include.join(os_dir)]); } } - bail!("no JDK with jni.h was found; install a JDK and set JAVA_HOME") + Err(miette::miette!( + "no JDK with jni.h was found; install a JDK and set JAVA_HOME" + )) } /// Compile the JNI shim `glue` for `platform` (a desktop JVM platform, or @@ -228,7 +335,7 @@ pub fn jdk_includes() -> Result> { /// /// Returns an error when this host can't compile for `platform`, the JDK or /// NDK is missing, or the compiler fails. -pub fn compile_jni_shim( +pub(crate) fn compile_jni_shim( glue: &GlueSource, platform: Platform, dir: &Utf8Path, @@ -252,10 +359,12 @@ pub fn compile_jni_shim( .arg("-Wl,-z,max-page-size=16384"); cmd } - Os::Windows => bail!( - "prebuilding the JNI shim for windows-x64 isn't supported; build it with the \ + Os::Windows => { + return Err(miette::miette!( + "prebuilding the JNI shim for windows-x64 isn't supported; build it with the \ generated CMake project on Windows" - ), + )) + } Os::MacOs | Os::Linux => { check_desktop_host(platform)?; let mut cmd = Command::new(desktop_cc()); @@ -273,7 +382,7 @@ pub fn compile_jni_shim( } cmd } - Os::Ios | Os::Wasm => bail!("{} has no JVM", platform.id()), + Os::Ios | Os::Wasm => return Err(miette::miette!("{} has no JVM", platform.id())), }; cmd.args(["-shared", "-fPIC", "-O2"]) .arg(format!("-I{}", glue.include_dir)) @@ -291,7 +400,8 @@ fn run(mut cmd: Command, what: &str, platform: Platform) -> Result<()> { let program = cmd.get_program().to_string_lossy().into_owned(); let out = cmd .output() - .with_context(|| format!("failed to run `{program}` to compile {what}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to run `{program}` to compile {what}"))?; if !out.status.success() { bail!( "compiling {what} for {} failed:\n{}", diff --git a/crates/weaveffi-cli/src/build/mod.rs b/crates/weaveffi-cli/src/build/mod.rs index 9a09554d..c7aeefda 100644 --- a/crates/weaveffi-cli/src/build/mod.rs +++ b/crates/weaveffi-cli/src/build/mod.rs @@ -13,14 +13,14 @@ //! Node.js addon and JNI shim and `weaveffi package` reads them back as a //! [`BinarySet`](crate::platform::BinarySet). -pub mod glue; -pub mod ndk; +pub(crate) mod glue; +pub(crate) mod ndk; use std::process::{Command, Stdio}; use std::sync::OnceLock; -use anyhow::{bail, Context, Result}; use camino::Utf8PathBuf; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use crate::cargo::{cargo_bin, CargoCrate}; use crate::platform::{NativeBinary, Os, Platform}; @@ -29,22 +29,22 @@ use self::ndk::Ndk; /// Build settings shared by every platform (the `[build]` table). #[derive(Debug, Clone, PartialEq, Eq)] -pub struct BuildSettings { - /// Build with the release profile (`--release`). - pub release: bool, +pub(crate) struct BuildSettings { + /// The Cargo profile to build with (`--profile`). + pub(crate) profile: String, /// The minimum macOS version (`MACOSX_DEPLOYMENT_TARGET`). - pub macos_deployment_target: String, + pub(crate) macos_deployment_target: String, /// The minimum iOS version (`IPHONEOS_DEPLOYMENT_TARGET`). - pub ios_deployment_target: String, + pub(crate) ios_deployment_target: String, /// The minimum Android API level, which selects the NDK compiler. - pub android_api: u32, + pub(crate) android_api: u32, } impl Default for BuildSettings { /// Release builds for macOS 11.0, iOS 13.0, and Android API 21. fn default() -> Self { Self { - release: true, + profile: "release".into(), macos_deployment_target: "11.0".into(), ios_deployment_target: "13.0".into(), android_api: 21, @@ -53,7 +53,7 @@ impl Default for BuildSettings { } /// Cross-compiles one producer crate into `{target_dir}/weaveffi//`. -pub struct Builder<'a> { +pub(crate) struct Builder<'a> { krate: &'a CargoCrate, library: &'a str, settings: &'a BuildSettings, @@ -65,7 +65,11 @@ pub struct Builder<'a> { impl<'a> Builder<'a> { /// A builder for `krate` whose outputs are named after `library` (the /// identity's library, which the generated bindings load). - pub fn new(krate: &'a CargoCrate, library: &'a str, settings: &'a BuildSettings) -> Self { + pub(crate) fn new( + krate: &'a CargoCrate, + library: &'a str, + settings: &'a BuildSettings, + ) -> Self { Self { krate, library, @@ -78,7 +82,7 @@ impl<'a> Builder<'a> { /// Pass `--quiet` to Cargo. #[must_use] - pub fn quiet(mut self, quiet: bool) -> Self { + pub(crate) fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; self } @@ -86,7 +90,7 @@ impl<'a> Builder<'a> { /// The directory a platform's outputs are laid out in: /// `{target_dir}/weaveffi/`. #[must_use] - pub fn output_dir(&self, platform: Platform) -> Utf8PathBuf { + pub(crate) fn output_dir(&self, platform: Platform) -> Utf8PathBuf { self.krate.weaveffi_dir().join(platform.id()) } @@ -95,11 +99,11 @@ impl<'a> Builder<'a> { /// # Errors /// /// Returns an error when no NDK is found. - pub fn ndk(&self) -> Result<&Ndk> { + pub(crate) fn ndk(&self) -> Result<&Ndk> { self.ndk .get_or_init(|| Ndk::locate().map_err(|e| e.to_string())) .as_ref() - .map_err(|e| anyhow::anyhow!("{e}")) + .map_err(|e| miette::miette!("{e}")) } /// Check, before compiling anything, that `platform` can be built here: @@ -109,7 +113,7 @@ impl<'a> Builder<'a> { /// # Errors /// /// Returns an error naming the missing piece and how to add it. - pub fn check(&self, platform: Platform) -> Result<()> { + pub(crate) fn check(&self, platform: Platform) -> Result<()> { platform.check_host()?; let triple = platform.rust_target(); if let Some(installed) = self.installed_targets() { @@ -155,7 +159,7 @@ impl<'a> Builder<'a> { /// /// Returns an error when [`check`](Self::check) fails, Cargo fails, or /// Cargo produces none of the expected libraries. - pub fn build(&self, platform: Platform) -> Result { + pub(crate) fn build(&self, platform: Platform) -> Result { self.check(platform)?; let triple = platform.rust_target(); let library = self.library; @@ -186,9 +190,7 @@ impl<'a> Builder<'a> { .arg("--manifest-path") .arg(self.krate.manifest_path.as_str()) .args(["--target", triple, "--crate-type", crate_types]); - if self.settings.release { - cmd.arg("--release"); - } + cmd.args(["--profile", &self.settings.profile]); if self.quiet { cmd.arg("--quiet"); } @@ -228,7 +230,8 @@ impl<'a> Builder<'a> { .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .output() - .context("failed to run cargo")?; + .into_diagnostic() + .wrap_err("failed to run cargo")?; if !output.status.success() { bail!( "cargo failed to build `{}` for {} ({triple}); see its errors above", @@ -241,17 +244,20 @@ impl<'a> Builder<'a> { let dir = self.output_dir(platform); if dir.exists() { std::fs::remove_dir_all(dir.as_std_path()) - .with_context(|| format!("failed to clear {dir}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to clear {dir}"))?; } std::fs::create_dir_all(dir.as_std_path()) - .with_context(|| format!("failed to create {dir}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to create {dir}"))?; let place = |suffix: &str, name: String| -> Result> { let Some(source) = produced.iter().find(|p| p.as_str().ends_with(suffix)) else { return Ok(None); }; let dest = dir.join(name); std::fs::copy(source.as_std_path(), dest.as_std_path()) - .with_context(|| format!("failed to copy {source} to {dest}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to copy {source} to {dest}"))?; Ok(Some(dest)) }; let primary = platform.lib_filename(library); diff --git a/crates/weaveffi-cli/src/build/ndk.rs b/crates/weaveffi-cli/src/build/ndk.rs index 87e4e856..b128266f 100644 --- a/crates/weaveffi-cli/src/build/ndk.rs +++ b/crates/weaveffi-cli/src/build/ndk.rs @@ -1,7 +1,7 @@ //! Locating the Android NDK and its per-API-level compilers. -use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use crate::platform::Platform; @@ -57,11 +57,11 @@ impl Ndk { return Ok(Self { root: bundle }); } } - bail!( + Err(miette::miette!( "the Android NDK wasn't found: set ANDROID_NDK_HOME to an NDK directory, or install \ one with the Android SDK manager (`sdkmanager --install \"ndk;27.2.12479018\"`) so \ it lands in $ANDROID_HOME/ndk/" - ) + )) } /// The directory of the NDK's LLVM toolchain binaries for this host. @@ -73,16 +73,17 @@ impl Ndk { let prebuilt = self.root.join("toolchains/llvm/prebuilt"); let host = prebuilt .read_dir_utf8() - .with_context(|| format!("the NDK at {} has no {prebuilt}", self.root))? + .into_diagnostic() + .wrap_err_with(|| format!("the NDK at {} has no {prebuilt}", self.root))? .filter_map(Result::ok) .map(|e| e.path().to_path_buf()) .find(|p| p.join("bin").is_dir()); match host { Some(host) => Ok(host.join("bin")), - None => bail!( + None => Err(miette::miette!( "the NDK at {} has no LLVM toolchain under {prebuilt}", self.root - ), + )), } } diff --git a/crates/weaveffi-cli/src/cabi.rs b/crates/weaveffi-cli/src/cabi.rs index e7273912..72515d4e 100644 --- a/crates/weaveffi-cli/src/cabi.rs +++ b/crates/weaveffi-cli/src/cabi.rs @@ -1,38 +1,47 @@ -//! Shared rendering of the **C ABI declarations** from a -//! [`Model`]. +//! Rendering of the **C ABI declarations** from a [`Model`]: the body of +//! `{library}.h`. //! -//! Both the C generator (which emits the canonical `{library}.h`) and the C++ -//! generator (whose idiomatic wrapper opens an `extern "C"` block re-declaring -//! the same symbols) render their C declarations through this module, so the -//! two can't drift. +//! The header is the C ABI every other target binds to. The C target emits +//! it, and the C++, Swift, Go, Kotlin (JNI shim), and Node.js (addon) +//! targets ship or include the same file, so everything here renders from +//! the lowered model: every slot list is an [`AbiFn`] the model computed, +//! never re-derived. //! //! The normative description of what is rendered here is //! `docs/src/reference/abi.md`. use std::fmt::Write; -use crate::codegen::common::{emit_doc, DocCommentStyle}; +use crate::codegen::common::{self, DocCommentStyle}; +use crate::codegen::contract::{self, ContractTable}; +use crate::codegen::docs::{ApiNames, Doc}; +use crate::codegen::errors::{self, ErrorTable}; use crate::codegen::CodeWriter; use crate::lang::{is_reserved, CPP_KEYWORDS, C_KEYWORDS}; use weaveffi_model::abi::AbiParam; use weaveffi_model::model::{ - contract_check_symbol, contract_symbol, AbiFn, CallShape, CallbackInterfaceBinding, - EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, Model, ModuleBinding, + AbiFn, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, EnumBinding, FnBinding, + InterfaceBinding, Model, ModuleBinding, }; +use weaveffi_model::plan::ErrorStrategy; -/// The revision of the WeaveFFI C ABI the generators emit bindings for. -/// /// The C ABI revision the generated headers declare. Mirrors -/// `weaveffi::abi::ABI_VERSION`; the two are kept equal by a test in this -/// crate. -pub use weaveffi_model::model::ABI_VERSION; +/// `weaveffi::abi::ABI_VERSION`; a test in this crate keeps the two equal. +pub(crate) use weaveffi_model::model::ABI_VERSION; + +/// The width generated prose in the header wraps at. +const WRAP: usize = 76; /// Join lowered ABI slots into a `" , ..."` declaration string. /// /// Parameter names are the one position in a header where an IDL-chosen /// identifier lands verbatim (every other name carries the symbol prefix), so -/// each is escaped with [`c_param_name`] before it's printed. -pub fn params_str(params: &[AbiParam], prefix: &str) -> String { +/// each is escaped with [`c_param_name`] before it's printed. An empty list +/// renders as `void`. +pub(crate) fn params_str(params: &[AbiParam], prefix: &str) -> String { + if params.is_empty() { + return "void".to_string(); + } params .iter() .map(|p| format!("{} {}", p.ty.render_c(prefix), c_param_name(&p.name))) @@ -40,16 +49,15 @@ pub fn params_str(params: &[AbiParam], prefix: &str) -> String { .join(", ") } -/// The spelling of an IDL parameter name inside a C prototype. +/// The spelling of an IDL parameter (or field) name inside C source. /// -/// The header is consumed from C and, through `#ifdef __cplusplus` guards or -/// the C++ generator's inlined `extern "C"` block, from C++. A name reserved -/// in either language (`register`, `class`, `new`, ...) gains the shared -/// trailing-underscore escape so the same declaration compiles in both. -/// Derived slot names (`{name}_ptr`, `out_len`) never collide and pass -/// through unchanged. +/// The header is consumed from C and, through its `extern "C"` guards, from +/// C++. A name reserved in either language (`register`, `class`, `new`, ...) +/// gains a trailing underscore so the same declaration compiles in both. +/// Derived slot names (`{name}_ptr`, `has_{name}`, `out_len`) never collide +/// and pass through unchanged. #[must_use] -pub fn c_param_name(name: &str) -> String { +pub(crate) fn c_param_name(name: &str) -> String { if is_reserved(name, C_KEYWORDS) || is_reserved(name, CPP_KEYWORDS) { format!("{name}_") } else { @@ -57,50 +65,55 @@ pub fn c_param_name(name: &str) -> String { } } -/// The export-visibility macro name for `prefix`, for example `WEAVEFFI_API`. -/// -/// Every exported function prototype is tagged with this macro so a non-Rust -/// producer that implements the header can export the symbols under hidden -/// default visibility, and Windows consumers import them through `dllimport`. -/// See [`render_visibility_macros`] for the macro's definition. +/// The export-visibility macro, `{PREFIX}_API` (see +/// [`render_visibility_macros`]). fn export_macro(prefix: &str) -> String { format!("{}_API", prefix.to_uppercase()) } -/// The deprecation macro name for `prefix`, for example `WEAVEFFI_DEPRECATED`. -/// -/// Used in place of a bare `__attribute__((deprecated))` so the marker also -/// compiles under MSVC (which spells it `__declspec(deprecated(...))`). +/// The deprecation macro, `{PREFIX}_DEPRECATED`, used in place of a bare +/// `__attribute__((deprecated))` so the marker also compiles under MSVC. fn deprecated_macro(prefix: &str) -> String { format!("{}_DEPRECATED", prefix.to_uppercase()) } -/// Render the portable export-visibility and deprecation macros that the C ABI +/// `s` as the contents of a C string literal. +fn c_string(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + c => out.push(c), + } + } + out +} + +/// Word-wrap generated prose at [`WRAP`] columns. +fn wrap(text: &str) -> String { + common::wrap(text, WRAP) +} + +/// Render the portable export-visibility and deprecation macros the C ABI /// declarations are tagged with. /// -/// The C ABI header is both consumed (callers link the prebuilt library) and, -/// for non-Rust producers, implemented directly (C, C++, or Zig supply the -/// symbols). A bare prototype exports nothing under hidden default visibility -/// (`-fvisibility=hidden`, the norm for release builds and the MSVC default), -/// so an implementing library compiled that way ships no usable symbols. These -/// macros fix that portably: +/// The header is both consumed (callers link the prebuilt library) and, for +/// non-Rust producers, implemented directly. A bare prototype exports +/// nothing under hidden default visibility (`-fvisibility=hidden`, the norm +/// for release builds and the MSVC default), so these macros fix that +/// portably: /// /// - `{PREFIX}_API` expands to `__declspec(dllexport)` when the producer /// defines `{PREFIX}_BUILD`, `__declspec(dllimport)` otherwise on Windows, -/// `__attribute__((used, visibility("default")))` under Emscripten, /// `__attribute__((visibility("default")))` on GCC and Clang, and nothing -/// elsewhere. The Emscripten spelling matches `EMSCRIPTEN_KEEPALIVE`: the -/// `used` attribute keeps every tagged symbol alive through Emscripten's -/// aggressive dead-code elimination, so the exports survive without the -/// producer enumerating them in `-sEXPORTED_FUNCTIONS`. +/// elsewhere. /// - `{PREFIX}_DEPRECATED(msg)` expands to the compiler's deprecation marker. /// -/// Both definitions are wrapped in `#ifndef` guards so a translation unit that -/// includes both the C header and the C++ header (which inlines the same -/// declarations) defines each macro only once. The names are derived from the -/// configured symbol prefix so two WeaveFFI libraries included together never -/// collide. -pub fn render_visibility_macros(out: &mut String, prefix: &str) { +/// Both are `#ifndef`-guarded and named from the symbol prefix, so two +/// WeaveFFI libraries included together never collide. +pub(crate) fn render_visibility_macros(out: &mut String, prefix: &str) { let body = r#"#ifndef @U@_API # if defined(_WIN32) || defined(__CYGWIN__) # ifdef @U@_BUILD @@ -108,8 +121,6 @@ pub fn render_visibility_macros(out: &mut String, prefix: &str) { # else # define @U@_API __declspec(dllimport) # endif -# elif defined(__EMSCRIPTEN__) -# define @U@_API __attribute__((used, visibility("default"))) # elif defined(__GNUC__) && (__GNUC__ >= 4) # define @U@_API __attribute__((visibility("default"))) # else @@ -131,10 +142,9 @@ pub fn render_visibility_macros(out: &mut String, prefix: &str) { out.push_str(&body.replace("@U@", &prefix.to_uppercase())); } -/// Render a full `{API} {ret} {symbol}({params});` declaration for a lowered -/// symbol, tagged with the export-visibility macro (see -/// [`render_visibility_macros`]). -pub fn fn_decl(out: &mut String, f: &AbiFn, prefix: &str) { +/// Render a `{API} {ret} {symbol}({params});` declaration for a lowered +/// symbol, tagged with the export-visibility macro. +pub(crate) fn fn_decl(out: &mut String, f: &AbiFn, prefix: &str) { let _ = writeln!( out, "{} {} {}({});", @@ -145,456 +155,503 @@ pub fn fn_decl(out: &mut String, f: &AbiFn, prefix: &str) { ); } -/// Render the runtime surface every producer exports (see the C ABI -/// contract): the ABI revision check, the error struct and its helpers, the -/// allocation functions, cancel tokens, and the debug leak counter, followed -/// by each top-level module's contract table (see [`render_contract`]). -pub fn render_runtime_decls(out: &mut String, model: &Model) { - let prefix = model.prefix(); - let api = export_macro(prefix); - let upper = prefix.to_uppercase(); - let _ = write!( - out, - "/* The C ABI revision this header was generated against. The producer\n \ - exports {prefix}_abi_version() so a consumer can refuse to load a\n \ - library built for a different revision. */\n\ - #define {upper}_ABI_VERSION {ABI_VERSION}u\n\ - {api} uint32_t {prefix}_abi_version(void);\n\n\ - /* Error slot written by every fallible call. `message` is NUL-terminated\n \ - UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields\n \ - serialized in the value buffer format (null when the code declares no\n \ - fields). Both are released by {prefix}_error_clear. Positive codes are\n \ - the API's declared error codes; negative codes are runtime traps:\n \ - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback\n \ - interface implementation failed, -5 cancelled. */\n\ - typedef struct {prefix}_error {{\n \ - int32_t code;\n \ - const char* message;\n \ - const uint8_t* payload_ptr;\n \ - size_t payload_len;\n\ - }} {prefix}_error;\n\n\ - /* Fill `err` with `code` and a producer-owned copy of `message`. Callback\n \ - interface implementations call this to report a failure without\n \ - allocating with a foreign allocator. */\n\ - {api} void {prefix}_error_set({prefix}_error* err, int32_t code, const char* message);\n\ - /* Replace `err`'s payload with a producer-owned copy of `len` bytes: the\n \ - fields of a declared error code, which a callback method that throws\n \ - attaches after {prefix}_error_set. */\n\ - {api} void {prefix}_error_set_payload({prefix}_error* err, const uint8_t* ptr, size_t len);\n\ - {api} void {prefix}_error_clear({prefix}_error* err);\n\n\ - /* Async completions receive a heap-boxed error the consumer owns;\n \ - {prefix}_error_free releases the message, the payload, and the box.\n \ - NULL is a no-op. */\n\ - {api} void {prefix}_error_free({prefix}_error* err);\n\n\ - /* Allocate a zero-filled run of `len` bytes (NULL for 0). A callback\n \ - method returns a string, bytes, or buffer as such a run, which the\n \ - producer adopts; any other run is released with {prefix}_free_bytes. */\n\ - {api} uint8_t* {prefix}_alloc(size_t len);\n\ - /* Release a string, bytes, or value buffer the producer returned, or a\n \ - run from {prefix}_alloc, with its exact length. NULL or 0 is a no-op. */\n\ - {api} void {prefix}_free_bytes(uint8_t* ptr, size_t len);\n\n\ - /* Cancellation for `cancellable` async functions. A token starts with\n \ - one reference owned by the consumer; the producer takes its own for\n \ - each call it is passed to, so the consumer may cancel and destroy its\n \ - reference at any time. A cancelled call completes with code -5. */\n\ - typedef struct {prefix}_cancel_token {prefix}_cancel_token;\n\ - {api} {prefix}_cancel_token* {prefix}_cancel_token_create(void);\n\ - {api} void {prefix}_cancel_token_cancel({prefix}_cancel_token* token);\n\ - {api} bool {prefix}_cancel_token_is_cancelled(const {prefix}_cancel_token* token);\n\ - {api} void {prefix}_cancel_token_destroy({prefix}_cancel_token* token);\n\n\ - /* Live-allocation counters for leak checks in tests: 0 objects,\n \ - 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations.\n \ - Kind -1 is 1 when the producer counts at all (its `leak-check`\n \ - feature) and 0 when every counter reads 0 regardless. */\n\ - {api} uint64_t {prefix}_debug_live(int32_t kind);\n\n\ - /* One declaration's fingerprint in a module's contract table: FNV-1a 64\n \ - of its dotted path and of its canonical signature. */\n\ - typedef struct {prefix}_contract_entry {{\n \ - uint64_t id;\n \ - uint64_t hash;\n\ - }} {prefix}_contract_entry;\n\n", - ); - for root in model.roots() { - render_contract(out, model, root); - } +/// Everything the declaration renderers share: the model, its symbol +/// prefix, and the identifier index that rewrites backticked API names in +/// docs to their C spelling. +pub(crate) struct Decls<'m> { + model: &'m Model, + prefix: &'m str, + names: ApiNames, } -/// Render one top-level module's contract: the producer's table function, -/// the entries these bindings were generated with as -/// `{PREFIX}_{MODULE}_CONTRACT` and `{PREFIX}_{MODULE}_CONTRACT_LEN`, and a -/// `static inline` checker returning the id of the first expected entry the -/// producer lacks or hashes differently (`0` when compatible). -pub fn render_contract(out: &mut String, model: &Model, root: &ModuleBinding) { - let prefix = model.prefix(); - let api = export_macro(prefix); - let symbol = contract_symbol(prefix, &root.name); - let check = contract_check_symbol(prefix, &root.name); - let upper = symbol.to_uppercase(); - let entries = weaveffi_model::contract::entries(model, root); - let _ = writeln!( - out, - "/* The contract of module `{}`: one entry per declaration, sorted by\n \ - id. The producer's table comes from the function below; a consumer\n \ - checks each entry it was generated with against it once, before any\n \ - other call (producer entries it doesn't know are fine). */\n\ - {api} const {prefix}_contract_entry* {symbol}(size_t* out_len);\n\ - #define {upper}_LEN {}\n\ - #define {upper} {{ \\", - root.name, - entries.len() - ); - if entries.is_empty() { - out.push_str(" {0ull, 0ull} \\\n"); +impl<'m> Decls<'m> { + /// A renderer for `model`'s declarations. + pub(crate) fn new(model: &'m Model) -> Self { + Self { + model, + prefix: model.prefix(), + names: ApiNames::new(model), + } } - for e in &entries { - let _ = writeln!( + + /// The C spelling of a backticked identifier in a doc: the symbol, tag, + /// or constant it names (when it names exactly one declaration). + fn spell(&self, ident: &str) -> Option { + self.names.c_name(ident).map(str::to_string) + } + + /// A declaration's doc with its deprecation note, in C spelling. + fn doc_with_deprecation( + &self, + doc: &Option, + deprecated: &Option, + ) -> Option { + Doc::new(doc, deprecated).with_deprecation(|i| self.spell(i)) + } + + /// Render the runtime surface every producer exports (the ABI revision + /// check, the error struct and its helpers, allocation, cancel tokens, + /// the vtable flags, and the debug leak counter), followed by each + /// top-level module's contract table. + pub(crate) fn runtime(&self, out: &mut String) { + let p = self.prefix; + let api = export_macro(p); + let upper = p.to_uppercase(); + let _ = write!( out, - " {{0x{:016x}ull, 0x{:016x}ull}}, /* {} */ \\", - e.id, e.hash, e.path + "/* The C ABI revision this header was generated against. The producer\n \ + exports {p}_abi_version() so a consumer can refuse to load a\n \ + library built for a different revision. */\n\ + #define {upper}_ABI_VERSION {ABI_VERSION}u\n\ + {api} uint32_t {p}_abi_version(void);\n\n\ + /* Error slot written by every fallible call. `code` is 0 on success,\n \ + positive for an error domain's code, and negative for a runtime\n \ + failure: -1 an untyped error (`throws: any`), -2 a producer panic, -3 a\n \ + marshalling failure, -4 a callback interface implementation failed,\n \ + -5 cancelled. The message is `message_len` bytes of UTF-8 at\n \ + `message_ptr`, NOT NUL-terminated. `payload_ptr`/`payload_len` hold\n \ + the code's fields in the value buffer format (NULL when it declares\n \ + none). The producer owns both runs; {p}_error_clear releases them. */\n\ + typedef struct {p}_error {{\n \ + int32_t code;\n \ + const uint8_t* message_ptr;\n \ + size_t message_len;\n \ + const uint8_t* payload_ptr;\n \ + size_t payload_len;\n\ + }} {p}_error;\n\n\ + /* Fill `err` with `code` and a producer-owned copy of the `message_len`\n \ + bytes at `message_ptr` (UTF-8), replacing what it held. Callback\n \ + implementations report failures this way, never by allocating the\n \ + message themselves. */\n\ + {api} void {p}_error_set({p}_error* err, int32_t code, const uint8_t* message_ptr, size_t message_len);\n\ + /* Replace `err`'s payload with a producer-owned copy of `len` bytes: the\n \ + fields of a domain code, which a callback method attaches after\n \ + {p}_error_set. */\n\ + {api} void {p}_error_set_payload({p}_error* err, const uint8_t* ptr, size_t len);\n\ + /* Release `err`'s message and payload and reset it to success. */\n\ + {api} void {p}_error_clear({p}_error* err);\n\n\ + /* Async completions receive a heap-boxed error the consumer owns;\n \ + {p}_error_free releases the message, the payload, and the box.\n \ + NULL is a no-op. */\n\ + {api} void {p}_error_free({p}_error* err);\n\n\ + /* Allocate a zero-filled, 8-aligned run of `len` bytes (NULL for 0). A\n \ + callback method returns a string, bytes, buffer, or typed array as\n \ + such a run, which the producer adopts. */\n\ + {api} uint8_t* {p}_alloc(size_t len);\n\ + /* Release a run the producer returned (a string, bytes, value buffer,\n \ + or typed array: `len` is its size in bytes, `count * sizeof(T)` for\n \ + a typed array) or one from {p}_alloc. NULL or 0 is a no-op. */\n\ + {api} void {p}_free_bytes(uint8_t* ptr, size_t len);\n\n\ + /* Cancellation for `cancellable` async functions. A token starts with\n \ + one reference owned by the consumer; the producer takes its own for\n \ + each call it is passed to, so the consumer may cancel and destroy its\n \ + reference at any time. A cancelled call completes with code -5. */\n\ + typedef struct {p}_cancel_token {p}_cancel_token;\n\ + {api} {p}_cancel_token* {p}_cancel_token_create(void);\n\ + {api} void {p}_cancel_token_cancel({p}_cancel_token* token);\n\ + {api} bool {p}_cancel_token_is_cancelled(const {p}_cancel_token* token);\n\ + {api} void {p}_cancel_token_destroy({p}_cancel_token* token);\n\n\ + /* A callback interface vtable's `flags` bit: the methods that return a\n \ + value (a non-void C return or any out slot) may only be called on\n \ + the thread that passed the vtable to the producer, which fails such\n \ + a call from any other thread with -4 without calling it. */\n\ + #define {upper}_VTABLE_THREAD_AFFINE 1u\n\n\ + /* Live-allocation counters for leak checks in tests: 0 objects,\n \ + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations.\n \ + Kind -1 is 1 when the producer counts at all (its `leak-check`\n \ + feature) and 0 when every counter reads 0 regardless. */\n\ + {api} uint64_t {p}_debug_live(int32_t kind);\n\n\ + /* One declaration's fingerprint in a module's contract table: FNV-1a 64\n \ + of its dotted path and of its canonical signature. */\n\ + typedef struct {p}_contract_entry {{\n \ + uint64_t id;\n \ + uint64_t hash;\n\ + }} {p}_contract_entry;\n\n", ); + for table in contract::tables(self.model) { + self.contract(out, &table); + } } - out.push_str("}\n"); - if entries.is_empty() { + + /// Render one top-level module's contract: the producer's table + /// function, the rows these bindings were generated with as + /// `{PREFIX}_{MODULE}_CONTRACT` and `{PREFIX}_{MODULE}_CONTRACT_LEN` + /// (each row commented with its path and canonical signature), and a + /// `static inline` checker returning the id of the first expected row + /// the producer lacks or hashes differently (`0` when compatible). + fn contract(&self, out: &mut String, table: &ContractTable<'_>) { + let p = self.prefix; + let api = export_macro(p); + let symbol = &table.symbol; + let check = &table.check_symbol; + let upper = symbol.to_uppercase(); + let _ = writeln!( + out, + "/* The contract of module `{}`: one entry per declaration, sorted by\n \ + id. The producer's table comes from the function below; a consumer\n \ + checks each entry it was generated with against it once, before any\n \ + other call (producer entries it doesn't know are fine). */\n\ + {api} const {p}_contract_entry* {symbol}(size_t* out_len);\n\ + #define {upper}_LEN {}\n\ + #define {upper} {{ \\", + table.root.name, + table.rows.len() + ); + if table.rows.is_empty() { + out.push_str(" {0ull, 0ull} \\\n"); + } + for row in &table.rows { + let _ = writeln!( + out, + " {{{}ull, {}ull}}, /* {}: {} */ \\", + contract::hex(row.id), + contract::hex(row.hash), + row.path, + row.signature.replace("*/", "* /") + ); + } + out.push_str("}\n"); + if table.rows.is_empty() { + let _ = write!( + out, + "/* Nothing to check: the module declares nothing. */\n\ + static inline uint64_t {check}(void) {{\n \ + return 0;\n\ + }}\n\n" + ); + return; + } let _ = write!( out, - "/* Nothing to check: the module declares nothing. */\n\ + "/* The id of the first expected entry the loaded library lacks or\n \ + declares differently, or 0 when these bindings match it. */\n\ static inline uint64_t {check}(void) {{\n \ + static const {p}_contract_entry expected[] = {upper};\n \ + size_t len = 0;\n \ + const {p}_contract_entry* table = {symbol}(&len);\n \ + for (size_t i = 0; i < {upper}_LEN; i++) {{\n \ + size_t lo = 0, hi = len;\n \ + while (lo < hi) {{\n \ + size_t mid = lo + (hi - lo) / 2;\n \ + if (table[mid].id < expected[i].id) lo = mid + 1; else hi = mid;\n \ + }}\n \ + if (lo == len || table[lo].id != expected[i].id || table[lo].hash != expected[i].hash) {{\n \ + return expected[i].id;\n \ + }}\n \ + }}\n \ return 0;\n\ }}\n\n" ); - return; } - let _ = write!( - out, - "/* The id of the first expected entry the loaded library lacks or\n \ - declares differently, or 0 when these bindings match it. */\n\ - static inline uint64_t {check}(void) {{\n \ - static const {prefix}_contract_entry expected[] = {upper};\n \ - size_t len = 0;\n \ - const {prefix}_contract_entry* table = {symbol}(&len);\n \ - for (size_t i = 0; i < {upper}_LEN; i++) {{\n \ - size_t lo = 0, hi = len;\n \ - while (lo < hi) {{\n \ - size_t mid = lo + (hi - lo) / 2;\n \ - if (table[mid].id < expected[i].id) lo = mid + 1; else hi = mid;\n \ - }}\n \ - if (lo == len || table[lo].id != expected[i].id || table[lo].hash != expected[i].hash) {{\n \ - return expected[i].id;\n \ - }}\n \ - }}\n \ - return 0;\n\ - }}\n\n" - ); -} -fn render_enum_constants(out: &mut String, e: &EnumBinding, type_name: &str) { - let mut w = CodeWriter::four_space(); - w.doc(&e.doc, DocCommentStyle::Javadoc); - if e.variants.iter().any(|v| v.doc.is_some()) { - w.block("typedef enum {", format!("}} {type_name};"), |w| { - let last = e.variants.len(); - for (i, v) in e.variants.iter().enumerate() { - w.doc(&v.doc, DocCommentStyle::Javadoc); - let comma = if i + 1 == last { "" } else { "," }; - w.line(format!("{} = {}{comma}", v.c_const, v.value)); - } - }); - } else { - let variants: Vec = e + /// Render an `int32_t` typedef named `type_name` and an anonymous enum of + /// its constants (`(doc, name, value)`), multi-line when any constant is + /// documented. Enum types are never `typedef enum`: a C enum's size is + /// the compiler's choice, and every enum crosses the ABI as `int32_t`. + fn int_enum( + &self, + out: &mut String, + doc: Option, + type_name: &str, + constants: &[(Option, &str, i32)], + ) { + let mut w = CodeWriter::four_space(); + w.doc(&doc, DocCommentStyle::Javadoc); + w.line(format!("typedef int32_t {type_name};")); + if constants.iter().any(|(doc, ..)| doc.is_some()) { + w.block("enum {", "};", |w| { + let last = constants.len(); + for (i, (doc, name, value)) in constants.iter().enumerate() { + w.doc(doc, DocCommentStyle::Javadoc); + let comma = if i + 1 == last { "" } else { "," }; + w.line(format!("{name} = {value}{comma}")); + } + }); + } else { + let constants: Vec = constants + .iter() + .map(|(_, name, value)| format!("{name} = {value}")) + .collect(); + w.line(format!("enum {{ {} }};", constants.join(", "))); + } + out.push_str(&w.finish()); + } + + /// A C-style enum, or a rich enum's tag type `{c_tag}_Tag` (a rich enum + /// value crosses in a value buffer whose first field is the tag). + fn enumeration(&self, out: &mut String, e: &EnumBinding) { + let doc = self.doc_with_deprecation(&e.doc, &e.deprecated); + let (type_name, doc) = if e.is_rich() { + let note = format!( + "The variant tag of `{}`, the first field of its value-buffer encoding.", + e.name + ); + let doc = match doc { + Some(doc) => format!("{doc}\n\n{note}"), + None => note, + }; + (format!("{}_Tag", e.c_tag), Some(doc)) + } else { + (e.c_tag.clone(), doc) + }; + let constants: Vec<(Option, &str, i32)> = e .variants .iter() - .map(|v| format!("{} = {}", v.c_const, v.value)) + .map(|v| { + let doc = Doc::new(&v.doc, &None).text(|i| self.spell(i)); + (doc, v.c_const.as_str(), v.value) + }) .collect(); - w.line(format!( - "typedef enum {{ {} }} {type_name};", - variants.join(", ") - )); + self.int_enum(out, doc, &type_name, &constants); } - out.push_str(&w.finish()); -} - -/// Render a C-style enum typedef. Multi-line when any variant is documented. -pub fn render_enum_decl(out: &mut String, e: &EnumBinding) { - render_enum_constants(out, e, &e.c_tag); -} -/// Render the *tag* enum of a rich (algebraic) enum, named `{c_tag}_Tag`. -/// A rich enum value crosses the ABI serialized in a value buffer whose first -/// field is an `int32_t` holding one of these discriminant constants. -fn render_rich_enum_tag_decl(out: &mut String, e: &EnumBinding) { - let tag_enum = format!("{}_Tag", e.c_tag); - render_enum_constants(out, e, &tag_enum); -} - -/// Render an error domain's code constants as a C `typedef enum` named by the -/// domain's `c_tag`. These are the values a throwing function stores in -/// `{prefix}_error.code`. -fn render_error_domain_decl(out: &mut String, e: &ErrorBinding, owner: &str) { - let mut w = CodeWriter::four_space(); - w.doc( - &Some(format!( - "Error codes reported by throwing functions in the `{owner}` module tree." - )), - DocCommentStyle::Javadoc, - ); - if e.codes.iter().any(|c| c.doc.is_some()) { - w.block("typedef enum {", format!("}} {};", e.c_tag), |w| { - let last = e.codes.len(); - for (i, c) in e.codes.iter().enumerate() { - w.doc(&c.doc, DocCommentStyle::Javadoc); - let comma = if i + 1 == last { "" } else { "," }; - w.line(format!("{} = {}{comma}", c.c_const, c.value)); - } - }); - } else { - let codes: Vec = e + /// An error domain's code type and constants: the values a function + /// declared `throws: {Domain}` stores in `{prefix}_error.code`. + fn error_domain(&self, out: &mut String, table: &ErrorTable<'_>) { + let d = table.domain; + let doc = wrap(&format!( + "Error codes of the `{}` domain of module `{}` (`{}` in the language \ + bindings), which a function declared `throws: {}` reports in \ + `{}_error.code`. Domains are open: a newer library may report a positive \ + code not listed here, which keeps its message.", + d.name, table.module.dot_path, table.type_name, d.name, self.prefix + )); + let constants: Vec<(Option, &str, i32)> = table .codes .iter() - .map(|c| format!("{} = {}", c.c_const, c.value)) + .map(|row| { + let doc = Doc::new(&row.code.doc, &None).text(|i| self.spell(i)); + (doc, row.code.c_const.as_str(), row.code.value) + }) .collect(); - w.line(format!( - "typedef enum {{ {} }} {};", - codes.join(", "), - e.c_tag - )); + self.int_enum(out, Some(doc), &d.c_tag, &constants); } - out.push_str(&w.finish()); -} -/// Phase 1a: enum and error-code definitions for one module. These reference -/// no other types, so they are emitted first across all modules. -pub fn render_module_enum_defs(out: &mut String, module: &ModuleBinding) { - for e in &module.enums { - if e.is_rich() { - render_rich_enum_tag_decl(out, e); - } else { - render_enum_decl(out, e); + /// Phase 1a: every enum, rich-enum tag, and error-domain code type. + /// These reference no other types, so they come first. + fn enum_defs(&self, out: &mut String) { + for m in &self.model.modules { + for e in &m.enums { + self.enumeration(out, e); + } + } + for table in errors::tables(self.model, "Error") { + self.error_domain(out, &table); } } - if let Some(err) = &module.errors { - render_error_domain_decl(out, err, &module.dot_path); + + /// Phase 1b: opaque interface and iterator types for one module. The C + /// ABI only ever uses pointers to these, so a forward typedef is enough + /// and lets declarations in any module reference them. Records and rich + /// enums declare no tags: they cross serialized. + fn type_tags(&self, out: &mut String, module: &ModuleBinding) { + for i in &module.interfaces { + let t = &i.c_tag; + let _ = writeln!(out, "typedef struct {t} {t};"); + } + for f in module.callables() { + if let Some(it) = f.iterator() { + let t = &it.iter_tag; + let _ = writeln!(out, "typedef struct {t} {t};"); + } + } } -} -/// Phase 1b: opaque interface/iterator forward typedefs for one module. -/// Pointers to these are all the C ABI ever uses, so a forward typedef is -/// sufficient and lets declarations in any module reference any type. Records -/// and rich enums declare no tags: they are value types crossing the ABI as -/// serialized buffers. -pub fn render_module_type_tags(out: &mut String, module: &ModuleBinding) { - for i in &module.interfaces { - let t = &i.c_tag; - let _ = writeln!(out, "typedef struct {t} {t};"); + /// What a callable's failure may carry, for its doc: a domain's codes + /// or an untyped -1 (runtime codes are possible everywhere). + fn call_failure(&self, error: &ErrorStrategy) -> Option { + match error { + ErrorStrategy::Trap => None, + ErrorStrategy::Domain(name) => Some(format!( + "Fails with a `{}` code (and its fields as a payload when it declares any).", + self.model.error_domain(name).c_tag, + )), + ErrorStrategy::Untyped => Some("Fails with code -1 and a message.".to_string()), + } } - for f in module.callables() { - if let CallShape::Iterator(it) = &f.shape { - let t = &it.iter_tag; - let _ = writeln!(out, "typedef struct {t} {t};"); + + /// What a callback method may report, for its doc. + fn method_failure(&self, m: &CallbackMethodBinding) -> Option { + match &m.error { + ErrorStrategy::Trap => None, + ErrorStrategy::Domain(name) => Some(wrap(&format!( + "May fail with a `{}` code (attaching its fields with \ + `{}_error_set_payload` when it declares any).", + self.model.error_domain(name).c_tag, + self.prefix + ))), + ErrorStrategy::Untyped => Some("May fail with code -1 and a message.".to_string()), } } -} -/// Render one callback interface's vtable struct: the fixed header (`size`, -/// `flags`, `free`), then one function pointer per method in declaration -/// order. -fn render_vtable_decl( - out: &mut String, - cb: &CallbackInterfaceBinding, - domain: Option<&ErrorBinding>, - prefix: &str, -) { - let mut w = CodeWriter::four_space(); - let contract = "Consumer-implemented callback interface. The consumer passes a context \ - pointer plus a pointer to a static instance of this vtable, with `size` \ - set to `sizeof` the vtable and `flags` to 0 (the producer rejects a smaller \ - vtable). The producer may call any entry from any thread until it calls \ - `free(ctx)` exactly once, also from any thread. A method reports failure \ - with `error_set(out_err, ...)`; a string, bytes, or buffer return is a \ - run the consumer allocates with `alloc` and writes to `out_ptr` and \ - `out_len`, which the producer adopts."; - w.doc( - &Some(match &cb.doc { + /// One callback interface's vtable struct: the fixed header (`size`, + /// `flags`, `free`), then one function pointer per method in + /// declaration order. + fn vtable(&self, out: &mut String, cb: &CallbackInterfaceBinding) { + let p = self.prefix; + let upper = p.to_uppercase(); + let mut w = CodeWriter::four_space(); + let contract = wrap(&format!( + "A consumer-implemented callback interface. The consumer passes a context \ + pointer and a pointer to a vtable that outlives it, with `size` set to \ + `sizeof` the vtable (the producer rejects a smaller one) and `flags` to 0 or \ + `{upper}_VTABLE_THREAD_AFFINE`. The producer may call any method from any \ + thread (with `{upper}_VTABLE_THREAD_AFFINE`, a method that returns a value \ + only from the thread that passed the vtable) until it calls `free(ctx)` \ + exactly once, also from any thread. A method reports failure with \ + `{p}_error_set(out_err, ...)`. It returns a string, bytes, buffer, or typed \ + array as a run it allocates with `{p}_alloc` and stores in `*out_ptr` and \ + `*out_len` (an element count for a typed array), which the producer adopts, \ + and an optional scalar as a `bool` C return (present) with the value in \ + `*out_value`." + )); + let doc = match self.doc_with_deprecation(&cb.doc, &cb.deprecated) { Some(doc) => format!("{doc}\n\n{contract}"), - None => contract.to_string(), - }), - DocCommentStyle::Javadoc, - ); - w.block( - format!("typedef struct {} {{", cb.vtable_tag), - format!("}} {};", cb.vtable_tag), - |w| { - w.line("uint32_t size;"); - w.line("uint32_t flags;"); - w.line("void (*free)(void* ctx);"); - for m in &cb.methods { - let throws = match (m.throws, domain) { - (true, Some(d)) => Some(format!( - "May fail with a `{}` code (its fields attached with \ - `{prefix}_error_set_payload`).", - d.c_tag - )), - _ => None, - }; - let doc = match (&m.doc, throws) { - (Some(doc), Some(t)) => Some(format!("{doc}\n\n{t}")), - (doc, t) => doc.clone().or(t), - }; - w.doc(&doc, DocCommentStyle::Javadoc); - w.line(format!( - "{} (*{})({});", - m.abi_ret.render_c(prefix), - c_param_name(&m.name), - params_str(&m.abi_params, prefix) - )); - } - }, - ); - out.push_str(&w.finish()); -} + None => contract, + }; + w.doc(&Some(doc), DocCommentStyle::Javadoc); + w.block( + format!("typedef struct {} {{", cb.vtable_tag), + format!("}} {};", cb.vtable_tag), + |w| { + w.line("uint32_t size;"); + w.line("uint32_t flags;"); + w.line("void (*free)(void* ctx);"); + for m in &cb.methods { + let doc = self.doc_with_deprecation(&m.doc, &m.deprecated); + let doc = match (doc, self.method_failure(m)) { + (Some(doc), Some(t)) => Some(format!("{doc}\n\n{t}")), + (doc, t) => doc.or(t), + }; + w.doc(&doc, DocCommentStyle::Javadoc); + w.line(format!( + "{} (*{})({});", + m.abi.ret.render_c(p), + c_param_name(&m.abi.symbol), + params_str(&m.abi.params, p) + )); + } + }, + ); + out.push_str(&w.finish()); + } -/// Phase 1c: callback-interface vtables and async completion-callback -/// function-pointer typedefs for one module. These may reference enums (by -/// value) and interfaces (by pointer), so they are emitted after every -/// module's enums and type tags. -pub fn render_module_callback_types( - out: &mut String, - module: &ModuleBinding, - modules: &[ModuleBinding], - prefix: &str, -) { - // The domain in scope: the module's own, else the nearest ancestor's. - let domain = (1..=module.segments.len()).rev().find_map(|n| { - modules - .iter() - .find(|m| m.segments[..] == module.segments[..n]) - .and_then(|m| m.errors.as_ref()) - }); - for cb in &module.callback_interfaces { - render_vtable_decl(out, cb, domain, prefix); + /// Phase 1c: callback-interface vtables and async completion-callback + /// typedefs for one module. These may reference enums (by value) and + /// interfaces (by pointer), so they follow every module's enums and + /// type tags. + fn callback_types(&self, out: &mut String, module: &ModuleBinding) { + for cb in &module.callback_interfaces { + self.vtable(out, cb); + } + for f in module.callables() { + if let CallShape::Async(a) = &f.shape { + let _ = writeln!( + out, + "typedef void (*{})({});", + a.callback_type, + params_str(&a.callback_params, self.prefix) + ); + } + } } - for f in module.callables() { - if let CallShape::Async(a) = &f.shape { + + /// One callable's prototypes with its doc and deprecation marker: the + /// sync entry point, the async launcher, or the iterator launcher with + /// its `_next` and `_destroy`. + fn callable(&self, out: &mut String, f: &FnBinding) { + let p = self.prefix; + let doc = Doc::new(&f.doc, &f.deprecated); + let text = match (doc.text(|i| self.spell(i)), self.call_failure(&f.error)) { + (Some(text), Some(failure)) => Some(format!("{text}\n\n{}", wrap(&failure))), + (text, failure) => text.or(failure.map(|f| wrap(&f))), + }; + let mut w = CodeWriter::four_space(); + w.doc(&text, DocCommentStyle::Javadoc); + out.push_str(&w.finish()); + if let Some(msg) = doc.deprecation(|i| self.spell(i)) { + let _ = writeln!(out, "{}(\"{}\")", deprecated_macro(p), c_string(&msg)); + } + fn_decl(out, &f.abi, p); + if let Some(it) = f.iterator() { + fn_decl(out, &it.next, p); let _ = writeln!( out, - "typedef void (*{})({});", - a.callback_type, - params_str(&a.callback_params, prefix) + "{} void {}({}* iter);", + export_macro(p), + it.destroy_symbol, + it.iter_tag ); } } -} -/// Render one callable's prototypes according to its call shape (sync, async -/// launcher, or iterator launch/next/destroy triple), with doc comment and -/// deprecation marker. -fn render_callable_decl(out: &mut String, f: &FnBinding, prefix: &str) { - let api = export_macro(prefix); - let deprecated = deprecated_macro(prefix); - emit_doc(out, &f.doc, "", DocCommentStyle::Javadoc); - if let Some(msg) = &f.deprecated { - let _ = writeln!(out, "{deprecated}(\"{}\")", msg.replace('"', "\\\"")); + /// One interface's functions: constructors, statics, methods, then the + /// reference-count pair. The opaque tag is already declared (phase 1b). + fn interface_fns(&self, out: &mut String, i: &InterfaceBinding) { + let p = self.prefix; + let api = export_macro(p); + let tag = &i.c_tag; + let mut w = CodeWriter::four_space(); + w.doc( + &self.doc_with_deprecation(&i.doc, &i.deprecated), + DocCommentStyle::Javadoc, + ); + out.push_str(&w.finish()); + for f in i.constructors.iter().chain(&i.statics).chain(&i.methods) { + self.callable(out, f); + } + let _ = writeln!( + out, + "/** Returns a new strong reference to the same object (the pointer value is \ + unchanged). Null is a no-op returning null. */\n\ + {api} {tag}* {}(const {tag}* self);\n\ + /** Releases one strong reference; the object is dropped when the last reference \ + is released. Null is a no-op. */\n\ + {api} void {}({tag}* self);\n", + i.clone_symbol, i.destroy_symbol + ); } - match &f.shape { - CallShape::Iterator(it) => { - let t = &it.iter_tag; - fn_decl(out, &it.launch, prefix); - fn_decl(out, &it.next, prefix); - let _ = writeln!(out, "{api} void {}({t}* iter);", it.destroy_symbol); + + /// Render every declaration in dependency-safe order: all enum and + /// error-code types, then all opaque type tags, then all vtables and + /// completion typedefs, then each module's functions under a + /// `// Module:` comment. Emitting every type before any function lets a + /// parent module's function reference a child module's interface. + pub(crate) fn declarations(&self, out: &mut String) { + self.enum_defs(out); + for m in &self.model.modules { + self.type_tags(out, m); } - CallShape::Async(a) => { - fn_decl(out, &a.launch, prefix); + for m in &self.model.modules { + self.callback_types(out, m); } - CallShape::Sync(abi) => { - fn_decl(out, abi, prefix); + out.push('\n'); + for m in &self.model.modules { + let _ = writeln!(out, "// Module: {}", m.path); + for i in &m.interfaces { + self.interface_fns(out, i); + } + for f in &m.functions { + self.callable(out, f); + } + out.push('\n'); } } } -/// Render the function surface of one interface: constructors, statics, -/// methods, then the reference-count pair. Assumes the opaque tag is already -/// forward-declared (phase 1b). -fn render_interface_fn_decls(out: &mut String, i: &InterfaceBinding, prefix: &str) { - let api = export_macro(prefix); - let tag = &i.c_tag; - emit_doc(out, &i.doc, "", DocCommentStyle::Javadoc); - for c in &i.constructors { - render_callable_decl(out, c, prefix); - } - for s in &i.statics { - render_callable_decl(out, s, prefix); - } - for m in &i.methods { - render_callable_decl(out, m, prefix); - } - emit_doc( - out, - &Some( - "Returns a new strong reference to the same object (the pointer value is \ - unchanged). Null is a no-op returning null." - .to_string(), - ), - "", - DocCommentStyle::Javadoc, - ); - let _ = writeln!(out, "{api} {tag}* {}(const {tag}* self);", i.clone_symbol); - emit_doc( - out, - &Some( - "Releases one strong reference; the object is dropped when the last reference \ - is released. Null is a no-op." - .to_string(), - ), - "", - DocCommentStyle::Javadoc, - ); - let _ = writeln!(out, "{api} void {}({tag}* self);", i.destroy_symbol); - out.push('\n'); -} +#[cfg(test)] +mod tests { + use super::*; -/// Phase 2: every function prototype for one module: interface members, then -/// sync/async/iterator functions. All type tags, vtables, and callback -/// typedefs are assumed already emitted (phases 1a-1c). Caller controls the -/// leading `// Module:` comment and any framing. -pub fn render_module_fn_decls(out: &mut String, module: &ModuleBinding, prefix: &str) { - for i in &module.interfaces { - render_interface_fn_decls(out, i, prefix); - } - for f in &module.functions { - render_callable_decl(out, f, prefix); + #[test] + fn reserved_parameter_names_are_escaped() { + assert_eq!(c_param_name("class"), "class_"); + assert_eq!(c_param_name("register"), "register_"); + assert_eq!(c_param_name("count"), "count"); } -} -/// Render the complete C ABI declaration surface for `modules` in -/// dependency-safe order: all enum definitions, then all opaque type tags, then -/// all vtables and callback typedefs, then per-module function prototypes. -/// Emitting every type tag before any function lets a parent module's function -/// reference a child module's interface: cross-module forward references a -/// per-module interleaving could not express. -/// -/// The runtime decls (`error`, `free_*`, cancel token) are *not* emitted here; -/// callers render those first (the C generator inserts its map convention -/// comment in between). -pub fn render_decls( - out: &mut String, - modules: &[ModuleBinding], - prefix: &str, - module_comments: bool, -) { - for m in modules { - render_module_enum_defs(out, m); - } - for m in modules { - render_module_type_tags(out, m); - } - for m in modules { - render_module_callback_types(out, m, modules, prefix); - } - out.push('\n'); - for m in modules { - if module_comments { - let _ = writeln!(out, "// Module: {}", m.path); - } - render_module_fn_decls(out, m, prefix); - out.push('\n'); + #[test] + fn string_literals_escape_quotes() { + assert_eq!(c_string(r#"say "hi" \ now"#), r#"say \"hi\" \\ now"#); } } diff --git a/crates/weaveffi-cli/src/cargo.rs b/crates/weaveffi-cli/src/cargo.rs index fa73a36e..4d9d5754 100644 --- a/crates/weaveffi-cli/src/cargo.rs +++ b/crates/weaveffi-cli/src/cargo.rs @@ -10,8 +10,8 @@ use std::process::{Command, Stdio}; -use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use serde::Deserialize; /// The facts about a Rust producer crate that building and packaging need, @@ -25,8 +25,6 @@ pub struct CargoCrate { /// The library target's name with `-` mapped to `_` (`my_kv`): the base /// name of the `lib{lib_name}.so` Cargo produces. pub lib_name: String, - /// The crate types declared for the library target (`cdylib`, `rlib`, …). - pub crate_types: Vec, /// The absolute path of the crate's `Cargo.toml`. pub manifest_path: Utf8PathBuf, /// The Cargo target directory (respecting `CARGO_TARGET_DIR`, @@ -63,7 +61,8 @@ impl CargoCrate { .arg("--manifest-path") .arg(manifest.as_str()) .output() - .context("failed to run `cargo metadata`; is Cargo installed and on PATH?")?; + .into_diagnostic() + .wrap_err("failed to run `cargo metadata`; is Cargo installed and on PATH?")?; if !output.status.success() { bail!( "`cargo metadata` failed for {manifest}:\n{}", @@ -71,7 +70,8 @@ impl CargoCrate { ); } let metadata: Metadata = serde_json::from_slice(&output.stdout) - .context("failed to parse `cargo metadata` output")?; + .into_diagnostic() + .wrap_err("failed to parse `cargo metadata` output")?; let wanted = canonical(manifest); let Some(package) = metadata .packages @@ -92,7 +92,6 @@ impl CargoCrate { }; Ok(Self { lib_name: lib.name.replace('-', "_"), - crate_types: lib.crate_types.clone(), name: package.name, version: package.version, manifest_path: package.manifest_path, @@ -118,24 +117,17 @@ impl CargoCrate { self.manifest_path.parent().unwrap_or(Utf8Path::new(".")) } - /// Build the crate's library for the host with - /// `cargo build --lib --message-format=json-render-diagnostics` (in - /// release mode with `release`), forwarding Cargo's diagnostics to - /// stderr, and return the library it produced: the `cdylib` if the crate - /// declares one, else the `staticlib`. - /// - /// # Errors - /// - /// Returns an error when Cargo can't be run or fails, or when the crate - /// produces neither a `cdylib` nor a `staticlib`. - pub fn build_library(&self, release: bool, quiet: bool) -> Result { + /// Build the crate's library for the host as a `cdylib` with `cargo + /// rustc --lib --crate-type cdylib --profile {profile}` (so the crate + /// needs no particular `crate-type`), forwarding Cargo's diagnostics to + /// stderr, and return the shared library it produced. + pub(crate) fn build_library(&self, profile: &str, quiet: bool) -> Result { let mut cmd = Command::new(cargo_bin()); - cmd.args(["build", "--lib", "--message-format=json-render-diagnostics"]) + cmd.args(["rustc", "--lib", "--crate-type", "cdylib"]) + .args(["--profile", profile]) + .arg("--message-format=json-render-diagnostics") .arg("--manifest-path") .arg(self.manifest_path.as_str()); - if release { - cmd.arg("--release"); - } if quiet { cmd.arg("--quiet"); } @@ -143,7 +135,8 @@ impl CargoCrate { .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .output() - .context("failed to run `cargo build`; is Cargo installed and on PATH?")?; + .into_diagnostic() + .wrap_err("failed to run `cargo rustc`; is Cargo installed and on PATH?")?; if !output.status.success() { bail!( "cargo failed to build `{}`; see its errors above", @@ -151,33 +144,30 @@ impl CargoCrate { ); } let files = self.lib_artifacts(&output.stdout); - let is = |suffixes: &[&str]| { - files - .iter() - .find(|f| suffixes.iter().any(|s| f.as_str().ends_with(s))) - .cloned() - }; - let library = is(&[".dylib", ".so", ".dll", ".wasm"]).or_else(|| { - files - .iter() - .find(|f| { - f.as_str().ends_with(".a") - || (f.as_str().ends_with(".lib") && !f.as_str().ends_with(".dll.lib")) - }) - .cloned() - }); - library.ok_or_else(|| { - anyhow::anyhow!( - "`{}` builds no cdylib or staticlib to read its API from; add `crate-type = \ - [\"cdylib\"]` under [lib] in {}", - self.name, - self.manifest_path - ) - }) + files + .iter() + .find(|f| { + [".dylib", ".so", ".dll"] + .iter() + .any(|s| f.as_str().ends_with(s)) + }) + .cloned() + .ok_or_else(|| { + miette::miette!("cargo built `{}` but reported no shared library", self.name) + }) } /// The files Cargo reported for this crate's library target, from the /// JSON messages (`--message-format=json`) it printed to stdout. + /// + /// Each is the file rustc wrote into the profile's `deps` directory + /// rather than the copy Cargo reports, which it uplifts into the profile + /// directory: Cargo deletes and re-creates that copy on every run, even + /// one with nothing to rebuild, so another `cargo` run on the same + /// target directory (a concurrent `weaveffi` command, an editor) can + /// remove it while this process reads it. The `deps` file changes only + /// when the crate is rebuilt. A file with no `deps` original (a name + /// Cargo decorated with a hash) is returned as reported. #[must_use] pub fn lib_artifacts(&self, stdout: &[u8]) -> Vec { let manifest = canonical(&self.manifest_path); @@ -203,7 +193,7 @@ impl CargoCrate { files .iter() .filter_map(|f| f.as_str()) - .map(Utf8PathBuf::from), + .map(|f| unuplifted(Utf8Path::new(f))), ); } } @@ -211,24 +201,24 @@ impl CargoCrate { } } -/// The nearest `Cargo.toml` at or above `start` (a file or a directory), -/// or `None` when there is none up to the filesystem root. -#[must_use] -pub fn find_manifest(start: &Utf8Path) -> Option { - let start = std::path::absolute(start.as_std_path()).ok()?; - let start = Utf8PathBuf::from_path_buf(start).ok()?; - start - .ancestors() - .map(|dir| dir.join("Cargo.toml")) - .find(|candidate| candidate.is_file()) -} - /// The `cargo` executable: `$CARGO` when running under Cargo, else `cargo`. #[must_use] pub fn cargo_bin() -> String { std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()) } +/// The original in `deps/` of a file Cargo uplifted to `path`, if there is +/// one (see [`CargoCrate::lib_artifacts`]), else `path`. +fn unuplifted(path: &Utf8Path) -> Utf8PathBuf { + if let (Some(dir), Some(name)) = (path.parent(), path.file_name()) { + let original = dir.join("deps").join(name); + if original.is_file() { + return original; + } + } + path.to_path_buf() +} + /// `path` with symlinks and `..` resolved where possible, for comparing /// manifest paths. fn canonical(path: &Utf8Path) -> Utf8PathBuf { @@ -260,8 +250,6 @@ struct Package { struct Target { name: String, kind: Vec, - #[serde(default)] - crate_types: Vec, } impl Target { @@ -289,7 +277,6 @@ mod tests { let krate = CargoCrate::resolve(&manifest).unwrap(); assert_eq!(krate.name, "calculator"); assert_eq!(krate.lib_name, "calculator"); - assert!(krate.crate_types.iter().any(|t| t == "cdylib")); assert!(krate.target_dir.ends_with("target"), "{}", krate.target_dir); assert_eq!(krate.weaveffi_dir(), krate.target_dir.join("weaveffi")); } @@ -307,10 +294,4 @@ mod tests { let err = CargoCrate::resolve(Utf8Path::new("/nonexistent/Cargo.toml")).unwrap_err(); assert!(err.to_string().contains("no Cargo.toml"), "{err}"); } - - #[test] - fn manifests_are_found_from_nested_paths() { - let found = find_manifest(&repo_root().join("samples/calculator/src/lib.rs")).unwrap(); - assert!(found.ends_with("samples/calculator/Cargo.toml"), "{found}"); - } } diff --git a/crates/weaveffi-cli/src/codegen/codecs.rs b/crates/weaveffi-cli/src/codegen/codecs.rs new file mode 100644 index 00000000..6d0b893d --- /dev/null +++ b/crates/weaveffi-cli/src/codegen/codecs.rs @@ -0,0 +1,176 @@ +//! The value-buffer **composites** an API uses and their one canonical +//! **stem**. +//! +//! Records, rich enums, and error payloads get one codec each, named after +//! the type. Optionals, lists, and maps that cross inside a value buffer +//! (record fields, variant fields, error payload fields, and every +//! [`Buffer`](weaveffi_model::ty::Family::Buffer)-family value at a call +//! boundary) are the *composites*: each distinct shape gets one codec too, +//! and every target names it with [`stem`] so the shapes line up across +//! languages (`list_i32`, `opt_Item`, `map_string_list_i64`). +//! +//! An optional scalar (OptDirect) or numeric list (Slice) at a call boundary +//! isn't a composite: it crosses directly. The same type nested in a buffer +//! (a record's `i64?` field) is. +//! +//! # Example +//! +//! A target that writes one reader and writer function per composite: +//! +//! ```ignore +//! use crate::codegen::codecs; +//! +//! for ty in codecs::composites(model) { +//! // Dependency order: `list_i32` comes before `opt_list_i32`, so a +//! // language that needs definitions before use can emit in order. +//! let stem = codecs::stem(&ty); +//! w.line(format!("def _write_{stem}(w, v): ...")); +//! w.line(format!("def _read_{stem}(r): ...")); +//! } +//! ``` + +use std::collections::HashSet; + +use weaveffi_model::model::Model; +use weaveffi_model::ty::Ty; + +/// Every distinct optional, list, and map type that crosses inside a value +/// buffer anywhere in `model`, innermost first (a shape always comes after +/// the shapes it contains), each once. +/// +/// The order is deterministic: record fields, rich-enum variant fields, and +/// error payload fields in declaration order, then every buffered value at a +/// call boundary (parameters, returns, async results, iterator elements, +/// and callback method parameters and returns), module by module. +#[must_use] +pub(crate) fn composites(model: &Model) -> Vec { + let mut out = Vec::new(); + let mut seen = HashSet::new(); + for m in &model.modules { + let fields = m + .structs + .iter() + .flat_map(|s| &s.fields) + .chain( + m.enums + .iter() + .flat_map(|e| &e.variants) + .flat_map(|v| &v.fields), + ) + .chain( + m.errors + .iter() + .flat_map(|e| &e.codes) + .flat_map(|c| &c.fields), + ); + for f in fields { + collect(&f.ty, &mut out, &mut seen); + } + } + let mut boundary = |ty: &Ty| { + if ty.is_buffered() { + collect(ty, &mut out, &mut seen); + } + }; + for m in &model.modules { + for f in m.callables() { + f.params + .iter() + .filter_map(|p| p.ty.value()) + .for_each(&mut boundary); + if let Some(ret) = &f.ret { + boundary(ret.elem()); + } + } + for cb in &m.callback_interfaces { + for meth in &cb.methods { + meth.params.iter().for_each(|p| boundary(&p.ty)); + meth.ret.iter().for_each(&mut boundary); + } + } + } + out +} + +/// Record every optional, list, and map shape inside `ty`, innermost first. +fn collect(ty: &Ty, out: &mut Vec, seen: &mut HashSet) { + match ty { + Ty::Optional(inner) | Ty::List(inner) => collect(inner, out, seen), + Ty::Map(k, v) => { + collect(k, out, seen); + collect(v, out, seen); + } + _ => return, + } + if seen.insert(ty.clone()) { + out.push(ty.clone()); + } +} + +/// The canonical identifier fragment naming `ty` in codec names: a +/// primitive's IDL spelling (`i64`, `string`), a user type's name (`Item`; +/// type names are global, so no module path is needed), or a composite's +/// shape in prefix notation (`opt_i64`, `list_string`, `map_string_Item`, +/// `list_opt_i32`). +/// +/// Every target derives its composite codec names from this one function +/// (adding its own prefix or casing), so a stem never differs between two +/// languages. +#[must_use] +pub(crate) fn stem(ty: &Ty) -> String { + match ty { + Ty::Prim(p) => p.snake().to_string(), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => n.clone(), + Ty::Optional(inner) => format!("opt_{}", stem(inner)), + Ty::List(inner) => format!("list_{}", stem(inner)), + Ty::Map(k, v) => format!("map_{}_{}", stem(k), stem(v)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::test_model; + use weaveffi_model::ty::Prim; + + #[test] + fn stems_are_prefix_notation() { + let i64s = Ty::List(Box::new(Ty::Prim(Prim::I64))); + let map = Ty::Map(Box::new(Ty::Prim(Prim::String)), Box::new(i64s.clone())); + assert_eq!(stem(&map), "map_string_list_i64"); + let opt = Ty::Optional(Box::new(Ty::Record("Item".into()))); + assert_eq!(stem(&opt), "opt_Item"); + assert_eq!(stem(&Ty::List(Box::new(opt))), "list_opt_Item"); + } + + #[test] + fn composites_are_buffered_shapes_innermost_first() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + structs: + - name: R + fields: + - { name: a, type: "[i32]" } + - { name: b, type: "{string:[i32]?}" } + functions: + - { name: slice, params: [{ name: xs, type: "[i32]" }], return: "[i64]" } + - { name: opt, params: [{ name: x, type: "i32?" }], return: "string?" } + - { name: it, params: [], return: "iter<[R]>" } +"#, + ); + let stems: Vec = composites(&model).iter().map(stem).collect(); + assert_eq!( + stems, + [ + "list_i32", + "opt_list_i32", + "map_string_opt_list_i32", + "opt_string", + "list_R", + ] + ); + } +} diff --git a/crates/weaveffi-cli/src/codegen/common.rs b/crates/weaveffi-cli/src/codegen/common.rs index dc6ab93f..45477132 100644 --- a/crates/weaveffi-cli/src/codegen/common.rs +++ b/crates/weaveffi-cli/src/codegen/common.rs @@ -1,5 +1,5 @@ //! Shared codegen primitives that every language generator can reuse: the -//! doc-comment emitter and `PascalCase` conversion. +//! doc-comment emitter, prose wrapping, and `PascalCase` conversion. //! //! Specialised flavours that exist in only one generator (Go's //! godoc-style first-line symbol prefix, .NET's `` XML tags, @@ -18,9 +18,6 @@ pub enum DocCommentStyle { TripleSlash, /// `# ...` per line (Python `#` comments, Ruby). Hash, - /// `// ...` per line (Go base case; Go's symbol-prefixed - /// godoc convention stays generator-local). - DoubleSlash, /// `/** ... */` block; single-line collapses to `/** text */` /// (C, C++, Kotlin/KDoc, JSDoc, TypeScript .d.ts). Javadoc, @@ -42,7 +39,6 @@ pub fn emit_doc(out: &mut String, doc: &Option, indent: &str, style: Doc match style { DocCommentStyle::TripleSlash => emit_line_doc(out, doc, indent, "///"), DocCommentStyle::Hash => emit_line_doc(out, doc, indent, "#"), - DocCommentStyle::DoubleSlash => emit_line_doc(out, doc, indent, "//"), DocCommentStyle::Javadoc => emit_javadoc(out, doc, indent), } } @@ -63,6 +59,14 @@ fn emit_line_doc(out: &mut String, doc: &str, indent: &str, marker: &str) { } fn emit_javadoc(out: &mut String, doc: &str, indent: &str) { + // A `*/` in the text would end the comment early. + let escaped; + let doc = if doc.contains("*/") { + escaped = doc.replace("*/", "*\\/"); + escaped.as_str() + } else { + doc + }; if doc.contains('\n') { out.push_str(indent); out.push_str("/**\n"); @@ -86,6 +90,31 @@ fn emit_javadoc(out: &mut String, doc: &str, indent: &str) { } } +/// Word-wrap generated prose into lines of at most `width` columns (a +/// longer word gets a line of its own), keeping blank-line paragraph +/// breaks. For text the generator writes; IDL docs keep their own breaks. +#[must_use] +pub fn wrap(text: &str, width: usize) -> String { + let mut lines: Vec = Vec::new(); + for paragraph in text.split("\n\n") { + if !lines.is_empty() { + lines.push(String::new()); + } + let mut line = String::new(); + for word in paragraph.split_whitespace() { + if !line.is_empty() && line.len() + 1 + word.len() > width { + lines.push(std::mem::take(&mut line)); + } + if !line.is_empty() { + line.push(' '); + } + line.push_str(word); + } + lines.push(line); + } + lines.join("\n") +} + /// Convert a `snake_case` identifier to `PascalCase` by uppercasing the /// first character of each `_`-separated segment and preserving the rest. /// @@ -167,30 +196,6 @@ mod tests { assert_eq!(out, "# ruby/python style\n"); } - #[test] - fn emit_doc_double_slash_single_line() { - let mut out = String::new(); - emit_doc( - &mut out, - &Some("Go-style line comment.".into()), - "", - DocCommentStyle::DoubleSlash, - ); - assert_eq!(out, "// Go-style line comment.\n"); - } - - #[test] - fn emit_doc_double_slash_multi_line() { - let mut out = String::new(); - emit_doc( - &mut out, - &Some("first\n\nsecond".into()), - "\t", - DocCommentStyle::DoubleSlash, - ); - assert_eq!(out, "\t// first\n\t//\n\t// second\n"); - } - #[test] fn emit_doc_hash_multi_line() { let mut out = String::new(); @@ -227,6 +232,18 @@ mod tests { assert_eq!(out, " /**\n * line one\n *\n * line three\n */\n"); } + #[test] + fn emit_doc_javadoc_escapes_comment_terminators() { + let mut out = String::new(); + emit_doc( + &mut out, + &Some("a */ b".into()), + "", + DocCommentStyle::Javadoc, + ); + assert_eq!(out, "/** a *\\/ b */\n"); + } + #[test] fn emit_doc_trims_outer_whitespace_before_decisions() { // A doc that's "single line" after trimming should still @@ -243,6 +260,17 @@ mod tests { assert_eq!(out, "/** hello */\n"); } + // --- wrap --- + + #[test] + fn wrap_keeps_paragraphs_and_width() { + let text = format!("{}\n\nshort", "word ".repeat(40)); + let wrapped = wrap(&text, 30); + assert!(wrapped.lines().all(|l| l.len() <= 30), "{wrapped}"); + assert!(wrapped.ends_with("\n\nshort"), "{wrapped}"); + assert_eq!(wrap("a-very-long-word b", 4), "a-very-long-word\nb"); + } + // --- pascal_case --- #[test] diff --git a/crates/weaveffi-cli/src/codegen/contract.rs b/crates/weaveffi-cli/src/codegen/contract.rs new file mode 100644 index 00000000..43b730f9 --- /dev/null +++ b/crates/weaveffi-cli/src/codegen/contract.rs @@ -0,0 +1,146 @@ +//! The **contract rows** every consumer checks when it loads the library. +//! +//! A producer exports one contract table per top-level module, +//! `{prefix}_{module}_contract`, with one `(id, hash)` entry per +//! declaration: every function and interface member, interface, record, +//! enum, callback interface, callback method, error domain, and error code +//! (see [`weaveffi_model::contract`] for the canonical strings). Generated +//! bindings embed the rows they were generated with and check, once, that +//! the library's table holds each one with an equal hash, naming the row's +//! [`path`](ContractRow::path) when it doesn't. Producer rows the bindings +//! don't know are fine, so adding a declaration, an error code, or a +//! callback method never breaks a deployed binding. +//! +//! [`tables`] is what every target renders from, so the embedded rows are +//! the producer's by construction. +//! +//! # Example +//! +//! A target that embeds the rows as a literal table, with each row's +//! canonical signature as a comment: +//! +//! ```ignore +//! use crate::codegen::contract; +//! +//! for table in contract::tables(model) { +//! w.line(format!("{:?}: [", table.symbol)); // "kv_kv_contract" +//! for row in &table.rows { +//! w.line(format!( +//! "({}, {}, {:?}), # {}", +//! contract::hex(row.id), +//! contract::hex(row.hash), +//! row.path, +//! row.signature, +//! )); +//! } +//! w.line("],"); +//! } +//! ``` + +use weaveffi_model::contract::{entries, ContractEntry}; +use weaveffi_model::model::{contract_check_symbol, contract_symbol, Model, ModuleBinding}; + +/// One declaration's row: its dotted path, the FNV-1a 64 `id` of the path +/// and `hash` of its canonical signature, and the signature itself (for +/// generated comments and diagnostics). +pub(crate) type ContractRow = ContractEntry; + +/// A row's `id` or `hash` as an 18-character hex literal, `0x` plus 16 +/// digits (`0x01d4f09bbddc986f`), the spelling most targets embed. +#[must_use] +pub(crate) fn hex(value: u64) -> String { + format!("0x{value:016x}") +} + +/// One top-level module's contract: the producer function that returns its +/// table and the rows these bindings were generated with. +#[derive(Debug, Clone)] +pub(crate) struct ContractTable<'m> { + /// The top-level module. + pub(crate) root: &'m ModuleBinding, + /// The producer's table function, `{prefix}_{module}_contract` + /// (`const {prefix}_contract_entry* f(size_t* out_len)`). + pub(crate) symbol: String, + /// The C header's `static inline` checker for this table, + /// `{prefix}_{module}_contract_check`, for targets that compile against + /// the header. + pub(crate) check_symbol: String, + /// Every declaration in the module and its submodules, sorted by id + /// (the producer's order, so a lookup can binary-search either side). + pub(crate) rows: Vec, +} + +/// The rows of top-level module `root`: one per declaration in it and its +/// submodules, sorted by id. +#[must_use] +pub(crate) fn rows(model: &Model, root: &ModuleBinding) -> Vec { + entries(model, root) +} + +/// Every top-level module's contract table, in declaration order. +#[must_use] +pub(crate) fn tables(model: &Model) -> Vec> { + model + .roots() + .map(|root| ContractTable { + root, + symbol: contract_symbol(model.prefix(), &root.name), + check_symbol: contract_check_symbol(model.prefix(), &root.name), + rows: rows(model, root), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::test_model; + + #[test] + fn tables_cover_codes_and_callback_methods() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: kv + errors: + - name: KvError + codes: + - { name: NotFound, code: 1, message: missing, fields: [{ name: key, type: string }] } + callback_interfaces: + - name: Listener + methods: + - { name: on_put, params: [{ name: key, type: string }] } + functions: + - { name: get, params: [{ name: key, type: string }], return: i64, throws: KvError } +"#, + ); + let tables = tables(&model); + assert_eq!(tables.len(), 1); + assert_eq!(tables[0].symbol, "kv_kv_contract"); + assert_eq!(tables[0].check_symbol, "kv_kv_contract_check"); + let row = |path: &str| { + tables[0] + .rows + .iter() + .find(|r| r.path == path) + .unwrap_or_else(|| panic!("no row {path}")) + }; + assert_eq!( + row("kv.KvError.NotFound").signature, + "code NotFound = 1 {string}" + ); + assert_eq!( + row("kv.Listener.on_put").signature, + "callback_method #0 on_put(string) -> void" + ); + assert_eq!( + row("kv.get").signature, + "function get(string) -> i64 throws KvError" + ); + let r = row("kv.get"); + assert_eq!(hex(r.id), format!("0x{:016x}", r.id)); + assert_eq!(hex(1), "0x0000000000000001"); + assert!(tables[0].rows.windows(2).all(|w| w[0].id < w[1].id)); + } +} diff --git a/crates/weaveffi-cli/src/codegen/docs.rs b/crates/weaveffi-cli/src/codegen/docs.rs new file mode 100644 index 00000000..7c6a4399 --- /dev/null +++ b/crates/weaveffi-cli/src/codegen/docs.rs @@ -0,0 +1,367 @@ +//! **Doc and deprecation text** in a target's spelling. +//! +//! IDL docs and deprecation messages name API declarations in backticks: +//! `` deprecated: "Use `new_op` instead" ``. A target spells `new_op` its own +//! way (`newOp` in Swift and Kotlin, `NewOp` in Go and .NET, +//! `kv_kitchen_new_op` in C), so a message copied verbatim points at a name +//! that doesn't exist in the generated API. [`rewrite`] replaces each +//! backticked identifier through a closure the target supplies; +//! [`ApiNames`] tells the closure what the identifier names, and [`Doc`] +//! bundles a declaration's doc and deprecation message with the rewriting. +//! +//! Only code spans whose whole content is an identifier (or a dotted path +//! of identifiers, such as `` `Store.get` ``) are offered to the closure; +//! anything else in backticks (`` `x + 1` ``) is left alone, and so is an +//! identifier the closure returns `None` for. +//! +//! # Example +//! +//! A target with camel-cased callables and parameters and suffixed error +//! types: +//! +//! ```ignore +//! use crate::codegen::docs::{ApiNames, Doc, IdentKind}; +//! +//! let names = ApiNames::new(model); +//! let spell = |ident: &str| match names.kind(ident)? { +//! IdentKind::Function | IdentKind::Member | IdentKind::Param => Some(camel(ident)), +//! IdentKind::ErrorDomain => Some(errors::type_name(ident, "Error")), +//! _ => None, +//! }; +//! let doc = Doc::new(&f.doc, &f.deprecated); +//! w.doc(&doc.text(&spell), DocCommentStyle::TripleSlash); +//! if let Some(msg) = doc.deprecation(&spell) { +//! w.line(format!("@available(*, deprecated, message: {msg:?})")); +//! } +//! ``` + +use std::collections::HashMap; + +use weaveffi_model::model::Model; + +/// What an API identifier names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum IdentKind { + /// A module (`kitchen`). + Module, + /// A record, enum, interface, or callback interface (`Item`). + Type, + /// An error domain (`KitchenErrors`), which targets name through + /// [`weaveffi_model::errors::type_name`]. + ErrorDomain, + /// A module-level function (`new_op`). + Function, + /// An interface constructor, method, or static (`describe`). + Member, + /// A callback interface method (`on_ready`). + CallbackMethod, + /// An enum variant (`High`). + Variant, + /// An error code (`NotFound`). + ErrorCode, + /// A record, variant, or error-code field (`label`). + Field, + /// A parameter of a function, member, or callback method (`limit`). + Param, +} + +/// One declaration an identifier names. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ApiName { + /// What it is. + pub(crate) kind: IdentKind, + /// Its C ABI spelling, when it has its own: a callable's symbol, a + /// type's tag (a callback interface's vtable type), or an enum + /// variant's or error code's constant. `None` for modules, fields, + /// parameters, and callback methods. + pub(crate) c_name: Option, +} + +/// Every identifier the API declares, with what each one names. +/// +/// One identifier can name several declarations (a method `describe` on two +/// interfaces, a parameter `id` of many functions); [`get`](Self::get) +/// returns all of them, in declaration order. +#[derive(Debug, Clone, Default)] +pub(crate) struct ApiNames { + names: HashMap>, +} + +impl ApiNames { + /// Index every declaration in `model`. + #[must_use] + pub(crate) fn new(model: &Model) -> Self { + let mut index = Self::default(); + for m in &model.modules { + index.add(&m.name, IdentKind::Module, None); + for d in &m.errors { + index.add(&d.name, IdentKind::ErrorDomain, Some(&d.c_tag)); + for c in &d.codes { + index.add(&c.name, IdentKind::ErrorCode, Some(&c.c_const)); + for f in &c.fields { + index.add(&f.name, IdentKind::Field, None); + } + } + } + for e in &m.enums { + index.add(&e.name, IdentKind::Type, Some(&e.c_tag)); + for v in &e.variants { + index.add(&v.name, IdentKind::Variant, Some(&v.c_const)); + for f in &v.fields { + index.add(&f.name, IdentKind::Field, None); + } + } + } + for s in &m.structs { + index.add(&s.name, IdentKind::Type, Some(&s.c_tag)); + for f in &s.fields { + index.add(&f.name, IdentKind::Field, None); + } + } + for cb in &m.callback_interfaces { + index.add(&cb.name, IdentKind::Type, Some(&cb.vtable_tag)); + for meth in &cb.methods { + index.add(&meth.name, IdentKind::CallbackMethod, None); + for p in &meth.params { + index.add(&p.name, IdentKind::Param, None); + } + } + } + for i in &m.interfaces { + index.add(&i.name, IdentKind::Type, Some(&i.c_tag)); + } + let members = m.interfaces.iter().flat_map(|i| i.members()); + let callables = m + .functions + .iter() + .map(|f| (f, IdentKind::Function)) + .chain(members.map(|f| (f, IdentKind::Member))); + for (f, kind) in callables { + index.add(&f.name, kind, Some(&f.abi.symbol)); + for p in &f.params { + index.add(&p.name, IdentKind::Param, None); + } + } + } + index + } + + fn add(&mut self, name: &str, kind: IdentKind, c_name: Option<&String>) { + self.names + .entry(name.to_string()) + .or_default() + .push(ApiName { + kind, + c_name: c_name.cloned(), + }); + } + + /// Every declaration `name` names, in declaration order (empty when the + /// API declares nothing by that name). + #[must_use] + pub(crate) fn get(&self, name: &str) -> &[ApiName] { + self.names.get(name).map_or(&[], Vec::as_slice) + } + + /// What `name` names, when every declaration of it is the same kind. + #[must_use] + pub(crate) fn kind(&self, name: &str) -> Option { + let found = self.get(name); + let kind = found.first()?.kind; + found.iter().all(|n| n.kind == kind).then_some(kind) + } + + /// The C ABI spelling of `name`, when it names exactly one declaration + /// and that declaration has one. + #[must_use] + pub(crate) fn c_name(&self, name: &str) -> Option<&str> { + match self.get(name) { + [only] => only.c_name.as_deref(), + _ => None, + } + } +} + +/// `text` with every backticked identifier the closure spells replaced by +/// its spelling (the backticks stay). +/// +/// `spell` receives the span's content (`new_op`, or a dotted path such as +/// `Store.get`) and returns `None` to keep it unchanged. +#[must_use] +pub(crate) fn rewrite(text: &str, spell: impl Fn(&str) -> Option) -> String { + map_code_spans(text, |ident| spell(ident).map(|s| format!("`{s}`"))) +} + +/// `text` with every backticked identifier the closure maps replaced by the +/// closure's result, which replaces the whole span *including* its +/// backticks. Use it to change the markup as well as the spelling (Javadoc +/// `{@code newOp}`, .NET `NewOp`, reStructuredText ``` ``new_op`` ```). +#[must_use] +pub(crate) fn map_code_spans(text: &str, mut map: impl FnMut(&str) -> Option) -> String { + let mut out = String::with_capacity(text.len()); + let mut rest = text; + while let Some(open) = rest.find('`') { + out.push_str(&rest[..open]); + let after = &rest[open + 1..]; + let Some(close) = after.find('`') else { + out.push_str(&rest[open..]); + return out; + }; + let inner = &after[..close]; + match is_ident_path(inner).then(|| map(inner)).flatten() { + Some(replacement) => out.push_str(&replacement), + None => { + out.push('`'); + out.push_str(inner); + out.push('`'); + } + } + rest = &after[close + 1..]; + } + out.push_str(rest); + out +} + +/// Whether `s` is an identifier or a `.`-separated path of identifiers. +fn is_ident_path(s: &str) -> bool { + !s.is_empty() + && s.split('.').all(|part| { + let mut chars = part.chars(); + chars + .next() + .is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') + }) +} + +/// A declaration's doc and deprecation message, rendered through a target's +/// spelling closure (see [`rewrite`]). +#[derive(Debug, Clone, Copy)] +pub(crate) struct Doc<'a> { + doc: Option<&'a str>, + deprecated: Option<&'a str>, +} + +impl<'a> Doc<'a> { + /// The doc and deprecation message of one declaration, as the model + /// stores them. + #[must_use] + pub(crate) fn new(doc: &'a Option, deprecated: &'a Option) -> Self { + Self { + doc: doc.as_deref().map(str::trim).filter(|d| !d.is_empty()), + deprecated: deprecated + .as_deref() + .map(str::trim) + .filter(|d| !d.is_empty()), + } + } + + /// The doc text, trimmed and rewritten, or `None` when there's none. + #[must_use] + pub(crate) fn text(&self, spell: impl Fn(&str) -> Option) -> Option { + self.doc.map(|d| rewrite(d, spell)) + } + + /// The deprecation message, trimmed and rewritten, or `None` when the + /// declaration isn't deprecated. + #[must_use] + pub(crate) fn deprecation(&self, spell: impl Fn(&str) -> Option) -> Option { + self.deprecated.map(|d| rewrite(d, spell)) + } + + /// The doc text followed by a `Deprecated: {message}` paragraph, for + /// targets (or positions) with no deprecation attribute. + #[must_use] + pub(crate) fn with_deprecation( + &self, + spell: impl Fn(&str) -> Option, + ) -> Option { + let note = self.deprecation(&spell).map(|m| format!("Deprecated: {m}")); + match (self.text(&spell), note) { + (Some(doc), Some(note)) => Some(format!("{doc}\n\n{note}")), + (doc, note) => doc.or(note), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::test_model; + + #[test] + fn rewrites_only_identifier_spans() { + let upper = |s: &str| Some(s.to_uppercase()); + assert_eq!( + rewrite("Use `new_op` or `a.b`, not `x + 1`.", upper), + "Use `NEW_OP` or `A.B`, not `x + 1`." + ); + assert_eq!(rewrite("unclosed `tick", upper), "unclosed `tick"); + assert_eq!(rewrite("`keep` it", |_| None), "`keep` it"); + assert_eq!( + map_code_spans("see `get`", |s| Some(format!("{{@code {s}}}"))), + "see {@code get}" + ); + } + + #[test] + fn indexes_every_declaration_kind() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: kv + errors: + - name: KvError + codes: [{ name: NotFound, code: 1, message: missing }] + enums: + - name: Mode + variants: [{ name: Fast, value: 0 }] + structs: + - name: Entry + fields: [{ name: key, type: string }] + interfaces: + - name: Store + methods: + - { name: get, params: [{ name: key, type: string }], return: Entry } + functions: + - { name: open, params: [{ name: path, type: string }], return: Store } +"#, + ); + let names = ApiNames::new(&model); + assert_eq!(names.kind("kv"), Some(IdentKind::Module)); + assert_eq!(names.kind("KvError"), Some(IdentKind::ErrorDomain)); + assert_eq!(names.c_name("NotFound"), Some("kv_kv_KvError_NotFound")); + assert_eq!(names.c_name("Fast"), Some("kv_kv_Mode_Fast")); + assert_eq!(names.c_name("Store"), Some("kv_kv_Store")); + assert_eq!(names.c_name("get"), Some("kv_kv_Store_get")); + assert_eq!(names.c_name("open"), Some("kv_kv_open")); + // `key` is both a field and a parameter. + assert_eq!(names.get("key").len(), 2); + assert_eq!(names.kind("key"), None); + assert_eq!(names.c_name("key"), None); + assert!(names.get("missing").is_empty()); + } + + #[test] + fn doc_appends_the_rewritten_deprecation() { + let doc = Some("Old `op`.".to_string()); + let deprecated = Some(" Use `new_op` ".to_string()); + let spell = |s: &str| Some(format!("my_{s}")); + let d = Doc::new(&doc, &deprecated); + assert_eq!(d.text(spell).as_deref(), Some("Old `my_op`.")); + assert_eq!(d.deprecation(spell).as_deref(), Some("Use `my_new_op`")); + assert_eq!( + d.with_deprecation(spell).as_deref(), + Some("Old `my_op`.\n\nDeprecated: Use `my_new_op`") + ); + let none = None; + assert_eq!( + Doc::new(&none, &deprecated) + .with_deprecation(spell) + .as_deref(), + Some("Deprecated: Use `my_new_op`") + ); + assert_eq!(Doc::new(&none, &none).with_deprecation(spell), None); + } +} diff --git a/crates/weaveffi-cli/src/codegen/errors.rs b/crates/weaveffi-cli/src/codegen/errors.rs new file mode 100644 index 00000000..c0dff394 --- /dev/null +++ b/crates/weaveffi-cli/src/codegen/errors.rs @@ -0,0 +1,133 @@ +//! The **error tables**: every error domain with its codes and the type +//! names a target gives them. +//! +//! A function declared `throws: KvError` fails with one of `KvError`'s +//! positive codes (plus its fields as a value-buffer payload when the code +//! declares any), a runtime code (negative), or, when the producer is newer +//! than the bindings, a positive code the bindings don't know. Domains are +//! open: an unknown positive code maps to the domain's base error type with +//! its code and message preserved, never to a crash. `throws: any` fails +//! with code -1 and a message only. +//! +//! Type names come from [`weaveffi_model::errors::type_name`] (one suffix, +//! never doubled: `KitchenErrors` with `Error` is `KitchenError`, `KvError` +//! stays `KvError`, `Failure` becomes `FailureError`), so no target names a +//! domain or code by hand. A target picks the suffix its ecosystem uses +//! (`"Error"`, `"Exception"`). +//! +//! # Example +//! +//! A target that raises one exception class per code, each deriving from +//! its domain's base class, and maps codes back when decoding a failure: +//! +//! ```ignore +//! use crate::codegen::errors; +//! +//! for table in errors::tables(model, "Exception") { +//! w.line(format!("class {}(LibraryError): ...", table.type_name)); +//! for row in &table.codes { +//! w.line(format!( +//! "class {}({}): code = {}", +//! row.type_name, table.type_name, row.code.value +//! )); +//! } +//! // An unknown positive code: raise `table.type_name` itself. +//! } +//! ``` + +use weaveffi_model::errors::type_name; +use weaveffi_model::model::{ErrorBinding, ErrorCodeBinding, Model, ModuleBinding}; + +/// One error domain, its owning module, and its codes, named for a target. +#[derive(Debug, Clone)] +pub(crate) struct ErrorTable<'m> { + /// The module that declares the domain. + pub(crate) module: &'m ModuleBinding, + /// The domain as lowered (raw `name`, `c_tag`, ...). + pub(crate) domain: &'m ErrorBinding, + /// The domain's base type name: `type_name(domain.name, suffix)`. + pub(crate) type_name: String, + /// The codes in declaration order. + pub(crate) codes: Vec>, +} + +/// One code of an [`ErrorTable`]. +#[derive(Debug, Clone)] +pub(crate) struct CodeRow<'m> { + /// The code as lowered (`name`, `value`, `message`, `c_const`, fields). + pub(crate) code: &'m ErrorCodeBinding, + /// The code's own type name: `type_name(code.name, suffix)` + /// (`NotFound` -> `NotFoundError`), for targets with one type per code. + /// Targets that nest codes as cases of the domain type use + /// [`weaveffi_model::errors::pascal`] instead. + pub(crate) type_name: String, +} + +/// Every error domain in `model` (module by module, in declaration order) +/// with type names built with `suffix`. +#[must_use] +pub(crate) fn tables<'m>(model: &'m Model, suffix: &str) -> Vec> { + model + .error_domains() + .map(|(module, domain)| ErrorTable { + module, + domain, + type_name: type_name(&domain.name, suffix), + codes: domain + .codes + .iter() + .map(|code| CodeRow { + code, + type_name: type_name(&code.name, suffix), + }) + .collect(), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::test_model; + + #[test] + fn names_never_double_the_suffix() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + errors: + - name: KitchenErrors + codes: [{ name: NOT_FOUND, code: 1, message: missing }] + - name: Failure + codes: [{ name: Broken, code: 1, message: broken }] + modules: + - name: sub + errors: + - name: KvError + codes: [{ name: Full, code: 1, message: full }] +"#, + ); + let names: Vec<(String, Vec)> = tables(&model, "Error") + .into_iter() + .map(|t| { + ( + format!("{}:{}", t.module.dot_path, t.type_name), + t.codes.iter().map(|c| c.type_name.clone()).collect(), + ) + }) + .collect(); + assert_eq!( + names, + [ + ("m:KitchenError".into(), vec!["NotFoundError".into()]), + ("m:FailureError".into(), vec!["BrokenError".into()]), + ("m.sub:KvError".into(), vec!["FullError".into()]), + ] + ); + let exceptions = tables(&model, "Exception"); + assert_eq!(exceptions[0].type_name, "KitchenException"); + assert_eq!(exceptions[0].codes[0].type_name, "NotFoundException"); + } +} diff --git a/crates/weaveffi-cli/src/codegen/mod.rs b/crates/weaveffi-cli/src/codegen/mod.rs index a7ca6de0..d7052d08 100644 --- a/crates/weaveffi-cli/src/codegen/mod.rs +++ b/crates/weaveffi-cli/src/codegen/mod.rs @@ -1,96 +1,323 @@ -//! Target erasure and orchestration. +//! Orchestration: rendering targets and turning their output into a +//! [`Changeset`] against an output directory. //! -//! Each language target implements [`LanguageBackend`] with its own typed -//! `Config`. The orchestrator works on the object-safe [`Target`] trait, which -//! erases the concrete config; [`ConfiguredBackend`] is the adapter that pairs -//! a backend with a concrete config value and is what the CLI and tests pass -//! into [`Orchestrator::with_target`]. +//! Rendering is pure: a [`Target`] returns its files in memory, with paths +//! relative to its own directory (`{out_dir}/{target}/`). The +//! [`Orchestrator`] renders every target, compares the result with the +//! output directory ([`Orchestrator::plan`]), and only then touches disk +//! ([`Changeset::apply`]). That one plan is what `weaveffi generate` writes, +//! what `generate --check` reports, and what `generate --diff` prints. A +//! plan rewrites only files whose contents changed and removes files a +//! previous run wrote that the current run no longer produces (see the +//! generation records in `.weaveffi-cache/`). //! -//! Rendering is pure: a target returns its files in memory, and the -//! [`Orchestrator`] does every write. That is what lets it rewrite only files -//! whose contents changed, remove files a previous run wrote that the -//! current run no longer produces (see [`crate::cache`]), and lets -//! `weaveffi diff` compare without touching disk. - -use anyhow::{Context, Result}; -use camino::Utf8Path; +//! The submodules are the **shared emitters** every target renders through +//! instead of keeping its own copy: +//! +//! - `codecs`: the value-buffer composites an API uses and their one +//! canonical stem (`list_i32`, `opt_Item`). +//! - `contract`: the contract rows a consumer checks at load. +//! - `errors`: every error domain with its codes and target type names. +//! - `docs`: doc and deprecation text with backticked identifiers in the +//! target's spelling. +//! - `common`: doc-comment emission, prose wrapping, and `PascalCase`. +//! - [`CodeWriter`]: the indentation-aware writer. +//! +//! None of them matches on a value's family or re-derives lowering: the +//! passing contracts (`ArgPass`, `RetPass`, `ErrorStrategy`, ...) stored on +//! the [`Model`] say how every value crosses. + +use std::fmt::Write as _; + +use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; +use miette::{miette, IntoDiagnostic, Result, WrapErr}; use rayon::prelude::*; +use similar::TextDiff; -use crate::backend::{LanguageBackend, OutputFile}; -use crate::cache::{self, Record}; -use crate::package::{Artifact, PackageContext}; +use crate::record::{self, Record}; +use crate::targets::Target; use weaveffi_model::model::Model; -pub mod common; -pub mod writer; +pub(crate) mod codecs; +pub(crate) mod common; +pub(crate) mod contract; +pub(crate) mod docs; +pub(crate) mod errors; +mod writer; pub use writer::CodeWriter; -/// Object-safe view of a [`LanguageBackend`] paired with a concrete config. -/// -/// The orchestrator stores targets as `&dyn Target` so it can hold a -/// heterogeneous set whose `Config` types differ. [`ConfiguredBackend`] is -/// the canonical adapter. -pub trait Target: Send + Sync { - /// The target's stable short name. Mirrors [`LanguageBackend::name`]. - fn name(&self) -> &'static str; - /// Render every file the target produces for `model`, with paths under - /// `out_dir`. Pure: nothing is written. - fn render(&self, model: &Model, out_dir: &Utf8Path) -> Vec; - /// Assemble the installable artifacts for this target, using the bound - /// config. Returns `None` when the target has no packaging. - fn package(&self, model: &Model, ctx: &PackageContext) -> Option>; +/// A single generated file: its path relative to the target's directory and +/// its rendered contents. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutputFile { + /// Path relative to the target's directory (`{out_dir}/{target}/`), + /// with `/` separators. + pub path: Utf8PathBuf, + /// The rendered file contents. + pub contents: String, } -/// Binds a [`LanguageBackend`] to a concrete config value so it can be erased -/// to `&dyn Target`. -/// -/// ```ignore -/// let swift = ConfiguredBackend::new(SwiftGenerator, SwiftConfig::default()); -/// orchestrator.with_target(&swift); -/// ``` -pub struct ConfiguredBackend { - inner: B, - config: B::Config, +impl OutputFile { + /// Pair a path, relative to the target's directory, with its rendered + /// contents. + /// + /// The path is normalized to `/` separators, which every platform's file + /// APIs accept, so listings, generation records, and tests see the same + /// path on Windows as elsewhere. + pub fn new(path: impl Into, contents: impl Into) -> Self { + let path: Utf8PathBuf = path.into(); + let path = if path.as_str().contains('\\') { + Utf8PathBuf::from(path.as_str().replace('\\', "/")) + } else { + path + }; + Self { + path, + contents: contents.into(), + } + } } -impl ConfiguredBackend { - /// Pair a backend with the concrete config it should run under. - pub fn new(inner: B, config: B::Config) -> Self { - Self { inner, config } +/// The path of a target's `file` relative to the output directory +/// (`{target}/{file}`), with `/` separators on every platform. +fn output_path(target: &str, file: &Utf8Path) -> Result { + let mut parts = vec![target]; + for component in file.components() { + match component { + Utf8Component::Normal(part) => parts.push(part), + Utf8Component::CurDir => {} + _ => { + return Err(miette!( + "the {target} target rendered {file}, which isn't a path inside its directory" + )) + } + } } + Ok(parts.join("/")) +} + +/// How applying a [`Changeset`] changes one file. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChangeKind { + /// The file doesn't exist yet. + Added, + /// The file exists with different contents. + Modified, + /// A previous generation wrote the file and this one doesn't. + Removed, } -impl Target for ConfiguredBackend { - fn name(&self) -> &'static str { - self.inner.name() +impl ChangeKind { + /// The one-character marker `generate --check` prints before a path. + #[must_use] + pub fn marker(self) -> char { + match self { + Self::Added => '+', + Self::Modified => '~', + Self::Removed => '-', + } } +} + +/// One planned file of one target. +#[derive(Debug, Clone)] +struct PlannedFile { + /// Path relative to the output directory. + path: String, + contents: String, + /// What's on disk now, when the file exists. + old: Option>, +} - fn render(&self, model: &Model, out_dir: &Utf8Path) -> Vec { - self.inner.files(model, out_dir, &self.config) +impl PlannedFile { + fn change(&self) -> Option { + match &self.old { + None => Some(ChangeKind::Added), + Some(old) if old != self.contents.as_bytes() => Some(ChangeKind::Modified), + Some(_) => None, + } } +} + +/// One target's part of a [`Changeset`]. +#[derive(Debug, Clone)] +struct TargetPlan { + name: &'static str, + files: Vec, + /// Files the previous generation wrote that this one doesn't, still on + /// disk, with their current contents. + removed: Vec<(String, Vec)>, + record: Record, + previous: Option, +} - fn package(&self, model: &Model, ctx: &PackageContext) -> Option> { - self.inner.package(model, ctx, &self.config) +impl TargetPlan { + fn changes_files(&self) -> bool { + !self.removed.is_empty() || self.files.iter().any(|f| f.change().is_some()) } + + fn is_current(&self) -> bool { + !self.changes_files() && self.previous.as_ref() == Some(&self.record) + } +} + +/// Everything regenerating would do to an output directory, computed +/// without writing: which files would be added, modified, or removed. +#[derive(Debug, Clone)] +pub struct Changeset { + out_dir: Utf8PathBuf, + targets: Vec, } -/// The path of `file` relative to `out_dir`, with `/` separators on every -/// platform, so listings and cache records are OS-independent. -/// -/// # Panics -/// -/// Panics if a backend emitted a path outside `out_dir`, which is a backend -/// bug. -#[must_use] -pub fn relative_path(out_dir: &Utf8Path, file: &Utf8Path) -> String { - let rel = file - .strip_prefix(out_dir) - .unwrap_or_else(|_| panic!("generated file {file} is outside {out_dir}")); - rel.components() - .map(|c| c.as_str()) - .collect::>() - .join("/") +/// One file a [`Changeset`] would add, modify, or remove. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Change<'a> { + /// What happens to the file. + pub kind: ChangeKind, + /// The file's path relative to the output directory. + pub path: &'a str, +} + +impl Changeset { + /// The output directory the plan was made against. + #[must_use] + pub fn out_dir(&self) -> &Utf8Path { + &self.out_dir + } + + /// Every file the targets render, relative to the output directory, in + /// target order (what `generate --dry-run` lists). + pub fn rendered(&self) -> impl Iterator { + self.targets + .iter() + .flat_map(|t| t.files.iter().map(|f| f.path.as_str())) + } + + /// Every file applying the plan would add, modify, or remove, in target + /// order. + pub fn changes(&self) -> impl Iterator> { + self.targets.iter().flat_map(|t| { + let written = t.files.iter().filter_map(|f| { + f.change().map(|kind| Change { + kind, + path: &f.path, + }) + }); + let removed = t.removed.iter().map(|(path, _)| Change { + kind: ChangeKind::Removed, + path, + }); + written.chain(removed) + }) + } + + /// Whether applying the plan would leave every generated file as it is. + /// (A missing generation record alone doesn't count as a change.) + #[must_use] + pub fn is_empty(&self) -> bool { + self.changes().next().is_none() + } + + /// A unified diff (`git diff` style, with `a/` and `b/` prefixes) of + /// every change, with `/dev/null` standing in for added and removed + /// files. + #[must_use] + pub fn unified_diff(&self) -> String { + let mut out = String::new(); + for t in &self.targets { + for f in &t.files { + let old = match (&f.old, f.change()) { + (_, None) => continue, + (None, _) => None, + (Some(old), _) => Some(String::from_utf8_lossy(old)), + }; + push_diff(&mut out, &f.path, old.as_deref(), Some(&f.contents)); + } + for (path, old) in &t.removed { + push_diff(&mut out, path, Some(&String::from_utf8_lossy(old)), None); + } + } + out + } + + /// Apply the plan: write every added or modified file, remove stale + /// files, and update the generation records. + /// + /// # Errors + /// + /// Returns an error when a directory can't be created or a file can't + /// be written or removed. + pub fn apply(&self) -> Result { + let mut report = GenerateReport::default(); + if self.targets.iter().all(TargetPlan::is_current) { + report.up_to_date = self.targets.iter().map(|t| t.name).collect(); + return Ok(report); + } + std::fs::create_dir_all(self.out_dir.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create the output directory {}", self.out_dir))?; + for t in &self.targets { + let record_changed = t.previous.as_ref() != Some(&t.record); + let mut changed = record_changed; + for file in &t.files { + if file.change().is_none() { + report.unchanged += 1; + continue; + } + let path = self.out_dir.join(&file.path); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create {parent}"))?; + } + std::fs::write(path.as_std_path(), &file.contents) + .into_diagnostic() + .wrap_err_with(|| format!("failed to write {path}"))?; + report.written += 1; + changed = true; + } + for (rel, _) in &t.removed { + let path = self.out_dir.join(rel); + std::fs::remove_file(path.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to remove the stale file {path}"))?; + report.removed.push(rel.clone()); + changed = true; + } + if record_changed { + record::write_record(&self.out_dir, t.name, &t.record)?; + } + if changed { + report.generated.push(t.name); + } else { + report.up_to_date.push(t.name); + } + } + Ok(report) + } +} + +/// Append one file's unified diff to `out`. +fn push_diff(out: &mut String, path: &str, old: Option<&str>, new: Option<&str>) { + let old_name = if old.is_some() { + format!("a/{path}") + } else { + "/dev/null".to_string() + }; + let new_name = if new.is_some() { + format!("b/{path}") + } else { + "/dev/null".to_string() + }; + let diff = TextDiff::from_lines(old.unwrap_or(""), new.unwrap_or("")); + let _ = write!( + out, + "{}", + diff.unified_diff() + .context_radius(3) + .header(&old_name, &new_name) + ); } /// What a generation run did. @@ -109,38 +336,33 @@ pub struct GenerateReport { pub removed: Vec, } -/// One target's rendered output, paired with its previous record. -struct Rendered<'a> { - target: &'a dyn Target, - files: Vec, - previous: Option, - record: Record, -} - -impl Rendered<'_> { - /// Files the previous run wrote that this run no longer produces and - /// that are still on disk. - fn stale<'r>(&'r self, out_dir: &'r Utf8Path) -> impl Iterator { - self.previous - .iter() - .flat_map(|p| p.files.iter()) - .filter(|rel| !self.record.files.contains(*rel) && out_dir.join(rel).exists()) - } - - /// Whether writing this target would change anything on disk. - fn is_current(&self, out_dir: &Utf8Path) -> bool { - self.previous.as_ref() == Some(&self.record) - && self.stale(out_dir).next().is_none() - && self.files.iter().all(on_disk) +impl GenerateReport { + /// One line describing the run, for `weaveffi generate`'s output. + #[must_use] + pub fn summary(&self, out_dir: &Utf8Path) -> String { + if self.generated.is_empty() { + return format!( + "{out_dir} is up to date ({} targets)", + self.up_to_date.len() + ); + } + let mut line = format!( + "Generated {} in {out_dir}: {} written, {} unchanged", + self.generated.join(", "), + self.written, + self.unchanged + ); + if !self.removed.is_empty() { + let _ = write!(line, ", {} stale removed", self.removed.len()); + } + if !self.up_to_date.is_empty() { + let _ = write!(line, " ({} up to date)", self.up_to_date.join(", ")); + } + line } } -/// Whether `file` is already on disk with its rendered contents. -fn on_disk(file: &OutputFile) -> bool { - std::fs::read(file.path.as_std_path()).is_ok_and(|bytes| bytes == file.contents.as_bytes()) -} - -/// Runs a set of targets against one API and output directory. +/// Runs a set of targets against one model and output directory. #[derive(Default)] pub struct Orchestrator<'a> { targets: Vec<&'a dyn Target>, @@ -148,6 +370,7 @@ pub struct Orchestrator<'a> { impl<'a> Orchestrator<'a> { /// An orchestrator with no targets. + #[must_use] pub fn new() -> Self { Self::default() } @@ -159,78 +382,89 @@ impl<'a> Orchestrator<'a> { self } - /// Generate every target under `out_dir`. - /// - /// Every target renders, in parallel. Only files whose contents differ - /// from the output directory are written, and files the previous run of - /// a target wrote that this run no longer produces are removed. + /// Add several targets. + #[must_use] + pub fn with_targets(mut self, targets: impl IntoIterator) -> Self { + self.targets.extend(targets); + self + } + + /// Render every target (in parallel) and compare the result with + /// `out_dir`, without writing anything. /// /// # Errors /// - /// Returns an error when a file cannot be written or removed. - pub fn run(&self, model: &Model, out_dir: &Utf8Path) -> Result { - let rendered: Vec> = self + /// Returns an error when a target renders a path outside its directory. + pub fn plan(&self, model: &Model, out_dir: &Utf8Path) -> Result { + let targets = self .targets .par_iter() - .map(|&target| { - let files = target.render(model, out_dir); - let record = Record { - files: files - .iter() - .map(|f| relative_path(out_dir, &f.path)) - .collect(), - }; - Rendered { - target, - files, - previous: cache::read_record(out_dir, target.name()), - record, - } - }) - .collect(); + .map(|&target| plan_target(target, model, out_dir)) + .collect::>>()?; + Ok(Changeset { + out_dir: out_dir.to_path_buf(), + targets, + }) + } - let mut report = GenerateReport::default(); - if rendered.iter().all(|r| r.is_current(out_dir)) { - report.up_to_date = rendered.iter().map(|r| r.target.name()).collect(); - return Ok(report); - } - for r in &rendered { - let mut changed = r.previous.as_ref() != Some(&r.record); - for file in &r.files { - if on_disk(file) { - report.unchanged += 1; - continue; - } - if let Some(parent) = file.path.parent() { - std::fs::create_dir_all(parent.as_std_path()) - .with_context(|| format!("failed to create {parent}"))?; - } - std::fs::write(file.path.as_std_path(), &file.contents) - .with_context(|| format!("failed to write {}", file.path))?; - report.written += 1; - changed = true; - } - let stale: Vec = r.stale(out_dir).cloned().collect(); - for rel in stale { - let path = out_dir.join(&rel); - std::fs::remove_file(path.as_std_path()) - .with_context(|| format!("failed to remove stale file {path}"))?; - report.removed.push(rel); - changed = true; - } - if r.previous.as_ref() != Some(&r.record) { - cache::write_record(out_dir, r.target.name(), &r.record)?; - } - if changed { - report.generated.push(r.target.name()); - } else { - report.up_to_date.push(r.target.name()); - } - } - Ok(report) + /// Generate every target under `out_dir`: [`plan`](Self::plan), then + /// [`apply`](Changeset::apply). + /// + /// # Errors + /// + /// Returns the errors of [`plan`](Self::plan) and + /// [`apply`](Changeset::apply). + pub fn run(&self, model: &Model, out_dir: &Utf8Path) -> Result { + self.plan(model, out_dir)?.apply() } } +/// Render `target` and compare its files with `out_dir`. +fn plan_target(target: &dyn Target, model: &Model, out_dir: &Utf8Path) -> Result { + let name = target.name(); + let files = target + .render(model) + .into_iter() + .map(|file| { + let path = output_path(name, &file.path)?; + let old = std::fs::read(out_dir.join(&path).as_std_path()).ok(); + Ok(PlannedFile { + path, + contents: file.contents, + old, + }) + }) + .collect::>>()?; + let record = Record { + files: files.iter().map(|f| f.path.clone()).collect(), + }; + let previous = record::read_record(out_dir, name); + let removed = previous + .iter() + .flat_map(|p| p.files.iter()) + .filter(|rel| !record.files.contains(*rel)) + .filter_map(|rel| { + let bytes = std::fs::read(out_dir.join(rel).as_std_path()).ok()?; + Some((rel.clone(), bytes)) + }) + .collect(); + Ok(TargetPlan { + name, + files, + removed, + record, + previous, + }) +} + +/// Parse and validate a YAML IDL for a unit test, with identity `kv`. +#[cfg(test)] +pub(crate) fn test_model(yaml: &str) -> Model { + let api = weaveffi_model::parse::parse_api_str(yaml, "yaml").expect("valid YAML"); + let identity = weaveffi_model::pkg::Identity::named("kv"); + weaveffi_model::validate::validate(&api, &identity, None).expect("valid API") +} + #[cfg(test)] mod tests { use super::*; @@ -238,38 +472,25 @@ mod tests { use std::sync::{Arc, Mutex}; use weaveffi_model::ir::{Api, Module}; - #[derive(Default, Clone)] - struct TestConfig; - struct Counting { name: &'static str, calls: Arc, files: Arc>>, } - impl LanguageBackend for Counting { - type Config = TestConfig; - + impl Target for Counting { fn name(&self) -> &'static str { self.name } - fn files( - &self, - model: &Model, - out_dir: &Utf8Path, - _config: &Self::Config, - ) -> Vec { + fn render(&self, model: &Model) -> Vec { self.calls.fetch_add(1, Ordering::SeqCst); self.files .lock() .unwrap() .iter() .map(|f| { - OutputFile::new( - out_dir.join(self.name).join(f), - format!("{} {}", model.prefix(), model.modules[0].name), - ) + OutputFile::new(*f, format!("{} {}", model.prefix(), model.modules[0].name)) }) .collect() } @@ -286,7 +507,7 @@ mod tests { structs: vec![], enums: vec![], callback_interfaces: vec![], - errors: None, + errors: Vec::new(), modules: vec![], }], }; @@ -294,15 +515,12 @@ mod tests { weaveffi_model::validate::validate(&api, &identity, None).unwrap() } - fn backend(name: &'static str, calls: &Arc) -> ConfiguredBackend { - ConfiguredBackend::new( - Counting { - name, - calls: Arc::clone(calls), - files: Arc::new(Mutex::new(vec!["out.txt"])), - }, - TestConfig, - ) + fn counting(name: &'static str, calls: &Arc) -> Counting { + Counting { + name, + calls: Arc::clone(calls), + files: Arc::new(Mutex::new(vec!["out.txt"])), + } } #[test] @@ -310,7 +528,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); let calls = Arc::new(AtomicUsize::new(0)); - let c = backend("c", &calls); + let c = counting("c", &calls); let orch = Orchestrator::new().with_target(&c); let model = model_named("math"); @@ -323,12 +541,27 @@ mod tests { assert_eq!(calls.load(Ordering::SeqCst), 2); std::fs::write(out.join("c/out.txt"), "edited").unwrap(); - let third = orch.run(&model, out).unwrap(); + let plan = orch.plan(&model, out).unwrap(); + let changes: Vec<_> = plan.changes().collect(); + assert_eq!( + changes, + [Change { + kind: ChangeKind::Modified, + path: "c/out.txt" + }] + ); + assert!( + plan.unified_diff().contains("-edited\n"), + "{}", + plan.unified_diff() + ); + let third = plan.apply().unwrap(); assert_eq!((third.generated.len(), third.written), (1, 1)); assert_eq!( std::fs::read_to_string(out.join("c/out.txt")).unwrap(), rendered ); + assert!(orch.plan(&model, out).unwrap().is_empty()); } #[test] @@ -336,15 +569,23 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); let calls = Arc::new(AtomicUsize::new(0)); - let c = backend("c", &calls); - let files = Arc::clone(&c.inner.files); + let c = counting("c", &calls); + let files = Arc::clone(&c.files); *files.lock().unwrap() = vec!["a.txt", "b.txt"]; let orch = Orchestrator::new().with_target(&c); orch.run(&model_named("math"), out).unwrap(); std::fs::write(out.join("c/user.txt"), "mine").unwrap(); *files.lock().unwrap() = vec!["a.txt"]; - let report = orch.run(&model_named("math"), out).unwrap(); + let plan = orch.plan(&model_named("math"), out).unwrap(); + assert_eq!( + plan.changes().collect::>(), + [Change { + kind: ChangeKind::Removed, + path: "c/b.txt" + }] + ); + let report = plan.apply().unwrap(); assert_eq!(report.removed, ["c/b.txt"]); assert_eq!(report.generated, ["c"]); assert!(out.join("c/a.txt").exists()); @@ -353,8 +594,12 @@ mod tests { } #[test] - fn relative_paths_use_forward_slashes() { - let out = Utf8Path::new("gen"); - assert_eq!(relative_path(out, &out.join("c").join("x.h")), "c/x.h"); + fn output_paths_stay_inside_the_target_directory() { + assert_eq!( + output_path("c", &Utf8Path::new("include").join("x.h")).unwrap(), + "c/include/x.h" + ); + assert!(output_path("c", Utf8Path::new("../x.h")).is_err()); + assert!(output_path("c", Utf8Path::new("/x.h")).is_err()); } } diff --git a/crates/weaveffi-cli/src/codegen/writer.rs b/crates/weaveffi-cli/src/codegen/writer.rs index 77652192..484651fb 100644 --- a/crates/weaveffi-cli/src/codegen/writer.rs +++ b/crates/weaveffi-cli/src/codegen/writer.rs @@ -12,23 +12,6 @@ //! stays in full control of the exact text it emits while losing the manual //! `\n`/indent bookkeeping. Output is byte-deterministic: blank lines never //! carry trailing whitespace, and the indent unit is fixed per writer. -//! -//! ``` -//! use weaveffi_cli::codegen::writer::CodeWriter; -//! -//! let mut w = CodeWriter::new(" "); -//! w.line("class Greeter:"); -//! w.scope(|w| { -//! w.line("def hello(self):"); -//! w.scope(|w| { -//! w.line("return \"hi\""); -//! }); -//! }); -//! assert_eq!( -//! w.finish(), -//! "class Greeter:\n def hello(self):\n return \"hi\"\n", -//! ); -//! ``` use crate::codegen::common::{emit_doc, DocCommentStyle}; @@ -39,6 +22,23 @@ use crate::codegen::common::{emit_doc, DocCommentStyle}; /// with [`scope`](Self::scope) / [`block`](Self::block), splice pre-rendered /// multi-line text with [`block_raw`](Self::block_raw), and finish with /// [`finish`](Self::finish). +/// +/// ``` +/// use weaveffi_cli::codegen::CodeWriter; +/// +/// let mut w = CodeWriter::new(" "); +/// w.line("class Greeter:"); +/// w.scope(|w| { +/// w.line("def hello(self):"); +/// w.scope(|w| { +/// w.line("return \"hi\""); +/// }); +/// }); +/// assert_eq!( +/// w.finish(), +/// "class Greeter:\n def hello(self):\n return \"hi\"\n", +/// ); +/// ``` #[derive(Debug, Clone)] pub struct CodeWriter { buf: String, @@ -188,7 +188,7 @@ impl CodeWriter { /// block. /// /// ``` - /// use weaveffi_cli::codegen::writer::CodeWriter; + /// use weaveffi_cli::codegen::CodeWriter; /// let mut w = CodeWriter::four_space(); /// w.block("fn main() {", "}", |w| { /// w.line("println!(\"hi\");"); @@ -211,7 +211,7 @@ impl CodeWriter { /// No-op when `doc` is `None` or trims to empty. Mirrors [`emit_doc`], /// but indents from the writer's current depth instead of an explicit /// prefix argument. - pub fn doc(&mut self, doc: &Option, style: DocCommentStyle) -> &mut Self { + pub(crate) fn doc(&mut self, doc: &Option, style: DocCommentStyle) -> &mut Self { let prefix = self.unit.repeat(self.depth); emit_doc(&mut self.buf, doc, &prefix, style); self diff --git a/crates/weaveffi-cli/src/commands/build.rs b/crates/weaveffi-cli/src/commands/build.rs index 0a7359cd..6489dc1e 100644 --- a/crates/weaveffi-cli/src/commands/build.rs +++ b/crates/weaveffi-cli/src/commands/build.rs @@ -2,32 +2,43 @@ //! prebuild the per-language C glue, laying everything out in //! `{target_dir}/weaveffi//` for `weaveffi package`. +use std::process::ExitCode; + use camino::Utf8PathBuf; -use miette::{bail, miette, IntoDiagnostic, Result, WrapErr}; -use weaveffi_cli::build::glue::{self, GlueSource}; -use weaveffi_cli::build::{BuildSettings, Builder}; -use weaveffi_cli::codegen::Target; -use weaveffi_cli::platform::{jni_shim_name, node_addon_name, BinarySet, Os, Platform}; -use weaveffi_cli::project::{Project, Source}; +use miette::{bail, miette, Result}; use weaveffi_model::model::Model; +use super::Locate; +use crate::build::glue::{self, one_line}; +use crate::build::{BuildSettings, Builder}; +use crate::package::Skips; +use crate::platform::{BinarySet, Platform}; +use crate::project::{Project, Source}; +use crate::targets::Target; + /// Options for [`cmd_build`]. -pub(crate) struct BuildArgs<'a> { - pub(crate) input: Option<&'a str>, - pub(crate) config: Option<&'a str>, - pub(crate) platforms: Option<&'a str>, - pub(crate) targets: Option<&'a str>, - pub(crate) debug: bool, - pub(crate) manifest_path: Option<&'a str>, - pub(crate) warn: bool, - pub(crate) quiet: bool, +pub struct BuildArgs<'a> { + /// Where the project is (`--library` doesn't apply). + pub locate: Locate<'a>, + /// `--platforms`. + pub platforms: Option<&'a str>, + /// `--target`. + pub targets: Option<&'a [String]>, + /// `--manifest-path`. + pub manifest_path: Option<&'a str>, + /// `--warn`. + pub warn: bool, + /// `--strict`: fail when an artifact is skipped. + pub strict: bool, } -pub(crate) fn cmd_build(args: &BuildArgs<'_>) -> Result<()> { - let project = Project::locate(args.config, args.input, None)?.quiet(args.quiet); - let targets = project.config.select_targets(args.targets)?; +/// Run `weaveffi build`. +pub fn cmd_build(args: &BuildArgs<'_>) -> Result { + let project = args.locate.project()?; + let targets = project.config.targets(args.targets)?; let platforms = resolve_platforms(args.platforms, &project)?; - let settings = project.config.build.settings(args.debug); + let settings = project.config.build.settings(args.locate.profile); + let skips = Skips::default(); let built = build_project( &project, &targets, @@ -35,8 +46,9 @@ pub(crate) fn cmd_build(args: &BuildArgs<'_>) -> Result<()> { &settings, args.manifest_path, args.warn, + &skips, )?; - if !args.quiet { + if !args.locate.quiet { println!("Built `{}` into {}", built.binaries.lib_name, built.dir); for nb in &built.binaries.binaries { let mut files = vec![nb.library.file_name().unwrap_or_default().to_string()]; @@ -50,7 +62,7 @@ pub(crate) fn cmd_build(args: &BuildArgs<'_>) -> Result<()> { println!(" {}: {}", nb.platform.id(), files.join(", ")); } } - Ok(()) + Ok(super::finish(&skips, args.strict)) } /// The platforms to build: `--platforms`, else `[build] platforms`, else @@ -110,13 +122,13 @@ pub(crate) fn model_from_binaries( super::load_model(&project.clone().library(first.library.clone())?, warn) } -/// Build the project's producer for every platform, then prebuild the glue -/// of the selected targets that need it (the Node.js addon for `node`, the -/// JNI shim for `kotlin`). Every platform is checked before anything -/// compiles, so a missing rustup target or NDK fails fast with every -/// problem listed. Glue a host can't compile is skipped with a warning; the -/// packages then fall back to compiling it at install or build time. A Rust -/// producer's API is read from the first library built. +/// Build the project's producer for every platform, then prebuild the +/// [`glue`](Target::glue) of the selected targets. Every platform is +/// checked before anything compiles; one this machine can't build (a +/// missing rustup target or NDK) is skipped and recorded in `skips`, and so +/// is glue the host can't compile (the packages then fall back to compiling +/// it at install or build time). A Rust producer's API is read from the +/// first library built. pub(crate) fn build_project( project: &Project, targets: &[Box], @@ -124,6 +136,7 @@ pub(crate) fn build_project( settings: &BuildSettings, manifest: Option<&str>, warn: bool, + skips: &Skips, ) -> Result { let quiet = project.is_quiet(); let krate = super::producer_crate(project, manifest)?; @@ -137,19 +150,21 @@ pub(crate) fn build_project( ); } let builder = Builder::new(&krate, library, settings).quiet(quiet); - let problems: Vec = platforms - .iter() - .filter_map(|p| builder.check(*p).err().map(|e| format!(" - {e:#}"))) - .collect(); - if !problems.is_empty() { + let mut buildable = Vec::new(); + for &platform in platforms { + match builder.check(platform) { + Ok(()) => buildable.push(platform), + Err(e) => skips.skip(format!("the {} library", platform.id()), one_line(&e)), + } + } + if buildable.is_empty() { bail!( - "can't build every requested platform on this machine:\n{}", - problems.join("\n") + "none of the requested platforms can be built on this machine (see the warnings above)" ); } let mut binaries = BinarySet::new(library.as_str()); - for &platform in platforms { + for platform in buildable { if !quiet { println!( "Building `{}` for {} ({})...", @@ -158,8 +173,7 @@ pub(crate) fn build_project( platform.rust_target() ); } - let binary = builder.build(platform).map_err(|e| miette!("{e:#}"))?; - binaries.insert(binary); + binaries.insert(builder.build(platform)?); } let model = match model { Some(model) => model, @@ -167,20 +181,10 @@ pub(crate) fn build_project( }; let dir = krate.weaveffi_dir(); - let wants = |name: &str| targets.iter().any(|t| t.name() == name); - if wants("node") { - prebuild_node(&model, targets, &dir, &mut binaries, settings, quiet)?; - } - if wants("kotlin") { - prebuild_jni( - &model, - targets, - &dir, - &mut binaries, - &builder, - settings, - quiet, - )?; + for target in targets { + if let Some(g) = target.glue(&model) { + glue::prebuild(&g, &dir, &mut binaries, &builder, settings, skips)?; + } } Ok(Built { binaries, @@ -188,150 +192,3 @@ pub(crate) fn build_project( model, }) } - -/// Render `target_name`'s files in memory and stage its glue source and the -/// C header under `{dir}/.glue/{target_name}/`. -fn stage_glue( - model: &Model, - targets: &[Box], - target_name: &str, - source_name: &str, - dir: &Utf8PathBuf, -) -> Result { - let target = targets - .iter() - .find(|t| t.name() == target_name) - .expect("the target is selected"); - let header_name = format!("{}.h", model.identity.library); - let staging = dir.join(".glue").join(target_name); - std::fs::create_dir_all(staging.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to create {staging}"))?; - let files = target.render(model, &staging); - for wanted in [source_name, header_name.as_str()] { - let file = files - .iter() - .find(|f| f.path.file_name() == Some(wanted)) - .ok_or_else(|| miette!("the {target_name} target rendered no {wanted}"))?; - std::fs::write(staging.join(wanted).as_std_path(), &file.contents) - .into_diagnostic() - .wrap_err_with(|| format!("failed to stage {wanted}"))?; - } - Ok(GlueSource { - source: staging.join(source_name), - include_dir: staging, - }) -} - -fn warn_skip(quiet: bool, what: &str, platform: Platform, reason: &str) { - if !quiet { - eprintln!("warning: skipped {what} for {}: {reason}", platform.id()); - } -} - -/// Prebuild the Node.js addon for every desktop platform this host can -/// compile for. -fn prebuild_node( - model: &Model, - targets: &[Box], - dir: &Utf8PathBuf, - binaries: &mut BinarySet, - settings: &BuildSettings, - quiet: bool, -) -> Result<()> { - let library = binaries.lib_name.clone(); - let desktop: Vec = binaries.platforms().filter(|p| p.is_desktop()).collect(); - if desktop.is_empty() { - return Ok(()); - } - let glue_source = stage_glue( - model, - targets, - "node", - &format!("{}.c", node_addon_name(&library)), - dir, - )?; - let headers = match glue::node_headers() { - Ok(headers) => headers, - Err(e) => { - for p in desktop { - warn_skip(quiet, "the Node.js addon", p, &format!("{e:#}")); - } - return Ok(()); - } - }; - for platform in desktop { - let platform_dir = dir.join(platform.id()); - match glue::compile_node_addon( - &glue_source, - platform, - &platform_dir, - &library, - &headers, - settings, - ) { - Ok(addon) => { - if let Some(nb) = binaries - .binaries - .iter_mut() - .find(|b| b.platform == platform) - { - nb.node_addon = Some(addon); - } - } - Err(e) => warn_skip(quiet, "the Node.js addon", platform, &format!("{e:#}")), - } - } - Ok(()) -} - -/// Prebuild the JNI shim for every Android ABI (with the NDK) and every -/// desktop platform this host can compile for (with the JDK's headers). -fn prebuild_jni( - model: &Model, - targets: &[Box], - dir: &Utf8PathBuf, - binaries: &mut BinarySet, - builder: &Builder<'_>, - settings: &BuildSettings, - quiet: bool, -) -> Result<()> { - let library = binaries.lib_name.clone(); - let platforms: Vec = binaries - .platforms() - .filter(|p| p.is_desktop() || p.os() == Os::Android) - .collect(); - if platforms.is_empty() { - return Ok(()); - } - let shim = jni_shim_name(&library); - let glue_source = stage_glue(model, targets, "kotlin", &format!("{shim}.c"), dir)?; - for platform in platforms { - let ndk = if platform.os() == Os::Android { - Some(builder.ndk().map_err(|e| miette!("{e:#}"))?) - } else { - None - }; - let platform_dir = dir.join(platform.id()); - match glue::compile_jni_shim( - &glue_source, - platform, - &platform_dir, - &library, - ndk, - settings, - ) { - Ok(path) => { - if let Some(nb) = binaries - .binaries - .iter_mut() - .find(|b| b.platform == platform) - { - nb.jni_shim = Some(path); - } - } - Err(e) => warn_skip(quiet, "the JNI shim", platform, &format!("{e:#}")), - } - } - Ok(()) -} diff --git a/crates/weaveffi-cli/src/commands/dev.rs b/crates/weaveffi-cli/src/commands/dev.rs index 6a508ba9..23f60583 100644 --- a/crates/weaveffi-cli/src/commands/dev.rs +++ b/crates/weaveffi-cli/src/commands/dev.rs @@ -1,27 +1,34 @@ //! `weaveffi dev`: the inner loop for a Rust producer. //! -//! It builds the crate's debug library, generates the bindings from it, and -//! then makes the generated packages find that library without any setup -//! where a package looks for a bundled copy: the library is copied into the -//! Python package, next to the Node.js package's `binding.gyp` (which links -//! and loads it from there), and into the Ruby gem's `lib/native/`. For the +//! It builds the crate's library (the `dev` profile unless `--profile` says +//! otherwise), generates the bindings from it, and then makes the generated +//! packages find that library without any setup where a package looks for a +//! bundled copy (each target's +//! [`dev_bundle_dir`](crate::targets::Target::dev_bundle_dir)). For the //! other targets it prints the one environment variable to set (or, for //! targets that link at build time, the directory to link from). +use std::process::ExitCode; + use camino::{Utf8Path, Utf8PathBuf}; use miette::{bail, IntoDiagnostic, Result, WrapErr}; -use super::generate::{generate, report_summary}; use super::Locate; +use crate::codegen::Orchestrator; +use crate::targets::Linkage; /// Options for [`cmd_dev`]. -pub(crate) struct DevArgs<'a> { - pub(crate) locate: Locate<'a>, - pub(crate) out: Option<&'a str>, - pub(crate) targets: Option<&'a str>, +pub struct DevArgs<'a> { + /// Where the project is. + pub locate: Locate<'a>, + /// `--out`. + pub out: Option<&'a str>, + /// `--target`. + pub targets: Option<&'a [String]>, } -pub(crate) fn cmd_dev(args: &DevArgs<'_>) -> Result<()> { +/// Run `weaveffi dev`. +pub fn cmd_dev(args: &DevArgs<'_>) -> Result { let project = args.locate.project()?; let Some(krate) = project.krate() else { bail!( @@ -39,53 +46,35 @@ pub(crate) fn cmd_dev(args: &DevArgs<'_>) -> Result<()> { let model = super::load_model(&project, false)?; let out_dir = project.config.out_dir(args.out); let quiet = args.locate.quiet; - if let Some(report) = generate(&project, &model, &out_dir, args.targets, false)? { - if !quiet { - println!("{}", report_summary(&report, &out_dir)); - } + let targets = project.config.targets(args.targets)?; + let report = Orchestrator::new() + .with_targets(targets.iter().map(AsRef::as_ref)) + .run(&model, &out_dir)?; + if !quiet { + println!("{}", report.summary(&out_dir)); } let shared = matches!(library.extension(), Some("so" | "dylib" | "dll")); let mut placed = Vec::new(); let mut by_env = Vec::new(); let mut by_link = Vec::new(); - for target in project.config.select_targets(args.targets)? { - let files = target.render(&model, &out_dir); - let file_dir = |suffix: &str| { - files - .iter() - .find(|f| f.path.as_str().ends_with(suffix)) - .and_then(|f| f.path.parent().map(Utf8Path::to_path_buf)) - }; - let dest = match target.name() { - "python" => file_dir("/__init__.py"), - "node" => file_dir("/binding.gyp"), - "ruby" => file_dir("runtime.rb").and_then(|d| d.parent().map(|p| p.join("native"))), - "c" | "cpp" | "swift" | "go" => { - by_link.push(target.name()); - None - } - "wasm" => None, - other => { - by_env.push(other); - None - } - }; - match dest { - Some(dir) if shared => { - copy_into(&library, &dir)?; - placed.push(dir); - } - Some(_) | None => { - if matches!(target.name(), "python" | "node" | "ruby") { - by_env.push(target.name()); - } - } + let mut wasm = false; + for target in &targets { + if let Some(dir) = target.dev_bundle_dir(&model).filter(|_| shared) { + let dir = out_dir.join(target.name()).join(dir); + copy_into(&library, &dir)?; + placed.push(dir); + continue; + } + match target.linkage() { + Linkage::Runtime => by_env.push(target.name()), + Linkage::Link => by_link.push(target.name()), + Linkage::Wasm => wasm = true, } } if quiet { - return Ok(()); + return Ok(ExitCode::SUCCESS); } println!("Built `{name}`: {library}"); for dir in &placed { @@ -108,15 +97,10 @@ pub(crate) fn cmd_dev(args: &DevArgs<'_>) -> Result<()> { model.identity.library ); } - if project - .config - .select_targets(args.targets)? - .iter() - .any(|t| t.name() == "wasm") - { + if wasm { println!(" wasm: needs a wasm32 build: `weaveffi build --platforms wasm32`"); } - Ok(()) + Ok(ExitCode::SUCCESS) } /// Copy `library` into `dir`, replacing an older copy. diff --git a/crates/weaveffi-cli/src/commands/diff.rs b/crates/weaveffi-cli/src/commands/diff.rs deleted file mode 100644 index c6a8dad7..00000000 --- a/crates/weaveffi-cli/src/commands/diff.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! `weaveffi diff`: show how regenerating would change an existing output -//! directory, without writing anything. `--check` turns it -//! into a CI gate. - -use std::collections::{BTreeMap, BTreeSet}; - -use camino::Utf8Path; -use miette::{IntoDiagnostic, Result}; -use similar::TextDiff; -use weaveffi_cli::cache; -use weaveffi_cli::codegen::relative_path; - -/// Options for [`cmd_diff`]. -pub(crate) struct DiffArgs<'a> { - pub(crate) locate: super::Locate<'a>, - pub(crate) out: Option<&'a str>, - pub(crate) targets: Option<&'a str>, - pub(crate) check: bool, -} - -/// Exit status of `diff --check` when files differ. -const EXIT_MODIFIED: i32 = 2; -/// Exit status of `diff --check` when files would be added or removed. -const EXIT_ADDED_OR_REMOVED: i32 = 3; - -pub(crate) fn cmd_diff(args: &DiffArgs<'_>) -> Result<()> { - let project = args.locate.project()?; - let model = super::load_model(&project, false)?; - let out_dir = project.config.out_dir(args.out); - let targets = project.config.select_targets(args.targets)?; - - let mut generated: BTreeMap = BTreeMap::new(); - let mut existing: BTreeSet = BTreeSet::new(); - for target in &targets { - for file in target.render(&model, &out_dir) { - generated.insert(relative_path(&out_dir, &file.path), file.contents); - } - // Files a previous generation recorded are the generator's; without a - // record, everything under the target's directory counts. - match cache::read_record(&out_dir, target.name()) { - Some(record) => existing.extend( - record - .files - .into_iter() - .filter(|rel| out_dir.join(rel).exists()), - ), - None => collect_files(&out_dir, &out_dir.join(target.name()), &mut existing)?, - } - } - - let (mut added, mut removed, mut modified) = (0usize, 0usize, 0usize); - let all: BTreeSet<&String> = generated.keys().chain(existing.iter()).collect(); - for rel in all { - match (generated.get(rel), existing.contains(rel)) { - (Some(_), false) => { - added += 1; - if !args.check { - println!("{rel}: [new file]"); - } - } - (None, true) => { - removed += 1; - if !args.check { - println!("{rel}: [would be removed]"); - } - } - (Some(new), true) => { - let old = - std::fs::read_to_string(out_dir.join(rel).as_std_path()).into_diagnostic()?; - if &old != new { - modified += 1; - if !args.check { - print_unified_diff(rel, &old, new); - } - } - } - (None, false) => {} - } - } - - if args.check { - println!("+ {added} added, - {removed} removed, ~ {modified} modified"); - if added > 0 || removed > 0 { - std::process::exit(EXIT_ADDED_OR_REMOVED); - } - if modified > 0 { - std::process::exit(EXIT_MODIFIED); - } - } else if added == 0 && removed == 0 && modified == 0 && !args.locate.quiet { - println!("No differences found."); - } - Ok(()) -} - -/// Every file under `dir` (recursively), relative to `base`. -fn collect_files(base: &Utf8Path, dir: &Utf8Path, out: &mut BTreeSet) -> Result<()> { - let Ok(entries) = std::fs::read_dir(dir.as_std_path()) else { - return Ok(()); - }; - for entry in entries { - let path = entry.into_diagnostic()?.path(); - let Some(path) = Utf8Path::from_path(&path) else { - continue; - }; - if path.is_dir() { - collect_files(base, path, out)?; - } else { - out.insert(relative_path(base, path)); - } - } - Ok(()) -} - -fn print_unified_diff(path: &str, old: &str, new: &str) { - let diff = TextDiff::from_lines(old, new); - println!("--- {path}"); - println!("+++ {path}"); - for hunk in diff.unified_diff().context_radius(3).iter_hunks() { - println!("{hunk}"); - } -} diff --git a/crates/weaveffi-cli/src/commands/extract.rs b/crates/weaveffi-cli/src/commands/extract.rs index c8832ca7..fa14298d 100644 --- a/crates/weaveffi-cli/src/commands/extract.rs +++ b/crates/weaveffi-cli/src/commands/extract.rs @@ -1,23 +1,33 @@ //! `weaveffi extract`: print the IDL a Rust producer's built library //! embeds. //! -//! The library is the project's crate, built here (`--release` for a -//! release build), or the one `--library` names. The document is the API +//! The library is the project's crate, built here (with the `dev` profile +//! unless `--profile` says otherwise), or the one `--library` names. The document is the API //! exactly as `weaveffi generate` would read it, validated against the //! crate's identity first, so what it prints always generates. +use std::process::ExitCode; + use miette::{miette, IntoDiagnostic, Report, Result, WrapErr}; -use weaveffi_cli::project::Source; -use crate::IdlFormat; +use crate::project::Source; + +/// An IDL serialization format. +#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)] +pub enum IdlFormat { + /// YAML. + Yaml, + /// JSON. + Json, +} /// Extract the project's API and write it as `format` to `output` (else /// stdout). -pub(crate) fn cmd_extract( +pub fn cmd_extract( locate: &super::Locate<'_>, output: Option<&str>, format: IdlFormat, -) -> Result<()> { +) -> Result { let project = locate.project()?; if let Source::Idl(idl) = &project.source { return Err(miette!( @@ -29,7 +39,7 @@ pub(crate) fn cmd_extract( definition.validate().map_err(Report::new)?; let api = &definition.api; let serialized = match format { - IdlFormat::Yaml => serde_yaml::to_string(api) + IdlFormat::Yaml => serde_saphyr::to_string(api) .into_diagnostic() .wrap_err("failed to serialize API as YAML")?, IdlFormat::Json => { @@ -39,9 +49,6 @@ pub(crate) fn cmd_extract( json.push('\n'); json } - IdlFormat::Toml => toml::to_string_pretty(api) - .into_diagnostic() - .wrap_err("failed to serialize API as TOML")?, }; match output { Some(path) => { @@ -54,5 +61,5 @@ pub(crate) fn cmd_extract( } None => print!("{serialized}"), } - Ok(()) + Ok(ExitCode::SUCCESS) } diff --git a/crates/weaveffi-cli/src/commands/generate.rs b/crates/weaveffi-cli/src/commands/generate.rs index 1bf97c86..9d125dc0 100644 --- a/crates/weaveffi-cli/src/commands/generate.rs +++ b/crates/weaveffi-cli/src/commands/generate.rs @@ -1,90 +1,80 @@ -//! `weaveffi generate`: parse, validate, and run the selected targets -//! through the orchestrator (plus `--dry-run`). +//! `weaveffi generate`: parse, validate, plan the selected targets against +//! the output directory, then write the plan, or with `--dry-run`, +//! `--check`, or `--diff`, report it without writing. -use camino::Utf8Path; -use miette::{miette, IntoDiagnostic, Result, WrapErr}; -use weaveffi_cli::codegen::{relative_path, GenerateReport, Orchestrator}; -use weaveffi_cli::project::Project; -use weaveffi_model::model::Model; +use std::process::ExitCode; + +use miette::Result; use super::Locate; +use crate::codegen::Orchestrator; /// Options for [`cmd_generate`]. -pub(crate) struct GenerateArgs<'a> { - pub(crate) locate: Locate<'a>, - pub(crate) out: Option<&'a str>, - pub(crate) targets: Option<&'a str>, - pub(crate) warn: bool, - pub(crate) dry_run: bool, +pub struct GenerateArgs<'a> { + /// Where the project is. + pub locate: Locate<'a>, + /// `--out`. + pub out: Option<&'a str>, + /// `--target`. + pub targets: Option<&'a [String]>, + /// `--warn`. + pub warn: bool, + /// `--dry-run`: list the files the targets render. + pub dry_run: bool, + /// `--check`: exit 1 when anything would change. + pub check: bool, + /// `--diff`: print the unified diff of what would change. + pub diff: bool, } -pub(crate) fn cmd_generate(args: &GenerateArgs<'_>) -> Result<()> { +/// Run `weaveffi generate`. +pub fn cmd_generate(args: &GenerateArgs<'_>) -> Result { + let quiet = args.locate.quiet; let project = args.locate.project()?; let model = super::load_model(&project, args.warn)?; let out_dir = project.config.out_dir(args.out); - let report = generate(&project, &model, &out_dir, args.targets, args.dry_run)?; - if let Some(report) = report { - if !args.locate.quiet { - println!("{}", report_summary(&report, &out_dir)); - } - } - Ok(()) -} + let targets = project.config.targets(args.targets)?; + let plan = Orchestrator::new() + .with_targets(targets.iter().map(AsRef::as_ref)) + .plan(&model, &out_dir)?; -/// Generate the selected targets into `out_dir` (or, with `dry_run`, list -/// the files that would be written), returning the run's report. -pub(crate) fn generate( - project: &Project, - model: &Model, - out_dir: &Utf8Path, - targets: Option<&str>, - dry_run: bool, -) -> Result> { - let selected = project.config.select_targets(targets)?; - if dry_run { - for target in &selected { - for file in target.render(model, out_dir) { - println!("{}", relative_path(out_dir, &file.path)); - } + if args.dry_run { + for path in plan.rendered() { + println!("{path}"); } - return Ok(None); + return Ok(ExitCode::SUCCESS); } - - std::fs::create_dir_all(out_dir.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to create output directory: {out_dir}"))?; - - let mut orchestrator = Orchestrator::new(); - for target in &selected { - orchestrator = orchestrator.with_target(target.as_ref()); + if args.diff { + print!("{}", plan.unified_diff()); } - let report = orchestrator - .run(model, out_dir) - .map_err(|e| miette!("{:#}", e))?; - Ok(Some(report)) -} - -/// One line describing what a generation run did. -pub(crate) fn report_summary(report: &GenerateReport, out_dir: &Utf8Path) -> String { - if report.generated.is_empty() { - return format!( - "{out_dir} is up to date ({} targets)", - report.up_to_date.len() + if args.check { + let changes: Vec<_> = plan.changes().collect(); + if !args.diff { + for change in &changes { + println!("{} {}", change.kind.marker(), change.path); + } + } + if changes.is_empty() { + if !quiet { + eprintln!("{out_dir} is up to date"); + } + return Ok(ExitCode::SUCCESS); + } + eprintln!( + "{} generated file{} in {out_dir} would change; run `weaveffi generate`", + changes.len(), + if changes.len() == 1 { "" } else { "s" } ); + return Ok(ExitCode::FAILURE); } - let mut line = format!( - "Generated {} in {out_dir}: {} written, {} unchanged", - report.generated.join(", "), - report.written, - report.unchanged - ); - if !report.removed.is_empty() { - line.push_str(&format!(", {} stale removed", report.removed.len())); + if args.diff { + return Ok(ExitCode::SUCCESS); } - if !report.up_to_date.is_empty() { - line.push_str(&format!(" ({} up to date)", report.up_to_date.join(", "))); + let report = plan.apply()?; + if !quiet { + println!("{}", report.summary(&out_dir)); } - line + Ok(ExitCode::SUCCESS) } #[cfg(test)] @@ -97,19 +87,22 @@ mod tests { let yml = dir.path().join("api.yml"); std::fs::write( &yml, - "version: \"0.11.0\"\nmodules:\n - name: math\n functions:\n - { name: add, params: [{ name: a, type: i32 }], return: i32 }\n", + "version: \"0.12.0\"\nmodules:\n - name: math\n functions:\n - { name: add, params: [{ name: a, type: i32 }], return: i32 }\n", ) .unwrap(); let out = dir.path().join("out"); + let targets = ["c".to_string()]; cmd_generate(&GenerateArgs { locate: Locate { input: yml.to_str(), ..Locate::default() }, out: out.to_str(), - targets: Some("c"), + targets: Some(&targets), warn: false, dry_run: true, + check: false, + diff: false, }) .unwrap(); assert!(!out.exists(), "dry-run should not create output directory"); diff --git a/crates/weaveffi-cli/src/commands/init.rs b/crates/weaveffi-cli/src/commands/init.rs index 70757789..4599608b 100644 --- a/crates/weaveffi-cli/src/commands/init.rs +++ b/crates/weaveffi-cli/src/commands/init.rs @@ -3,27 +3,38 @@ //! In a Rust crate (a directory with a `Cargo.toml` that has a `[package]`), //! it writes a `weaveffi.toml` whose `[project] input` is the crate itself //! (`"."`), whose API WeaveFFI reads from the library the crate builds, then -//! checks the things a producer needs (a `cdylib` crate type, a `weaveffi` -//! dependency, a `#[weaveffi::module]`, and one `weaveffi::export_runtime!()` -//! call) and prints any that are missing. It never edits `Cargo.toml` or +//! checks the things a producer needs (a `weaveffi` dependency, a +//! `#[weaveffi::module]`, and one `weaveffi::export_runtime!()` call) and +//! prints any that are missing. It never edits `Cargo.toml` or //! source files. Anywhere else, it writes a starter IDL plus a //! `weaveffi.toml` that points at it. +use std::process::ExitCode; + use camino::{Utf8Path, Utf8PathBuf}; use miette::{bail, IntoDiagnostic, Result, WrapErr}; use weaveffi_model::pkg::c_ident; -use weaveffi_cli::config::CONFIG_FILE_NAME; +use crate::config::CONFIG_FILE_NAME; + +/// The `targets` line `init` writes: the C header plus one language, which +/// a project edits to the targets it ships. +const TARGETS_LINE: &str = "targets = [\"c\", \"python\"]\n"; /// Options for [`cmd_init`]. -pub(crate) struct InitArgs<'a> { - pub(crate) dir: &'a str, - pub(crate) name: Option<&'a str>, - pub(crate) force: bool, - pub(crate) quiet: bool, +pub struct InitArgs<'a> { + /// The project directory. + pub dir: &'a str, + /// `--name`. + pub name: Option<&'a str>, + /// `--force`. + pub force: bool, + /// `--quiet`. + pub quiet: bool, } -pub(crate) fn cmd_init(args: &InitArgs<'_>) -> Result<()> { +/// Run `weaveffi init`. +pub fn cmd_init(args: &InitArgs<'_>) -> Result { let dir = Utf8PathBuf::from(args.dir); std::fs::create_dir_all(dir.as_std_path()) .into_diagnostic() @@ -43,9 +54,10 @@ pub(crate) fn cmd_init(args: &InitArgs<'_>) -> Result<()> { .map(str::to_string) }); match crate_name { - Some(name) => init_rust(&dir, &config_path, &name, args.quiet), - None => init_idl(&dir, &config_path, args.name, args.quiet), + Some(name) => init_rust(&dir, &config_path, &name, args.quiet)?, + None => init_idl(&dir, &config_path, args.name, args.quiet)?, } + Ok(ExitCode::SUCCESS) } fn write(path: &Utf8Path, contents: &str) -> Result<()> { @@ -57,11 +69,13 @@ fn write(path: &Utf8Path, contents: &str) -> Result<()> { fn init_rust(dir: &Utf8Path, config_path: &Utf8Path, crate_name: &str, quiet: bool) -> Result<()> { write( config_path, - "# WeaveFFI project configuration. See https://weaveffi.com/guides/config.html\n\ - [project]\n\ - input = \".\"\n\ - out = \"bindings\"\n\ - # targets = [\"c\", \"swift\", \"kotlin\", \"python\"]\n", + &format!( + "# WeaveFFI project configuration. See https://weaveffi.com/guides/config.html\n\ + [project]\n\ + input = \".\"\n\ + out = \"bindings\"\n\ + {TARGETS_LINE}" + ), )?; if quiet { return Ok(()); @@ -71,12 +85,6 @@ fn init_rust(dir: &Utf8Path, config_path: &Utf8Path, crate_name: &str, quiet: bo std::fs::read_to_string(dir.join("Cargo.toml").as_std_path()).unwrap_or_default(); let lib = std::fs::read_to_string(dir.join("src/lib.rs").as_std_path()).unwrap_or_default(); let mut todo = Vec::new(); - if !manifest.contains("cdylib") { - todo.push( - "add `crate-type = [\"cdylib\"]` under [lib] in Cargo.toml so `cargo build` \ - produces the shared library (`weaveffi build` picks the right kinds itself)", - ); - } if !manifest.contains("weaveffi") { todo.push("run `cargo add weaveffi`"); } @@ -138,6 +146,7 @@ fn init_idl(dir: &Utf8Path, config_path: &Utf8Path, name: Option<&str>, quiet: b [project]\n\ input = \"{idl_name}\"\n\ out = \"bindings\"\n\ + {TARGETS_LINE}\ \n\ [package]\n\ name = \"{name}\"\n\ @@ -147,8 +156,9 @@ fn init_idl(dir: &Utf8Path, config_path: &Utf8Path, name: Option<&str>, quiet: b if !quiet { println!("Wrote {idl_path} and {config_path}."); println!( - "Run `weaveffi generate --target c` to produce the header your native library \ - implements, then generate the other targets you ship." + "Run `weaveffi generate` to produce the C header your native library implements \ + and the Python package over it; edit `targets` in {config_path} for the other \ + languages you ship." ); } Ok(()) @@ -169,15 +179,12 @@ mod tests { quiet: true, }) .unwrap(); - let project = weaveffi_cli::project::Project::locate( - Some(root.join(CONFIG_FILE_NAME).as_str()), - None, - None, - ) - .unwrap(); + let project = + crate::project::Project::locate(Some(root.join(CONFIG_FILE_NAME).as_str()), None, None) + .unwrap(); assert!(matches!( &project.source, - weaveffi_cli::project::Source::Idl(p) if *p == root.join("greeter.yml") + crate::project::Source::Idl(p) if *p == root.join("greeter.yml") )); assert_eq!( project.config.package.identity.name.as_deref(), @@ -205,8 +212,14 @@ mod tests { quiet: true, }) .unwrap(); - let cfg = - weaveffi_cli::config::ProjectConfig::from_file(&root.join(CONFIG_FILE_NAME)).unwrap(); + let cfg = crate::config::ProjectConfig::from_file(&root.join(CONFIG_FILE_NAME)).unwrap(); assert_eq!(cfg.project.input.as_deref(), Some(Utf8Path::new("."))); + let names: Vec<&str> = cfg + .targets(None) + .unwrap() + .iter() + .map(|t| t.name()) + .collect(); + assert_eq!(names, ["c", "python"]); } } diff --git a/crates/weaveffi-cli/src/commands/mod.rs b/crates/weaveffi-cli/src/commands/mod.rs index e260bf01..2f2e99ba 100644 --- a/crates/weaveffi-cli/src/commands/mod.rs +++ b/crates/weaveffi-cli/src/commands/mod.rs @@ -1,38 +1,52 @@ -//! One module per subcommand. `main.rs` holds only argument parsing and -//! dispatch; everything a command does lives here, on top of the library's -//! [`Project`]. +//! The `weaveffi` subcommands, one module each. The binary parses the +//! arguments and dispatches here; every command returns the process's exit +//! code, and only the binary renders errors. +//! +//! This module is the binary's, not part of the library's API. -pub(crate) mod build; -pub(crate) mod dev; -pub(crate) mod diff; -pub(crate) mod extract; -pub(crate) mod generate; -pub(crate) mod init; -pub(crate) mod package; -pub(crate) mod validate; +// The binary is the only caller; its `--help` documents the errors. +#![allow(clippy::missing_errors_doc)] + +pub mod build; +pub mod dev; +pub mod extract; +pub mod generate; +pub mod init; +pub mod package; +pub mod validate; + +use std::process::ExitCode; use camino::{Utf8Path, Utf8PathBuf}; use miette::{bail, Result}; -use weaveffi_cli::cargo::CargoCrate; -use weaveffi_cli::project::{Project, Source}; use weaveffi_model::model::Model; -/// How a command finds its project: the positional input, `--config`, and -/// `--library`. +use crate::cargo::CargoCrate; +use crate::package::Skips; +use crate::project::{Project, Source}; + +/// How a command finds its project: the positional input, `--config`, +/// `--library`, and `--profile`. #[derive(Clone, Copy, Default)] -pub(crate) struct Locate<'a> { - pub(crate) input: Option<&'a str>, - pub(crate) config: Option<&'a str>, - pub(crate) library: Option<&'a str>, - pub(crate) release: bool, - pub(crate) quiet: bool, +pub struct Locate<'a> { + /// The input: a producer crate or an IDL. + pub input: Option<&'a str>, + /// `--config`. + pub config: Option<&'a str>, + /// `--library`. + pub library: Option<&'a str>, + /// `--profile`. + pub profile: Option<&'a str>, + /// `--quiet`. + pub quiet: bool, } impl Locate<'_> { - /// Locate the project. + /// Locate the project, building a crate's library with `--profile` + /// (default `dev`). pub(crate) fn project(&self) -> Result { Ok(Project::locate(self.config, self.input, self.library)? - .release(self.release) + .profile(self.profile.unwrap_or("dev")) .quiet(self.quiet)) } } @@ -75,5 +89,45 @@ pub(crate) fn producer_crate(project: &Project, manifest: Option<&str>) -> Resul --binaries `" ); } - CargoCrate::resolve(&path).map_err(|e| miette::miette!("{e:#}")) + CargoCrate::resolve(&path) +} + +/// Summarize the artifacts a `build` or `package` run skipped, returning +/// the exit code: failure only with `strict`. +pub(crate) fn finish(skips: &Skips, strict: bool) -> ExitCode { + let skipped = skips.list(); + if skipped.is_empty() { + return ExitCode::SUCCESS; + } + eprintln!( + "warning: skipped {} artifact{} because a tool is missing:", + skipped.len(), + if skipped.len() == 1 { "" } else { "s" } + ); + for s in &skipped { + eprintln!(" - {}: {}", s.what, s.reason); + } + if strict { + eprintln!("error: --strict fails on skipped artifacts"); + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn skipped_artifacts_fail_only_strict_runs() { + let code = |skips: &Skips, strict| format!("{:?}", finish(skips, strict)); + let success = format!("{:?}", ExitCode::SUCCESS); + let none = Skips::default(); + assert_eq!(code(&none, true), success); + let some = Skips::default(); + some.skip("the NuGet package", "no `dotnet` on PATH"); + assert_eq!(code(&some, false), success); + assert_eq!(code(&some, true), format!("{:?}", ExitCode::FAILURE)); + } } diff --git a/crates/weaveffi-cli/src/commands/package.rs b/crates/weaveffi-cli/src/commands/package.rs index 4be5990a..884ce022 100644 --- a/crates/weaveffi-cli/src/commands/package.rs +++ b/crates/weaveffi-cli/src/commands/package.rs @@ -5,46 +5,52 @@ //! platforms) or, with `--binaries `, from a directory laid out the same //! way, `//`, which is how CI hands libraries built on //! separate runners to one packaging job. Each target then writes its -//! ecosystem's artifacts: wheels, npm tarballs, gems, a SwiftPM package with -//! its `XCFramework` archive, a NuGet package, and so on. +//! ecosystem's artifacts through its +//! [`package`](crate::targets::Target::package) hook: wheels, npm tarballs, +//! gems, a SwiftPM package with its `XCFramework` archive, a NuGet package, +//! and so on. -use std::process::{Command, Stdio}; +use std::process::ExitCode; use camino::{Utf8Path, Utf8PathBuf}; -use miette::{bail, miette, IntoDiagnostic, Result, WrapErr}; -use weaveffi_cli::package::{ - archive, write_artifact, Artifact, PackageContext, PackagedFile, XcframeworkArchive, -}; -use weaveffi_cli::platform::{BinarySet, Os, Platform}; - -use weaveffi_cli::project::Project; -use weaveffi_model::model::Model; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use super::build::{build_project, library_name, model_from_binaries, resolve_platforms}; +use super::Locate; +use crate::package::{write_artifact, PackageContext, Skips}; +use crate::platform::{BinarySet, Platform}; /// Options for [`cmd_package`]. -pub(crate) struct PackageArgs<'a> { - pub(crate) input: Option<&'a str>, - pub(crate) out: Option<&'a str>, - pub(crate) targets: Option<&'a str>, - pub(crate) config: Option<&'a str>, - pub(crate) binaries: Option<&'a str>, - pub(crate) platforms: Option<&'a str>, - pub(crate) debug: bool, - pub(crate) manifest_path: Option<&'a str>, - pub(crate) warn: bool, - pub(crate) quiet: bool, +pub struct PackageArgs<'a> { + /// Where the project is (`--library` doesn't apply). + pub locate: Locate<'a>, + /// `--out`. + pub out: Option<&'a str>, + /// `--target`. + pub targets: Option<&'a [String]>, + /// `--binaries`. + pub binaries: Option<&'a str>, + /// `--platforms`. + pub platforms: Option<&'a str>, + /// `--manifest-path`. + pub manifest_path: Option<&'a str>, + /// `--warn`. + pub warn: bool, + /// `--strict`: fail when an artifact is skipped. + pub strict: bool, } -pub(crate) fn cmd_package(args: &PackageArgs<'_>) -> Result<()> { - let quiet = args.quiet; - let project = Project::locate(args.config, args.input, None)?.quiet(quiet); +/// Run `weaveffi package`. +pub fn cmd_package(args: &PackageArgs<'_>) -> Result { + let quiet = args.locate.quiet; + let project = args.locate.project()?; let config = &project.config; - let selected = config.select_targets(args.targets)?; + let selected = config.targets(args.targets)?; if selected.is_empty() { bail!("no targets selected to package"); } - let settings = config.build.settings(args.debug); + let settings = config.build.settings(args.locate.profile); + let skips = Skips::default(); let (binaries, model) = match args.binaries { Some(dir) => { @@ -53,8 +59,7 @@ pub(crate) fn cmd_package(args: &PackageArgs<'_>) -> Result<()> { None => None, }; let (library, model) = library_name(&project, args.warn)?; - let binaries = BinarySet::read_dir(Utf8Path::new(dir), &library, platforms.as_deref()) - .map_err(|e| miette!("{e:#}"))?; + let binaries = BinarySet::read_dir(Utf8Path::new(dir), &library, platforms.as_deref())?; let model = match model { Some(model) => model, None => model_from_binaries(&project, &binaries, args.warn)?, @@ -70,6 +75,7 @@ pub(crate) fn cmd_package(args: &PackageArgs<'_>) -> Result<()> { &settings, args.manifest_path, args.warn, + &skips, )?; (built.binaries, built.model) } @@ -85,60 +91,41 @@ pub(crate) fn cmd_package(args: &PackageArgs<'_>) -> Result<()> { println!("Packaging `{library}` for {} into {dist}", plats.join(", ")); } - let xcframework = if selected.iter().any(|t| t.name() == "swift") { - assemble_xcframework(&project, &model, &binaries, &dist, quiet)? - } else { - None - }; let ctx = PackageContext { binaries: &binaries, macos_deployment_target: &settings.macos_deployment_target, - ios_deployment_target: &settings.ios_deployment_target, - xcframework: xcframework.as_ref(), + skips: Some(&skips), }; - let mut written = 0usize; - let mut skipped = Vec::new(); - let mut failures = Vec::new(); + let mut empty = Vec::new(); for target in &selected { - let artifacts = target.package(&model, &ctx).unwrap_or_default(); + let skipped_before = skips.list().len(); + let artifacts = target.package(&model, &ctx)?; if artifacts.is_empty() { - skipped.push(target.name()); - continue; - } - if target.name() == "swift" { - if let Some(archive) = &xcframework { - if !quiet { - println!(" swift: swift/{}", archive.file_name); - } + if skips.list().len() == skipped_before { + empty.push(target.name()); } + continue; } for artifact in &artifacts { - let path = write_artifact(&dist, artifact).map_err(|e| miette!("{e:#}"))?; + let path = write_artifact(&dist, artifact)?; written += 1; if !quiet { println!(" {}: {}", target.name(), relative(&dist, &path)); } } - if target.name() == "dotnet" { - match dotnet_pack(&dist, &artifacts) { - Ok(nupkgs) => { - for nupkg in nupkgs { - if !quiet { - println!(" dotnet: dotnet/{}", nupkg.file_name().unwrap_or_default()); - } - } - } - Err(e) => failures.push(format!("{e}")), + for extra in target.finish_package(&dist, &artifacts, &ctx)? { + if !quiet { + println!(" {}: {extra}", target.name()); } } } - if !skipped.is_empty() && !quiet { + if !empty.is_empty() && !quiet { eprintln!( "note: no artifacts for {}: none of the built platforms ({}) is one they ship. \ Run `weaveffi generate` for their source bindings.", - skipped.join(", "), + empty.join(", "), binaries .platforms() .map(Platform::id) @@ -146,218 +133,18 @@ pub(crate) fn cmd_package(args: &PackageArgs<'_>) -> Result<()> { .join(", ") ); } - if !failures.is_empty() { - bail!("{}", failures.join("\n")); - } if written == 0 { bail!( "none of the selected targets produced an artifact: build a platform each one ships \ (with --platforms)" ); } - Ok(()) + Ok(super::finish(&skips, args.strict)) } /// `path` relative to `dist`, for progress lines. -fn relative(dist: &Utf8Path, path: &Utf8Path) -> String { +fn relative(dist: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf { path.strip_prefix(dist) - .map(|p| p.to_string()) - .unwrap_or_else(|_| path.to_string()) -} - -/// Run `dotnet pack` on the project directory the .NET target wrote, -/// returning the `.nupkg` files it produced under `dist/dotnet/`. -fn dotnet_pack(dist: &Utf8Path, artifacts: &[Artifact]) -> Result> { - let mut produced = Vec::new(); - for artifact in artifacts { - let Some(csproj) = artifact - .files - .iter() - .find(|f| f.path.extension() == Some("csproj")) - else { - continue; - }; - let project = dist.join(&artifact.path).join(&csproj.path); - let out = std::path::absolute(dist.join("dotnet").as_std_path()) - .ok() - .and_then(|p| Utf8PathBuf::from_path_buf(p).ok()) - .unwrap_or_else(|| dist.join("dotnet")); - // `PackageOutputPath` rather than `--output`, which some SDKs - // mis-forward when a project path is given. - let Ok(output) = Command::new("dotnet") - .args(["pack", project.as_str(), "--configuration", "Release"]) - .arg(format!("-p:PackageOutputPath={out}")) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - else { - bail!( - "the .NET SDK isn't installed (no `dotnet` on PATH), so the NuGet package wasn't \ - built; install the SDK and rerun, or run `dotnet pack {project} -c Release`" - ); - }; - if !output.status.success() { - bail!( - "`dotnet pack {project}` failed:\n{}{}", - String::from_utf8_lossy(&output.stdout).trim_end(), - String::from_utf8_lossy(&output.stderr).trim_end() - ); - } - let stem = csproj.path.file_stem().unwrap_or_default(); - for entry in out.read_dir_utf8().into_diagnostic()? { - let path = entry.into_diagnostic()?.path().to_path_buf(); - if path.extension() == Some("nupkg") - && path - .file_name() - .is_some_and(|n| n.starts_with(&format!("{stem}."))) - { - produced.push(path); - } - } - } - Ok(produced) -} - -/// Fuse the Apple static libraries into `C{Module}.xcframework`, zip it into -/// `dist/swift/C{Module}.xcframework.zip`, and write its SHA-256 checksum -/// next to it, returning the archive's name and checksum for the Swift -/// package's binary target. -/// -/// Slices are grouped the way `xcodebuild -create-xcframework` needs them -/// (iOS device, iOS simulator, macOS), with the architectures of a group -/// fused by `lipo`. Returns `None` (with a note) when there are no Apple -/// static libraries or the host isn't macOS. -fn assemble_xcframework( - project: &Project, - model: &Model, - binaries: &BinarySet, - dist: &Utf8Path, - quiet: bool, -) -> Result> { - let static_lib = |p: Platform| binaries.get(p).and_then(|nb| nb.staticlib.clone()); - let any_apple = binaries - .platforms() - .any(|p| matches!(p.os(), Os::MacOs | Os::Ios) && static_lib(p).is_some()); - if !any_apple { - if !quiet { - eprintln!( - "note: the Swift package needs an Apple platform (darwin-*, ios-*) built with \ - `weaveffi build`, which adds the static library its XCFramework is made from" - ); - } - return Ok(None); - } - if !cfg!(target_os = "macos") { - eprintln!("warning: skipping the Swift XCFramework: xcodebuild needs macOS"); - return Ok(None); - } - let library = &model.identity.library; - let c_module = project.config.generators.swift.c_module_name(model); - - let scratch = tempfile::tempdir().into_diagnostic()?; - let scratch = Utf8Path::from_path(scratch.path()) - .ok_or_else(|| miette!("temp directory path is not valid UTF-8"))? - .to_path_buf(); - let headers = scratch.join("Headers"); - std::fs::create_dir_all(headers.as_std_path()).into_diagnostic()?; - let header = format!("{library}.h"); - std::fs::write( - headers.join(&header).as_std_path(), - weaveffi_cli::targets::c::render_c_header_from_model(model, &header), - ) - .into_diagnostic()?; - std::fs::write( - headers.join("module.modulemap").as_std_path(), - format!("module {c_module} {{\n header \"{header}\"\n export *\n}}\n"), - ) - .into_diagnostic()?; - - let mut args: Vec = vec!["-create-xcframework".into()]; - for (group, platforms) in [ - ("ios", &[Platform::IosArm64][..]), - ( - "ios-simulator", - &[Platform::IosSimArm64, Platform::IosSimX64][..], - ), - ("macos", &[Platform::MacosArm64, Platform::MacosX64][..]), - ] { - let libs: Vec = platforms.iter().filter_map(|p| static_lib(*p)).collect(); - let lib = match libs.as_slice() { - [] => continue, - [one] => one.clone(), - many => { - let fat = scratch.join(group).join(format!("lib{library}.a")); - std::fs::create_dir_all(scratch.join(group).as_std_path()).into_diagnostic()?; - let status = Command::new("lipo") - .arg("-create") - .args(many.iter().map(|p| p.as_str())) - .args(["-output", fat.as_str()]) - .status() - .into_diagnostic() - .wrap_err("failed to run lipo")?; - if !status.success() { - bail!("lipo failed to fuse the {group} slices"); - } - fat - } - }; - args.extend(["-library".into(), lib.to_string()]); - args.extend(["-headers".into(), headers.to_string()]); - } - let framework_name = format!("{c_module}.xcframework"); - let framework = scratch.join(&framework_name); - args.extend(["-output".into(), framework.to_string()]); - let output = Command::new("xcodebuild") - .args(&args) - .output() - .into_diagnostic() - .wrap_err("failed to run xcodebuild (is Xcode installed?)")?; - if !output.status.success() { - bail!( - "xcodebuild -create-xcframework failed:\n{}", - String::from_utf8_lossy(&output.stderr).trim_end() - ); - } - - let mut files = Vec::new(); - collect_files(&framework, &framework_name, &mut files)?; - let entries = archive::entries(&files, None).map_err(|e| miette!("{e:#}"))?; - let zip = archive::zip(&entries).map_err(|e| miette!("{e:#}"))?; - let checksum = archive::sha256_hex(&zip); - let file_name = format!("{framework_name}.zip"); - let swift_dist = dist.join("swift"); - std::fs::create_dir_all(swift_dist.as_std_path()).into_diagnostic()?; - std::fs::write(swift_dist.join(&file_name).as_std_path(), &zip) - .into_diagnostic() - .wrap_err("failed to write the XCFramework archive")?; - std::fs::write( - swift_dist.join(format!("{file_name}.sha256")).as_std_path(), - format!("{checksum} {file_name}\n"), - ) - .into_diagnostic()?; - Ok(Some(XcframeworkArchive { - file_name, - checksum, - })) -} - -/// Every file under `dir`, in sorted order, as an archive entry under -/// `prefix/`. -fn collect_files(dir: &Utf8Path, prefix: &str, out: &mut Vec) -> Result<()> { - let mut entries: Vec = dir - .read_dir_utf8() - .into_diagnostic()? - .map(|e| e.map(|e| e.path().to_path_buf())) - .collect::>() - .into_diagnostic()?; - entries.sort(); - for path in entries { - let name = format!("{prefix}/{}", path.file_name().unwrap_or_default()); - if path.is_dir() { - collect_files(&path, &name, out)?; - } else { - out.push(PackagedFile::copy(name, path)); - } - } - Ok(()) + .map(Utf8Path::to_path_buf) + .unwrap_or_else(|_| path.to_path_buf()) } diff --git a/crates/weaveffi-cli/src/commands/validate.rs b/crates/weaveffi-cli/src/commands/validate.rs index 83f590d7..35c4efdc 100644 --- a/crates/weaveffi-cli/src/commands/validate.rs +++ b/crates/weaveffi-cli/src/commands/validate.rs @@ -1,10 +1,13 @@ //! `weaveffi validate`: schema validation with human-readable or `--format //! json` output, plus advisory warnings under `--warn`. +use std::process::ExitCode; + use miette::{Report, Result}; use weaveffi_model::validate::{ValidationError, ValidationWarning}; -pub(crate) fn cmd_validate(locate: &super::Locate<'_>, warn: bool, json_mode: bool) -> Result<()> { +/// Run `weaveffi validate`, printing one JSON object with `json_mode`. +pub fn cmd_validate(locate: &super::Locate<'_>, warn: bool, json_mode: bool) -> Result { let quiet = locate.quiet; let definition = locate.project()?.definition()?; match definition.validate() { @@ -35,7 +38,7 @@ pub(crate) fn cmd_validate(locate: &super::Locate<'_>, warn: bool, json_mode: bo println!("Validation passed"); println!(" {}", counts.summary()); } - Ok(()) + Ok(ExitCode::SUCCESS) } Err(diags) => { if json_mode { @@ -48,7 +51,7 @@ pub(crate) fn cmd_validate(locate: &super::Locate<'_>, warn: bool, json_mode: bo .collect::>(), }); println!("{json}"); - std::process::exit(1); + return Ok(ExitCode::FAILURE); } Err(Report::new(diags)) } @@ -82,7 +85,7 @@ impl Counts { self.callback_interfaces += m.callback_interfaces.len(); self.records += m.structs.len(); self.enums += m.enums.len(); - self.error_domains += usize::from(m.errors.is_some()); + self.error_domains += m.errors.len(); self.add(&m.modules); } } diff --git a/crates/weaveffi-cli/src/config.rs b/crates/weaveffi-cli/src/config.rs index 65b4c7ac..3d0ad438 100644 --- a/crates/weaveffi-cli/src/config.rs +++ b/crates/weaveffi-cli/src/config.rs @@ -1,6 +1,4 @@ -//! Project configuration: `weaveffi.toml` discovery and parsing, the -//! `cli_targets!` registry, and the per-target [`Targets`] struct it -//! generates. +//! Project configuration: `weaveffi.toml` discovery and parsing. //! //! A project's identity, inputs, build settings, and generator options live //! in one TOML file at the project root: @@ -28,21 +26,13 @@ //! table, `weaveffi generate` (no arguments) works from any directory inside //! the project, like `cargo build`. With an explicit input, the CLI finds the //! nearest `weaveffi.toml` at or above the input's directory; `--config -//! ` names one explicitly. +//! ` names one explicitly. Each `[generators.]` table is the +//! configuration of one target in the [`REGISTRY`]. + +use std::collections::BTreeMap; use crate::build::BuildSettings; -use crate::codegen::{ConfiguredBackend, Target}; -use crate::targets::c::{CConfig, CGenerator}; -use crate::targets::cpp::{CppConfig, CppGenerator}; -use crate::targets::dart::{DartConfig, DartGenerator}; -use crate::targets::dotnet::{DotnetConfig, DotnetGenerator}; -use crate::targets::go::{GoConfig, GoGenerator}; -use crate::targets::kotlin::{KotlinConfig, KotlinGenerator}; -use crate::targets::node::{NodeConfig, NodeGenerator}; -use crate::targets::python::{PythonConfig, PythonGenerator}; -use crate::targets::ruby::{RubyConfig, RubyGenerator}; -use crate::targets::swift::{SwiftConfig, SwiftGenerator}; -use crate::targets::wasm::{WasmConfig, WasmGenerator}; +use crate::targets::{self, Target, REGISTRY}; use camino::{Utf8Path, Utf8PathBuf}; use miette::{IntoDiagnostic, Result, WrapErr}; use serde::{Deserialize, Deserializer}; @@ -51,66 +41,6 @@ use weaveffi_model::pkg::Package; /// The file name `weaveffi generate` looks for when `--config` is absent. pub const CONFIG_FILE_NAME: &str = "weaveffi.toml"; -/// Declarative registry of every language target the CLI can drive. -/// -/// This macro is the **single source of truth** for the target set. One -/// invocation (below) expands to: -/// * the [`Targets`] struct: one typed field per target, deserialized from -/// the `[generators.]` tables; -/// * `build`: the ordered, object-safe target list. -/// -/// Adding a language is therefore a one-line edit here (plus the generator -/// crate and its workspace dependency), keeping WeaveFFI's promise that a new -/// platform is a code-gen change, not a rewrite. -/// -/// Per-entry syntax: `"" => : via `. -macro_rules! cli_targets { - ( - $( $key:literal => $field:ident : $cfg:ident via $gen:ident ),* $(,)? - ) => { - /// Per-target generator options, one field per registered language. - /// Each field is the generator crate's own `Config` type, so adding a - /// knob to a generator only requires changing that generator's crate. - /// Generated by `cli_targets!`. - #[derive(Debug, Default, Clone, Deserialize)] - #[serde(default, deny_unknown_fields)] - pub struct Targets { - $( - #[doc = concat!("The `[generators.", $key, "]` table.")] - pub $field: $cfg, - )* - } - - impl Targets { - /// Every registered target name, in registry order. - pub const NAMES: &'static [&'static str] = &[ $( $key, )* ]; - - /// Build the heterogeneous list of object-safe targets that the - /// orchestrator and dry-run paths share. Order matches the entry - /// order in the `cli_targets!` invocation for stable output. - pub fn build(&self) -> Vec> { - vec![ - $( Box::new(ConfiguredBackend::new($gen, self.$field.clone())), )* - ] - } - } - }; -} - -cli_targets! { - "c" => c: CConfig via CGenerator, - "cpp" => cpp: CppConfig via CppGenerator, - "swift" => swift: SwiftConfig via SwiftGenerator, - "kotlin" => kotlin: KotlinConfig via KotlinGenerator, - "node" => node: NodeConfig via NodeGenerator, - "wasm" => wasm: WasmConfig via WasmGenerator, - "python" => python: PythonConfig via PythonGenerator, - "dotnet" => dotnet: DotnetConfig via DotnetGenerator, - "dart" => dart: DartConfig via DartGenerator, - "go" => go: GoConfig via GoGenerator, - "ruby" => ruby: RubyConfig via RubyGenerator, -} - /// The `[package]` table: the distribution identity shared by every /// generated manifest, plus where `weaveffi package` writes its artifacts. #[derive(Debug, Default, Clone)] @@ -150,8 +80,9 @@ pub struct BuildTable { /// The platform ids to build when `--platforms` isn't given (default: /// the host). pub platforms: Option>, - /// Build with the release profile. - pub release: bool, + /// The Cargo profile `build` and `package` use when `--profile` isn't + /// given (default `release`). + pub profile: String, /// `MACOSX_DEPLOYMENT_TARGET` for macOS builds. pub macos_deployment_target: String, /// `IPHONEOS_DEPLOYMENT_TARGET` for iOS builds. @@ -166,7 +97,7 @@ impl Default for BuildTable { Self { manifest: None, platforms: None, - release: settings.release, + profile: settings.profile, macos_deployment_target: settings.macos_deployment_target, ios_deployment_target: settings.ios_deployment_target, android_api: settings.android_api, @@ -175,10 +106,10 @@ impl Default for BuildTable { } impl BuildTable { - /// The build settings, in debug when `debug` overrides `release`. - pub fn settings(&self, debug: bool) -> BuildSettings { + /// The build settings, with `profile` (`--profile`) over the table's. + pub(crate) fn settings(&self, profile: Option<&str>) -> BuildSettings { BuildSettings { - release: self.release && !debug, + profile: profile.unwrap_or(&self.profile).to_string(), macos_deployment_target: self.macos_deployment_target.clone(), ios_deployment_target: self.ios_deployment_target.clone(), android_api: self.android_api, @@ -195,9 +126,10 @@ pub struct ProjectTable { /// The API definition: a Rust producer crate (its directory, usually /// `"."`, or its `Cargo.toml`) or an IDL document. pub input: Option, - /// The output directory for `generate` and `diff` (default `bindings`). + /// The output directory for `generate` (default `bindings`). pub out: Option, - /// The targets to generate when `--target` is not given (default: all). + /// The targets to generate when `--target` is not given (default: every + /// default target). pub targets: Option>, } @@ -212,8 +144,9 @@ pub struct ProjectConfig { pub package: PackageTable, /// The `[build]` table. pub build: BuildTable, - /// The `[generators.]` tables. - pub generators: Targets, + /// The `[generators.]` tables, by target name. Each is checked + /// against its target's configuration when the file is loaded. + pub generators: BTreeMap, /// Where the config was loaded from, when it came from a file. #[serde(skip)] pub source: Option, @@ -252,6 +185,17 @@ impl ProjectConfig { let mut cfg: Self = toml::from_str(&contents) .into_diagnostic() .wrap_err_with(|| format!("failed to parse config file: {path}"))?; + for (name, table) in &cfg.generators { + let Some(desc) = targets::find(name) else { + miette::bail!( + "failed to parse config file: {path}: unknown target `{name}` in \ + [generators.{name}]; expected any of: {}", + targets::names().collect::>().join(", ") + ); + }; + desc.build(table.clone()) + .wrap_err_with(|| format!("failed to parse config file: {path}"))?; + } cfg.source = Some(path.to_path_buf()); Ok(cfg) } @@ -327,54 +271,50 @@ impl ProjectConfig { } } - /// Build every registered target, keeping only those named in the - /// comma-separated `filter`, else those listed in `[project] targets`, - /// else all of them. + /// Build the targets named in `filter` (`--target`), else those listed + /// in `[project] targets`, else every default target, in registry order, + /// each with its `[generators.]` configuration. /// /// # Errors /// - /// Returns an error naming any filter entry that is not a registered - /// target, so a typo in `--target` fails instead of silently generating - /// nothing. - pub fn select_targets(&self, filter: Option<&str>) -> Result>> { - let wanted: Option> = match filter { - Some(t) => Some( - t.split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .collect(), - ), - None => self - .project - .targets - .as_ref() - .map(|ts| ts.iter().map(String::as_str).collect()), - }; + /// Returns an error naming any entry that isn't a registered target, so + /// a typo fails instead of silently generating nothing, or when a + /// target's configuration doesn't parse. + pub fn targets(&self, filter: Option<&[String]>) -> Result>> { + let wanted: Option> = filter.or(self.project.targets.as_deref()).map(|names| { + names + .iter() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .collect() + }); if let Some(names) = &wanted { let unknown: Vec<&str> = names .iter() .copied() - .filter(|n| !Targets::NAMES.contains(n)) + .filter(|n| targets::find(n).is_none()) .collect(); if !unknown.is_empty() { miette::bail!( "unknown target(s): {}; expected any of: {}", unknown.join(", "), - Targets::NAMES.join(", ") + targets::names().collect::>().join(", ") ); } } - Ok(self - .generators - .build() - .into_iter() - .filter(|g| wanted.as_ref().is_none_or(|ts| ts.contains(&g.name()))) - .collect()) + REGISTRY + .iter() + .filter(|d| match &wanted { + Some(names) => names.contains(&d.name), + None => d.default, + }) + .map(|d| d.build(self.generators.get(d.name).cloned().unwrap_or_default())) + .collect() } } /// The current directory as a UTF-8 path. -fn current_dir() -> std::io::Result { +pub(crate) fn current_dir() -> std::io::Result { Utf8PathBuf::from_path_buf(std::env::current_dir()?) .map_err(|_| std::io::Error::other("current directory is not valid UTF-8")) } @@ -421,21 +361,11 @@ mod tests { "dist = \"out/dist\"\n", "[build]\n", "platforms = [\"darwin-arm64\", \"ios-arm64\"]\n", - "release = false\n", + "profile = \"dev\"\n", "macos_deployment_target = \"12.0\"\n", "android_api = 24\n", - "[generators.swift]\n", - "name = \"MyApp\"\n", - "min_ios = \"15.0\"\n", - "[generators.kotlin]\n", - "name = \"com.example.myapp\"\n", - "min_sdk = 24\n", - "[generators.python]\n", - "requires_python = \">=3.10\"\n", - "[generators.node]\n", - "node_engine = \">=20\"\n", - "[generators.dart]\n", - "sdk = \">=3.5.0 <4.0.0\"\n", + "[generators.c]\n", + "buffer_helpers = false\n", ), ); let cfg = ProjectConfig::from_file(&cfg_path).unwrap(); @@ -451,37 +381,30 @@ mod tests { cfg.build.platforms.as_deref(), Some(&["darwin-arm64".to_string(), "ios-arm64".to_string()][..]) ); - let settings = cfg.build.settings(false); - assert!(!settings.release); + let settings = cfg.build.settings(None); + assert_eq!(settings.profile, "dev"); + assert_eq!(cfg.build.settings(Some("bench")).profile, "bench"); assert_eq!(settings.macos_deployment_target, "12.0"); assert_eq!(settings.ios_deployment_target, "13.0"); assert_eq!(settings.android_api, 24); - assert_eq!(cfg.generators.swift.name.as_deref(), Some("MyApp")); - assert_eq!(cfg.generators.swift.min_ios, "15.0"); - assert_eq!(cfg.generators.swift.min_macos, "11.0"); - assert_eq!(cfg.generators.kotlin.package(), Some("com.example.myapp")); assert_eq!( - ( - cfg.generators.kotlin.min_sdk, - cfg.generators.kotlin.compile_sdk - ), - (24, 35) + cfg.generators["c"] + .get("buffer_helpers") + .and_then(toml::Value::as_bool), + Some(false) ); - assert_eq!(cfg.generators.python.requires_python, ">=3.10"); - assert_eq!(cfg.generators.node.node_engine, ">=20"); - assert_eq!(cfg.generators.dart.sdk, ">=3.5.0 <4.0.0"); + assert_eq!(cfg.targets(None).unwrap().len(), REGISTRY.len()); assert_eq!(cfg.source.as_deref(), Some(cfg_path.as_path())); } #[test] fn build_and_package_defaults() { let cfg = ProjectConfig::default(); - let settings = cfg.build.settings(false); - assert!(settings.release); + let settings = cfg.build.settings(None); + assert_eq!(settings.profile, "release"); assert_eq!(settings.macos_deployment_target, "11.0"); assert_eq!(settings.ios_deployment_target, "13.0"); assert_eq!(settings.android_api, 21); - assert!(!cfg.build.settings(true).release); assert_eq!(cfg.dist_dir(None), Utf8PathBuf::from("dist")); assert_eq!(cfg.out_dir(None), Utf8PathBuf::from("bindings")); } @@ -493,6 +416,9 @@ mod tests { ("[global]\npre_generate = \"true\"\n", "global"), ("[package]\nname = \"x\"\ndistdir = \"d\"\n", "distdir"), ("[build]\nplatform = [\"linux-x64\"]\n", "platform"), + ("[build]\nrelease = true\n", "release"), + ("[generators.rust]\nname = \"x\"\n", "`rust`"), + ("[generators.c]\nprefix = \"x\"\n", "prefix"), ("[generators.swift]\nmodule_name = \"X\"\n", "module_name"), ("[generators.kotlin]\npackage = \"x\"\n", "package"), ("[generators.node]\npackage_name = \"x\"\n", "package_name"), @@ -544,7 +470,7 @@ mod tests { CONFIG_FILE_NAME, "[package]\nname = \"found\"\n", ); - let input = write(&nested, "api.yml", "version: \"0.11.0\"\nmodules: []\n"); + let input = write(&nested, "api.yml", "version: \"0.12.0\"\nmodules: []\n"); let cfg = ProjectConfig::load(None, &input).unwrap(); assert_eq!(cfg.package.identity.name.as_deref(), Some("found")); @@ -553,7 +479,7 @@ mod tests { #[test] fn defaults_when_no_config_exists() { let dir = tempfile::tempdir().unwrap(); - let input = write(dir.path(), "api.yml", "version: \"0.11.0\"\nmodules: []\n"); + let input = write(dir.path(), "api.yml", "version: \"0.12.0\"\nmodules: []\n"); let cfg = ProjectConfig::load(None, &input).unwrap(); assert!(cfg.source.is_none(), "unexpected config: {:?}", cfg.source); assert!(cfg.package.identity.name.is_none()); @@ -569,18 +495,16 @@ mod tests { #[test] fn target_filter_rejects_unknown_names() { let cfg = ProjectConfig::default(); - let err = match cfg.select_targets(Some("swift,rustlang")) { + let list = |s: &str| s.split(',').map(str::to_string).collect::>(); + let err = match cfg.targets(Some(&list("c,rustlang"))) { Ok(_) => panic!("unknown target should be rejected"), Err(e) => e, }; assert!(format!("{err}").contains("rustlang"), "{err}"); - let ok = cfg.select_targets(Some("swift, c")).unwrap(); + let ok = cfg.targets(Some(&list(" c"))).unwrap(); let names: Vec<&str> = ok.iter().map(|g| g.name()).collect(); - assert_eq!(names, vec!["c", "swift"]); - assert_eq!( - cfg.select_targets(None).unwrap().len(), - Targets::NAMES.len() - ); + assert_eq!(names, vec!["c"]); + assert_eq!(cfg.targets(None).unwrap().len(), REGISTRY.len()); } #[test] @@ -589,22 +513,22 @@ mod tests { let cfg_path = write( dir.path(), CONFIG_FILE_NAME, - "[project]\ninput = \"api.yml\"\nout = \"bindings\"\ntargets = [\"c\", \"python\"]\n", + "[project]\ninput = \"api.yml\"\nout = \"bindings\"\ntargets = [\"c\"]\n", ); - write(dir.path(), "api.yml", "version: \"0.11.0\"\nmodules: []\n"); + write(dir.path(), "api.yml", "version: \"0.12.0\"\nmodules: []\n"); let (cfg, input) = ProjectConfig::locate(Some(cfg_path.as_str()), None).unwrap(); let root = cfg_path.parent().unwrap(); assert_eq!(input, root.join("api.yml")); assert_eq!(cfg.out_dir(None), root.join("bindings")); assert_eq!(cfg.out_dir(Some("x")), Utf8PathBuf::from("x")); let names: Vec<&str> = cfg - .select_targets(None) + .targets(None) .unwrap() .iter() .map(|t| t.name()) .collect(); - assert_eq!(names, ["c", "python"]); - assert_eq!(cfg.select_targets(Some("go")).unwrap().len(), 1); + assert_eq!(names, ["c"]); + assert_eq!(cfg.targets(Some(&["c".to_string()])).unwrap().len(), 1); let bare = write(dir.path(), "other.toml", "[package]\nname = \"x\"\n"); let err = ProjectConfig::locate(Some(bare.as_str()), None).unwrap_err(); diff --git a/crates/weaveffi-cli/src/lib.rs b/crates/weaveffi-cli/src/lib.rs index dfd7570f..2b418199 100644 --- a/crates/weaveffi-cli/src/lib.rs +++ b/crates/weaveffi-cli/src/lib.rs @@ -1,13 +1,12 @@ //! The library behind the `weaveffi` command-line tool: the //! [`Project`](project::Project) that locates a project and loads its API -//! (from an IDL, or from a Rust producer's built [`library`]), the -//! `weaveffi.toml` [`config`], the [`LanguageBackend`](backend::LanguageBackend) -//! trait, the code-generation [`Orchestrator`](codegen::Orchestrator), the -//! shared C ABI declaration renderer, the eleven language generators under -//! [`targets`], and the [`build`] and [`package`] layers that turn a producer -//! crate into installable per-ecosystem artifacts. +//! (from an IDL, or from a Rust producer's built library), the +//! `weaveffi.toml` [`config`], the language [`targets`] with their +//! [`REGISTRY`](targets::REGISTRY), the code-generation +//! [`Orchestrator`](codegen::Orchestrator), and the [`package`] artifacts +//! `weaveffi package` writes. //! -//! Every generator renders from the validated +//! Every target renders from the validated //! [`Model`](weaveffi_model::model::Model) alone, so symbol names and //! parameter lowering are computed once and shared. #![deny(missing_docs)] @@ -15,19 +14,20 @@ #![warn(clippy::missing_panics_doc)] #![warn(clippy::doc_markdown)] -pub mod backend; -pub mod build; -pub mod cabi; -pub mod cache; -pub mod cargo; +pub(crate) mod build; +pub(crate) mod cabi; +pub(crate) mod cargo; pub mod codegen; +#[doc(hidden)] +pub mod commands; pub mod config; -pub mod lang; -pub mod library; -pub mod manifest; +pub(crate) mod lang; +pub(crate) mod library; +pub(crate) mod manifest; pub mod package; -pub mod platform; +pub(crate) mod platform; pub mod project; +pub(crate) mod record; mod report; pub mod targets; -pub mod utils; +pub(crate) mod utils; diff --git a/crates/weaveffi-cli/src/library.rs b/crates/weaveffi-cli/src/library.rs index d1e0e252..17ae68de 100644 --- a/crates/weaveffi-cli/src/library.rs +++ b/crates/weaveffi-cli/src/library.rs @@ -11,8 +11,8 @@ use std::collections::BTreeSet; -use anyhow::{bail, Context, Result}; use camino::Utf8Path; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use object::read::archive::ArchiveFile; use object::{BinaryFormat, Object, ObjectSection, ObjectSymbol, SymbolSection}; use weaveffi_model::meta::{self, Frame}; @@ -26,18 +26,23 @@ use weaveffi_model::meta::{self, Frame}; /// /// Returns an error when the file can't be read or isn't an object file /// `object` understands, or when a frame is malformed. -pub fn read_frames(path: &Utf8Path) -> Result> { - let data = std::fs::read(path).with_context(|| format!("failed to read {path}"))?; +pub(crate) fn read_frames(path: &Utf8Path) -> Result> { + let data = std::fs::read(path) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {path}"))?; let mut frames = Vec::new(); if let Ok(archive) = ArchiveFile::parse(&*data) { for member in archive.members() { - let member = member.with_context(|| format!("{path} is a malformed archive"))?; + let member = member + .into_diagnostic() + .wrap_err_with(|| format!("{path} is a malformed archive"))?; let bytes = member .data(&*data) - .with_context(|| format!("{path} is a malformed archive"))?; + .into_diagnostic() + .wrap_err_with(|| format!("{path} is a malformed archive"))?; // Archives also carry symbol tables and other non-object members. if let Ok(file) = object::File::parse(bytes) { - frames.extend(object_frames(&file).with_context(|| { + frames.extend(object_frames(&file).wrap_err_with(|| { format!( "{path}({}) has malformed WeaveFFI metadata", String::from_utf8_lossy(member.name()) @@ -48,15 +53,16 @@ pub fn read_frames(path: &Utf8Path) -> Result> { return Ok(frames); } let file = object::File::parse(&*data) - .with_context(|| format!("{path} isn't a library WeaveFFI can read"))?; - object_frames(&file).with_context(|| format!("{path} has malformed WeaveFFI metadata")) + .into_diagnostic() + .wrap_err_with(|| format!("{path} isn't a library WeaveFFI can read"))?; + object_frames(&file).wrap_err_with(|| format!("{path} has malformed WeaveFFI metadata")) } /// The frames in one object file. fn object_frames(file: &object::File<'_>) -> Result> { if file.format() == BinaryFormat::Wasm { return match file.section_by_name(meta::SECTION) { - Some(section) => Ok(meta::decode(section.data()?)?), + Some(section) => meta::decode(section.data().into_diagnostic()?).into_diagnostic(), None => Ok(Vec::new()), }; } @@ -90,11 +96,11 @@ fn object_frames(file: &object::File<'_>) -> Result> { let Some(bytes) = data_at(file, address, section) else { bail!("the data of `{name}` isn't in the file"); }; - let len = meta::frame_len(bytes)?; + let len = meta::frame_len(bytes).into_diagnostic()?; let Some(frame) = bytes.get(..len) else { bail!("`{name}` is truncated"); }; - frames.extend(meta::decode(frame)?); + frames.extend(meta::decode(frame).into_diagnostic()?); } Ok(frames) } @@ -120,7 +126,7 @@ fn data_at<'d>( /// The base name a library file is loaded by: `libkv.so`, `libkv.dylib`, /// `libkv.a`, `kv.dll`, `kv.lib`, and `kv.wasm` are all `kv`. #[must_use] -pub fn library_name(path: &Utf8Path) -> String { +pub(crate) fn library_name(path: &Utf8Path) -> String { let stem = path.file_stem().unwrap_or_default(); match path.extension() { Some("so" | "dylib" | "a") => stem.strip_prefix("lib").unwrap_or(stem).to_string(), diff --git a/crates/weaveffi-cli/src/main.rs b/crates/weaveffi-cli/src/main.rs index 7d186932..4f1857ce 100644 --- a/crates/weaveffi-cli/src/main.rs +++ b/crates/weaveffi-cli/src/main.rs @@ -1,14 +1,17 @@ -//! `weaveffi` command-line entry point: `clap` definitions and dispatch. +//! `weaveffi` command-line entry point: `clap` definitions, dispatch, and +//! error rendering. //! -//! Each subcommand's implementation lives in `commands`, on top of the -//! library's `project` (locating a project and loading its API) and -//! `config` (`weaveffi.toml` and the generator registry). +//! Each subcommand's implementation lives in the library's `commands` +//! module and returns the process exit code; errors come back as `miette` +//! reports, which only this binary renders. -mod commands; +use std::process::ExitCode; -use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; -use commands::Locate; +use clap::builder::{PossibleValue, PossibleValuesParser}; +use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; use miette::{IntoDiagnostic, Result, WrapErr}; +use weaveffi_cli::commands::{self, extract::IdlFormat, Locate}; +use weaveffi_cli::targets::REGISTRY; use weaveffi_model::ir::CURRENT_SCHEMA_VERSION; const PLATFORMS_HELP: &str = "Comma-separated platform ids: darwin-arm64, darwin-x64, linux-x64, \ @@ -17,12 +20,21 @@ const PLATFORMS_HELP: &str = "Comma-separated platform ids: darwin-arm64, darwin else the host)"; const INPUT_HELP: &str = "A Rust producer crate (its directory or Cargo.toml), whose API is read \ - from its built library, or an IDL document (yaml|yml|json|toml); \ + from its built library, or an IDL document (yaml|yml|json); \ defaults to `[project] input` from the nearest weaveffi.toml"; const LIBRARY_HELP: &str = "Read a Rust producer's API from this built library (cdylib, \ staticlib, or .wasm) instead of building the crate"; +const TARGET_HELP: &str = + "Comma-separated targets (default: `[project] targets`, else every target)"; + +const DEV_PROFILE_HELP: &str = + "The Cargo profile to build a Rust producer's library with (default: dev)"; + +const RELEASE_PROFILE_HELP: &str = + "The Cargo profile to build with (default: `[build] profile`, else release)"; + #[derive(Parser, Debug)] #[command( name = "weaveffi", @@ -37,6 +49,39 @@ struct Cli { command: Commands, } +/// Where a command finds its project. +#[derive(Args, Debug)] +struct LocateArgs { + #[arg(help = INPUT_HELP)] + input: Option, + /// Path to weaveffi.toml (default: the nearest one at or above the input) + #[arg(long)] + config: Option, +} + +/// `--target`: registered target names, validated and listed by `clap`. +#[derive(Args, Debug)] +struct TargetArgs { + #[arg( + short, + long = "target", + value_name = "TARGETS", + value_delimiter = ',', + value_parser = target_names(), + help = TARGET_HELP + )] + targets: Option>, +} + +/// Every registered target, with its description, for `--target`. +fn target_names() -> PossibleValuesParser { + PossibleValuesParser::new( + REGISTRY + .iter() + .map(|d| PossibleValue::new(d.name).help(d.description)), + ) +} + /// Output format for `validate`. #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] enum ReportFormat { @@ -46,17 +91,6 @@ enum ReportFormat { Json, } -/// An IDL serialization format. -#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] -pub(crate) enum IdlFormat { - /// YAML. - Yaml, - /// JSON. - Json, - /// TOML. - Toml, -} - /// A schema export format. #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] enum SchemaFormat { @@ -80,120 +114,96 @@ enum Commands { }, /// Generate bindings for every (or the selected) language target Generate { - #[arg(help = INPUT_HELP)] - input: Option, + #[command(flatten)] + locate: LocateArgs, + #[command(flatten)] + targets: TargetArgs, /// Output directory (default: `[project] out`, else ./bindings) #[arg(short, long)] out: Option, - /// Comma-separated targets: c, cpp, swift, kotlin, node, wasm, python, dotnet, dart, go, ruby (default: `[project] targets`, else all) - #[arg(short, long)] - target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, + #[arg(long, help = LIBRARY_HELP)] + library: Option, + #[arg(long, value_name = "NAME", help = DEV_PROFILE_HELP)] + profile: Option, /// Print advisory lints after validation #[arg(long)] warn: bool, - /// Validate and list the files that would be written, without writing - #[arg(long)] + /// List the files the targets render, without writing + #[arg(long, conflicts_with_all = ["check", "diff"])] dry_run: bool, - #[arg(long, help = LIBRARY_HELP)] - library: Option, - /// Build a Rust producer's library in release mode + /// Write nothing; list the files that would change and exit 1 if any would #[arg(long)] - release: bool, + check: bool, + /// Write nothing; print a unified diff of what would change + #[arg(long)] + diff: bool, }, - /// Build a Rust producer's debug library, generate, and point the bindings at it + /// Build a Rust producer's library, generate, and point the bindings at it Dev { - #[arg(help = INPUT_HELP)] - input: Option, + #[command(flatten)] + locate: LocateArgs, + #[command(flatten)] + targets: TargetArgs, /// Output directory (default: `[project] out`, else ./bindings) #[arg(short, long)] out: Option, - /// Comma-separated targets (default: `[project] targets`, else all) - #[arg(short, long)] - target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, + #[arg(long, help = LIBRARY_HELP)] + library: Option, + #[arg(long, value_name = "NAME", help = DEV_PROFILE_HELP)] + profile: Option, }, /// Validate an API definition without generating anything Validate { - #[arg(help = INPUT_HELP)] - input: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, + #[command(flatten)] + locate: LocateArgs, + #[arg(long, help = LIBRARY_HELP)] + library: Option, + #[arg(long, value_name = "NAME", help = DEV_PROFILE_HELP)] + profile: Option, /// Also report advisory lints #[arg(long)] warn: bool, /// Output format #[arg(long, value_enum, default_value_t = ReportFormat::Human)] format: ReportFormat, - #[arg(long, help = LIBRARY_HELP)] - library: Option, - /// Build a Rust producer's library in release mode - #[arg(long)] - release: bool, - }, - /// Show how regenerating would change the output directory (writes nothing) - Diff { - #[arg(help = INPUT_HELP)] - input: Option, - /// Output directory to compare against (default: `[project] out`, else ./bindings) - #[arg(short, long)] - out: Option, - /// Comma-separated targets to compare (default: `[project] targets`, else all) - #[arg(short, long)] - target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, - /// Print only a summary and exit 2 if files differ, 3 if files would be added or removed - #[arg(long)] - check: bool, - #[arg(long, help = LIBRARY_HELP)] - library: Option, - /// Build a Rust producer's library in release mode - #[arg(long)] - release: bool, }, /// Cross-compile the Rust producer per platform into target/weaveffi// Build { - #[arg(help = INPUT_HELP)] - input: Option, - /// Comma-separated platform ids (default: `[build] platforms`, else the host) + #[command(flatten)] + locate: LocateArgs, + /// Comma-separated targets whose C glue to prebuild (default: `[project] targets`, + /// else every target) + #[arg( + short, + long = "target", + value_name = "TARGETS", + value_delimiter = ',', + value_parser = target_names() + )] + targets: Option>, #[arg(long, help = PLATFORMS_HELP)] platforms: Option, - /// Comma-separated targets whose C glue to prebuild (default: `[project] targets`, else all) - #[arg(short, long)] - target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, + #[arg(long, value_name = "NAME", help = RELEASE_PROFILE_HELP)] + profile: Option, /// The producer crate's Cargo.toml (default: `[build] manifest`, else the input's crate) #[arg(long)] manifest_path: Option, - /// Build with the dev profile instead of `--release` - #[arg(long)] - debug: bool, /// Print advisory lints after validation #[arg(long)] warn: bool, + /// Exit 1 when an artifact is skipped because a tool is missing + #[arg(long)] + strict: bool, }, /// Build (unless --binaries) and write installable artifacts for each target Package { - #[arg(help = INPUT_HELP)] - input: Option, + #[command(flatten)] + locate: LocateArgs, + #[command(flatten)] + targets: TargetArgs, /// Dist directory for the artifacts (default: `[package] dist`, else ./dist) #[arg(short, long)] out: Option, - /// Comma-separated targets to package (default: `[project] targets`, else all) - #[arg(short, long)] - target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, /// Package existing builds laid out as `//` instead of building #[arg(long)] binaries: Option, @@ -201,51 +211,50 @@ enum Commands { /// --binaries, every platform directory present) #[arg(long, help = PLATFORMS_HELP)] platforms: Option, + #[arg(long, value_name = "NAME", help = RELEASE_PROFILE_HELP)] + profile: Option, /// The producer crate's Cargo.toml (default: `[build] manifest`, else the input's crate) #[arg(long)] manifest_path: Option, - /// Build with the dev profile instead of `--release` - #[arg(long)] - debug: bool, /// Print advisory lints after validation #[arg(long)] warn: bool, + /// Exit 1 when an artifact is skipped because a tool is missing + #[arg(long)] + strict: bool, }, /// Print the IDL a Rust producer's built library embeds Extract { - /// The producer crate (its directory or Cargo.toml); defaults to `[project] input` - input: Option, + #[command(flatten)] + locate: LocateArgs, + #[arg(long, help = LIBRARY_HELP)] + library: Option, + #[arg(long, value_name = "NAME", help = DEV_PROFILE_HELP)] + profile: Option, /// Output file (default: stdout) #[arg(short, long)] output: Option, /// Output format #[arg(short, long, value_enum, default_value_t = IdlFormat::Yaml)] format: IdlFormat, - /// Path to weaveffi.toml (default: the nearest one at or above the input) - #[arg(long)] - config: Option, - #[arg(long, help = LIBRARY_HELP)] - library: Option, - /// Build the library in release mode + }, + /// Print the IDL document schema (or, with --version, its version) + Schema { + /// Print the IDL schema version this build reads and writes instead #[arg(long)] - release: bool, + version: bool, + /// Schema export format + #[arg(long, value_enum, default_value_t = SchemaFormat::JsonSchema)] + format: SchemaFormat, }, /// Print shell completions Completions { /// Shell to generate completions for shell: clap_complete::Shell, }, - /// Print the IDL schema version this build reads and writes - SchemaVersion, - /// Print the IDL document schema - Schema { - /// Schema export format - #[arg(long, value_enum, default_value_t = SchemaFormat::JsonSchema)] - format: SchemaFormat, - }, } -fn main() -> Result<()> { +fn main() -> Result { let _ = miette::set_hook(Box::new(|_| { Box::new( miette::MietteHandlerOpts::new() @@ -254,169 +263,142 @@ fn main() -> Result<()> { .build(), ) })); - let cli = Cli::parse(); - let quiet = cli.quiet; - match cli.command { + run(cli.command, cli.quiet) +} + +/// The [`Locate`] of a command's flags. +fn locate<'a>( + args: &'a LocateArgs, + library: Option<&'a String>, + profile: Option<&'a String>, + quiet: bool, +) -> Locate<'a> { + Locate { + input: args.input.as_deref(), + config: args.config.as_deref(), + library: library.map(String::as_str), + profile: profile.map(String::as_str), + quiet, + } +} + +fn run(command: Commands, quiet: bool) -> Result { + match command { Commands::Init { dir, name, force } => { commands::init::cmd_init(&commands::init::InitArgs { dir: &dir, name: name.as_deref(), force, quiet, - })? + }) } Commands::Generate { - input, + locate: l, + targets, out, - target, - config, + library, + profile, warn, dry_run, - library, - release, + check, + diff, } => commands::generate::cmd_generate(&commands::generate::GenerateArgs { - locate: Locate { - input: input.as_deref(), - config: config.as_deref(), - library: library.as_deref(), - release, - quiet, - }, + locate: locate(&l, library.as_ref(), profile.as_ref(), quiet), out: out.as_deref(), - targets: target.as_deref(), + targets: targets.targets.as_deref(), warn, dry_run, - })?, + check, + diff, + }), Commands::Dev { - input, + locate: l, + targets, out, - target, - config, + library, + profile, } => commands::dev::cmd_dev(&commands::dev::DevArgs { - locate: Locate { - input: input.as_deref(), - config: config.as_deref(), - library: None, - release: false, - quiet, - }, + locate: locate(&l, library.as_ref(), profile.as_ref(), quiet), out: out.as_deref(), - targets: target.as_deref(), - })?, + targets: targets.targets.as_deref(), + }), Commands::Validate { - input, - config, + locate: l, + library, + profile, warn, format, - library, - release, } => commands::validate::cmd_validate( - &Locate { - input: input.as_deref(), - config: config.as_deref(), - library: library.as_deref(), - release, - quiet, - }, + &locate(&l, library.as_ref(), profile.as_ref(), quiet), warn, format == ReportFormat::Json, - )?, - Commands::Diff { - input, - out, - target, - config, - check, - library, - release, - } => commands::diff::cmd_diff(&commands::diff::DiffArgs { - locate: Locate { - input: input.as_deref(), - config: config.as_deref(), - library: library.as_deref(), - release, - quiet, - }, - out: out.as_deref(), - targets: target.as_deref(), - check, - })?, + ), Commands::Build { - input, + locate: l, + targets, platforms, - target, - config, + profile, manifest_path, - debug, warn, + strict, } => commands::build::cmd_build(&commands::build::BuildArgs { - input: input.as_deref(), - config: config.as_deref(), + locate: locate(&l, None, profile.as_ref(), quiet), platforms: platforms.as_deref(), - targets: target.as_deref(), - debug, + targets: targets.as_deref(), manifest_path: manifest_path.as_deref(), warn, - quiet, - })?, + strict, + }), Commands::Package { - input, + locate: l, + targets, out, - target, - config, binaries, platforms, + profile, manifest_path, - debug, warn, + strict, } => commands::package::cmd_package(&commands::package::PackageArgs { - input: input.as_deref(), + locate: locate(&l, None, profile.as_ref(), quiet), out: out.as_deref(), - targets: target.as_deref(), - config: config.as_deref(), + targets: targets.targets.as_deref(), binaries: binaries.as_deref(), platforms: platforms.as_deref(), - debug, manifest_path: manifest_path.as_deref(), warn, - quiet, - })?, + strict, + }), Commands::Extract { - input, + locate: l, + library, + profile, output, format, - config, - library, - release, } => commands::extract::cmd_extract( - &Locate { - input: input.as_deref(), - config: config.as_deref(), - library: library.as_deref(), - release, - quiet, - }, + &locate(&l, library.as_ref(), profile.as_ref(), quiet), output.as_deref(), format, - )?, - Commands::Completions { shell } => cmd_completions(shell), - Commands::SchemaVersion => println!("{CURRENT_SCHEMA_VERSION}"), - Commands::Schema { format } => cmd_schema(format)?, + ), + Commands::Schema { version, format } => cmd_schema(version, format), + Commands::Completions { shell } => { + clap_complete::generate( + shell, + &mut Cli::command(), + "weaveffi", + &mut std::io::stdout(), + ); + Ok(ExitCode::SUCCESS) + } } - Ok(()) -} - -fn cmd_completions(shell: clap_complete::Shell) { - clap_complete::generate( - shell, - &mut Cli::command(), - "weaveffi", - &mut std::io::stdout(), - ); } -fn cmd_schema(format: SchemaFormat) -> Result<()> { +fn cmd_schema(version: bool, format: SchemaFormat) -> Result { + if version { + println!("{CURRENT_SCHEMA_VERSION}"); + return Ok(ExitCode::SUCCESS); + } match format { SchemaFormat::JsonSchema => { let schema = schemars::schema_for!(weaveffi_model::ir::Api); @@ -426,13 +408,18 @@ fn cmd_schema(format: SchemaFormat) -> Result<()> { println!("{json}"); } } - Ok(()) + Ok(ExitCode::SUCCESS) } #[cfg(test)] mod tests { use super::*; + #[test] + fn cli_definition_is_valid() { + Cli::command().debug_assert(); + } + #[test] fn rejects_unknown_human_json_output_formats() { let args = ["weaveffi", "validate", "input.yml", "--format", "jsonn"]; @@ -444,12 +431,25 @@ mod tests { ); } + #[test] + fn targets_are_validated_against_the_registry() { + let cli = Cli::try_parse_from(["weaveffi", "generate", "--target", "c"]).unwrap(); + let Commands::Generate { targets, .. } = cli.command else { + panic!("generate"); + }; + assert_eq!(targets.targets.unwrap(), ["c"]); + let error = Cli::try_parse_from(["weaveffi", "generate", "-t", "c,rustlang"]) + .expect_err("unknown target"); + assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); + assert!(error.to_string().contains("rustlang"), "{error}"); + } + #[test] fn completions_and_schema_version() { for (args, needle) in [ (&["completions", "bash"][..], "complete"), (&["completions", "zsh"][..], "compdef"), - (&["schema-version"][..], CURRENT_SCHEMA_VERSION), + (&["schema", "--version"][..], CURRENT_SCHEMA_VERSION), ] { let cmd = assert_cmd::Command::cargo_bin("weaveffi") .expect("binary not found") diff --git a/crates/weaveffi-cli/src/package/archive.rs b/crates/weaveffi-cli/src/package/archive.rs index 05d048cd..6da21ae4 100644 --- a/crates/weaveffi-cli/src/package/archive.rs +++ b/crates/weaveffi-cli/src/package/archive.rs @@ -11,7 +11,7 @@ use std::borrow::Cow; use std::io::Write as _; -use anyhow::{bail, Context, Result}; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; use sha2::{Digest, Sha256, Sha512}; use crate::package::PackagedFile; @@ -103,11 +103,13 @@ pub fn tar(entries: &[Entry<'_>]) -> Result> { header.set_entry_type(tar::EntryType::Regular); builder .append_data(&mut header, &entry.path, entry.data.as_ref()) - .with_context(|| format!("failed to add {} to a tar archive", entry.path))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to add {} to a tar archive", entry.path))?; } builder .into_inner() - .context("failed to finish a tar archive") + .into_diagnostic() + .wrap_err("failed to finish a tar archive") } /// `bytes` compressed with gzip (with a zeroed header timestamp). @@ -119,8 +121,14 @@ pub fn gzip(bytes: &[u8]) -> Result> { let mut encoder = flate2::GzBuilder::new() .mtime(0) .write(Vec::new(), flate2::Compression::default()); - encoder.write_all(bytes).context("failed to gzip")?; - encoder.finish().context("failed to gzip") + encoder + .write_all(bytes) + .into_diagnostic() + .wrap_err("failed to gzip")?; + encoder + .finish() + .into_diagnostic() + .wrap_err("failed to gzip") } /// A zip archive of `entries`, each deflated, with Unix permissions. @@ -137,8 +145,12 @@ pub fn zip(entries: &[Entry<'_>]) -> Result> { flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default()); encoder .write_all(&entry.data) - .context("failed to deflate")?; - let compressed = encoder.finish().context("failed to deflate")?; + .into_diagnostic() + .wrap_err("failed to deflate")?; + let compressed = encoder + .finish() + .into_diagnostic() + .wrap_err("failed to deflate")?; let crc = crc32fast::hash(&entry.data); let (Ok(size), Ok(packed), Ok(offset), Ok(name_len)) = ( u32::try_from(entry.data.len()), diff --git a/crates/weaveffi-cli/src/package/mod.rs b/crates/weaveffi-cli/src/package/mod.rs index 5475d905..f145a20b 100644 --- a/crates/weaveffi-cli/src/package/mod.rs +++ b/crates/weaveffi-cli/src/package/mod.rs @@ -1,4 +1,4 @@ -//! The packaging layer: the artifacts a backend assembles from a set of +//! The packaging layer: the artifacts a target assembles from a set of //! per-platform builds, and the driver that writes them to the dist //! directory. //! @@ -6,23 +6,23 @@ //! point at a native library themselves. `weaveffi package` produces the //! next artifact up: an installable package for each ecosystem (a wheel, an //! npm tarball, a gem, a SwiftPM `XCFramework` archive, …) with the prebuilt -//! native library for each [`Platform`] inside, so +//! native library for each platform inside, so //! `pip install`, `npm install`, and `gem install` work with no local //! toolchain. //! -//! A backend opts in by overriding -//! [`LanguageBackend::package`](crate::backend::LanguageBackend::package), -//! returning [`Artifact`]s: a directory tree or an archive, each a list of -//! [`PackagedFile`]s. Rendering stays pure (it returns values and does no -//! I/O), so package layouts are testable exactly like generated source, and -//! [`write_artifact`] does the I/O with the writers in [`archive`]. +//! A target opts in by overriding [`Target::package`](crate::targets::Target::package), +//! returning [`Artifact`]s: a directory tree, an archive, or a single file, +//! each a list of [`PackagedFile`]s. Packaging returns values and leaves the +//! writing to `weaveffi package`, so package layouts are testable exactly +//! like generated source. -pub mod archive; +pub(crate) mod archive; use std::borrow::Cow; +use std::sync::Mutex; -use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::{IntoDiagnostic, Result, WrapErr}; use crate::platform::{read_file, BinarySet, Platform}; @@ -125,6 +125,8 @@ pub enum ArtifactKind { Wheel(WheelMeta), /// A RubyGems package: a tar of the gzipped specification and data. Gem(GemSpec), + /// A single file: the contents of the artifact's one entry. + File, } /// One installable artifact a target produces. @@ -164,50 +166,94 @@ impl Artifact { } } + /// A single file at `path` holding `contents`. + pub fn single_file(path: impl Into, contents: Vec) -> Self { + let path = normalize_separators(path.into()); + let name = path.file_name().unwrap_or_default().to_string(); + Self { + path, + kind: ArtifactKind::File, + files: vec![PackagedFile::bytes(name, contents)], + } + } + /// The file at `path` inside this artifact, if any. pub fn file(&self, path: &str) -> Option<&PackagedFile> { self.files.iter().find(|f| f.path == path) } } -/// The prebuilt `XCFramework` archive a Swift package's binary target points -/// at. +/// An artifact skipped because an external tool it needs is missing. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct XcframeworkArchive { - /// The archive's file name (`CKvstore.xcframework.zip`). - pub file_name: String, - /// The SHA-256 checksum of the archive, as `swift package - /// compute-checksum` prints it. - pub checksum: String, +pub(crate) struct Skipped { + /// The artifact (`the NuGet package`, `the Node.js addon for linux-x64`). + pub(crate) what: String, + /// Why it was skipped. + pub(crate) reason: String, +} + +/// The artifacts a `build` or `package` run skipped, collected for the +/// summary at the end of the run. +#[derive(Debug, Default)] +pub(crate) struct Skips(Mutex>); + +impl Skips { + /// Record that `what` was skipped for `reason`, warning about it now. + pub(crate) fn skip(&self, what: impl Into, reason: impl std::fmt::Display) { + let skipped = Skipped { + what: what.into(), + reason: reason.to_string(), + }; + eprintln!("warning: skipping {}: {}", skipped.what, skipped.reason); + self.0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(skipped); + } + + /// Everything skipped so far, in order. + pub(crate) fn list(&self) -> Vec { + self.0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } } -/// Everything a backend's [`package`](crate::backend::LanguageBackend::package) +/// Everything a target's [`package`](crate::targets::Target::package) hook /// needs beyond the [`Model`](weaveffi_model::model::Model) and its own /// configuration. #[derive(Debug, Clone, Copy)] pub struct PackageContext<'a> { /// The per-platform builds to bundle. - pub binaries: &'a BinarySet, + pub(crate) binaries: &'a BinarySet, /// The macOS deployment target the libraries were built for (`"11.0"`), /// which wheel tags and Swift platforms carry. - pub macos_deployment_target: &'a str, - /// The iOS deployment target the libraries were built for (`"13.0"`). - pub ios_deployment_target: &'a str, - /// The assembled `XCFramework` archive, when the Swift target packages - /// Apple static libraries. - pub xcframework: Option<&'a XcframeworkArchive>, + pub(crate) macos_deployment_target: &'a str, + /// Where skipped artifacts are recorded. + pub(crate) skips: Option<&'a Skips>, } impl<'a> PackageContext<'a> { - /// A context for `binaries` built for the default deployment targets - /// (macOS 11.0, iOS 13.0), without an `XCFramework` archive. - #[must_use] - pub fn new(binaries: &'a BinarySet) -> Self { + /// A context for `binaries` built for the default macOS deployment + /// target (11.0). + #[cfg(test)] + pub(crate) fn new(binaries: &'a BinarySet) -> Self { Self { binaries, macos_deployment_target: "11.0", - ios_deployment_target: "13.0", - xcframework: None, + skips: None, + } + } + + /// Record that the artifact `what` was skipped because an external tool + /// it needs (named in `reason`) is missing. `weaveffi package` warns, + /// lists every skipped artifact at the end, and fails only with + /// `--strict`. + pub fn skip(&self, what: impl Into, reason: impl std::fmt::Display) { + match self.skips { + Some(skips) => skips.skip(what, reason), + None => eprintln!("warning: skipping {}: {reason}", what.into()), } } } @@ -215,7 +261,7 @@ impl<'a> PackageContext<'a> { /// The producer library of every build `keep` accepts, laid out as /// `{dir}//`, plus the import library MSVC links /// a Windows DLL through. -pub fn per_platform_libraries( +pub(crate) fn per_platform_libraries( binaries: &BinarySet, dir: &str, keep: impl Fn(Platform) -> bool, @@ -247,12 +293,13 @@ pub fn per_platform_libraries( /// /// Returns an error when a directory can't be created or replaced, a file /// can't be read or written, or an archive can't be assembled. -pub fn write_artifact(dist: &Utf8Path, artifact: &Artifact) -> Result { +pub(crate) fn write_artifact(dist: &Utf8Path, artifact: &Artifact) -> Result { let path = dist.join(&artifact.path); if let ArtifactKind::Directory = artifact.kind { if path.exists() { std::fs::remove_dir_all(path.as_std_path()) - .with_context(|| format!("failed to replace {path}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to replace {path}"))?; } for file in &artifact.files { let dest = path.join(&file.path); @@ -260,10 +307,12 @@ pub fn write_artifact(dist: &Utf8Path, artifact: &Artifact) -> Result { std::fs::copy(source.as_std_path(), dest.as_std_path()) - .with_context(|| format!("failed to copy {source} to {dest}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to copy {source} to {dest}"))?; } content => std::fs::write(dest.as_std_path(), content.bytes()?) - .with_context(|| format!("failed to write {dest}"))?, + .into_diagnostic() + .wrap_err_with(|| format!("failed to write {dest}"))?, } } return Ok(path); @@ -277,16 +326,27 @@ pub fn write_artifact(dist: &Utf8Path, artifact: &Artifact) -> Result archive::zip(&archive::entries(&artifact.files, None)?)?, ArtifactKind::Wheel(meta) => archive::wheel(meta, &artifact.files)?, ArtifactKind::Gem(spec) => archive::gem(spec, &artifact.files)?, + ArtifactKind::File => match artifact.files.as_slice() { + [file] => file.content.bytes()?.into_owned(), + _ => { + return Err(miette::miette!( + "the file artifact {path} must hold one entry" + )) + } + }, }; create_parent(&path)?; - std::fs::write(path.as_std_path(), bytes).with_context(|| format!("failed to write {path}"))?; + std::fs::write(path.as_std_path(), bytes) + .into_diagnostic() + .wrap_err_with(|| format!("failed to write {path}"))?; Ok(path) } fn create_parent(path: &Utf8Path) -> Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent.as_std_path()) - .with_context(|| format!("failed to create {parent}"))?; + .into_diagnostic() + .wrap_err_with(|| format!("failed to create {parent}"))?; } Ok(()) } diff --git a/crates/weaveffi-cli/src/platform.rs b/crates/weaveffi-cli/src/platform.rs index cb50dece..daca03ae 100644 --- a/crates/weaveffi-cli/src/platform.rs +++ b/crates/weaveffi-cli/src/platform.rs @@ -19,11 +19,12 @@ //! `XCFramework` needs). The [`crate::package`] driver and every packaging //! backend consume it. -use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; /// The operating-system family of a [`Platform`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[allow(clippy::enum_variant_names, reason = "`MacOs` is the OS's name")] pub enum Os { /// Apple platforms (macOS); shared libraries are `.dylib`. MacOs, @@ -441,29 +442,61 @@ pub fn jni_shim_name(library: &str) -> String { format!("{library}_jni") } -/// The newest glibc version a Linux shared library links against, read from -/// the `GLIBC_2.x` symbol-version names in its bytes, and never older than -/// 2.17 (the oldest glibc Rust supports). +/// The newest glibc version the Linux shared library at `path` needs, read +/// from the `GLIBC_2.x` version needs in its `.gnu.version_r` section, and +/// never older than 2.17 (the oldest glibc Rust supports). /// /// This picks the `manylinux_2_N` tag of a wheel: a library built on a /// recent distribution needs that distribution's glibc, whatever the target /// triple's minimum. -#[must_use] -pub fn glibc_requirement(library: &[u8]) -> (u32, u32) { - const NEEDLE: &[u8] = b"GLIBC_2."; - let mut best = (2, 17); - let mut rest = library; - while let Some(at) = rest.windows(NEEDLE.len()).position(|w| w == NEEDLE) { - rest = &rest[at + NEEDLE.len()..]; - let digits = rest.iter().take_while(|b| b.is_ascii_digit()).count(); - if let Some(minor) = std::str::from_utf8(&rest[..digits]) - .ok() - .and_then(|d| d.parse::().ok()) - { - best = best.max((2, minor)); +pub(crate) fn glibc_requirement(path: &Utf8Path) -> Result<(u32, u32)> { + use object::read::elf::{ElfFile32, ElfFile64, FileHeader}; + + fn needs( + file: &object::read::elf::ElfFile<'_, Elf>, + ) -> object::Result> { + let endian = file.endian(); + let data = file.data(); + let sections = file.elf_section_table(); + let mut names = Vec::new(); + if let Some((mut verneeds, link)) = sections.gnu_verneed(endian, data)? { + let strings = sections.strings(endian, data, link)?; + while let Some((_, mut vernauxs)) = verneeds.next()? { + while let Some(vernaux) = vernauxs.next()? { + names + .push(String::from_utf8_lossy(vernaux.name(endian, strings)?).into_owned()); + } + } + } + Ok(names) + } + + let data = read_file(path)?; + let names = match object::FileKind::parse(&*data) { + Ok(object::FileKind::Elf64) => { + ElfFile64::::parse(&*data).and_then(|f| needs(&f)) + } + Ok(object::FileKind::Elf32) => { + ElfFile32::::parse(&*data).and_then(|f| needs(&f)) + } + _ => { + return Err(miette::miette!( + "{path} isn't an ELF shared library, so its glibc requirement can't be read" + )) } } - best + .into_diagnostic() + .wrap_err_with(|| format!("failed to read the glibc version needs of {path}"))?; + Ok(newest_glibc(names.iter().map(String::as_str))) +} + +/// The newest `GLIBC_2.x` among version-need `names`, at least 2.17. +fn newest_glibc<'a>(names: impl Iterator) -> (u32, u32) { + names + .filter_map(|name| name.strip_prefix("GLIBC_2.")) + .filter_map(|rest| rest.split('.').next()?.parse::().ok()) + .map(|minor| (2, minor)) + .fold((2, 17), std::cmp::max) } /// The files `weaveffi build` laid out for one platform in @@ -577,12 +610,14 @@ impl BinarySet { let platform_dir = dir.join(platform.id()); match NativeBinary::read_dir(&platform_dir, *platform, lib_name) { Some(binary) => set.insert(binary), - None if platforms.is_some() => bail!( - "no {} in {platform_dir}; build {} first (`weaveffi build --platforms {}`)", - platform.lib_filename(lib_name), - platform.id(), - platform.id() - ), + None if platforms.is_some() => { + return Err(miette::miette!( + "no {} in {platform_dir}; build {} first (`weaveffi build --platforms {}`)", + platform.lib_filename(lib_name), + platform.id(), + platform.id() + )) + } None => {} } } @@ -639,7 +674,9 @@ impl BinarySet { /// /// Returns an error when the file can't be read. pub fn read_file(path: &Utf8Path) -> Result> { - std::fs::read(path.as_std_path()).with_context(|| format!("failed to read {path}")) + std::fs::read(path.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {path}")) } #[cfg(test)] @@ -815,11 +852,26 @@ mod tests { } #[test] - fn glibc_requirement_takes_the_newest_version_named() { - assert_eq!(glibc_requirement(b"nothing here"), (2, 17)); + fn glibc_requirement_takes_the_newest_version_needed() { + assert_eq!(newest_glibc(["GCC_3.0"].into_iter()), (2, 17)); assert_eq!( - glibc_requirement(b"\0GLIBC_2.2.5\0GLIBC_2.28\0GLIBC_2.3.4\0"), + newest_glibc(["GLIBC_2.2.5", "GLIBC_2.28", "GLIBC_2.3.4", "GCC_4.2"].into_iter()), (2, 28) ); } + + #[test] + fn glibc_requirement_reads_elf_version_needs_and_rejects_other_files() { + let dir = tempfile::tempdir().unwrap(); + let junk = Utf8Path::from_path(dir.path()).unwrap().join("libjunk.so"); + std::fs::write(&junk, b"\x00GLIBC_2.99\x00").unwrap(); + assert!(glibc_requirement(&junk).is_err()); + assert!(glibc_requirement(&junk.with_file_name("missing.so")).is_err()); + // This test binary links glibc on Linux. + if cfg!(all(target_os = "linux", target_env = "gnu")) { + let me = std::env::current_exe().unwrap(); + let me = Utf8Path::from_path(&me).unwrap(); + assert!(glibc_requirement(me).unwrap() >= (2, 17)); + } + } } diff --git a/crates/weaveffi-cli/src/project.rs b/crates/weaveffi-cli/src/project.rs index 0dd8ea87..29290e3b 100644 --- a/crates/weaveffi-cli/src/project.rs +++ b/crates/weaveffi-cli/src/project.rs @@ -2,17 +2,24 @@ //! called, and how to turn it into bindings. //! //! A project's input is either an IDL document or a Rust producer crate. -//! A crate's API is read from its built library (see [`crate::library`]): +//! A crate's API is read from the metadata frames in its built library: //! [`Project`] builds the crate with Cargo, or reads a library built //! earlier ([`Project::library`]). Every `weaveffi` subcommand starts here, //! and so can a `build.rs`: //! //! ```no_run //! // build.rs of a crate that ships bindings for an IDL-defined library. +//! use weaveffi_cli::codegen::Orchestrator; +//! use weaveffi_cli::project::Project; +//! //! fn main() -> miette::Result<()> { //! println!("cargo::rerun-if-changed=weaveffi.toml"); //! println!("cargo::rerun-if-changed=api.yml"); -//! weaveffi_cli::project::Project::discover(env!("CARGO_MANIFEST_DIR"))?.generate()?; +//! let project = Project::discover(env!("CARGO_MANIFEST_DIR"))?; +//! let targets = project.config.targets(None)?; +//! Orchestrator::new() +//! .with_targets(targets.iter().map(AsRef::as_ref)) +//! .run(&project.model()?, &project.config.out_dir(None))?; //! Ok(()) //! } //! ``` @@ -33,15 +40,14 @@ use weaveffi_model::validate::{ }; use crate::cargo::CargoCrate; -use crate::codegen::{GenerateReport, Orchestrator}; -use crate::config::{ProjectConfig, CONFIG_FILE_NAME}; +use crate::config::{current_dir, discover, ProjectConfig, CONFIG_FILE_NAME}; use crate::library; use crate::report::with_named_source; /// Where a project's API comes from. #[derive(Debug, Clone)] -pub enum Source { - /// An IDL document (`.yml`, `.yaml`, `.json`, or `.toml`). +pub(crate) enum Source { + /// An IDL document (`.yml`, `.yaml`, or `.json`). Idl(Utf8PathBuf), /// A Rust producer crate, whose API is read from its built library. Crate(Box), @@ -55,10 +61,11 @@ pub struct Project { /// The `weaveffi.toml` settings (the defaults when there is none). pub config: ProjectConfig, /// Where the API comes from. - pub source: Source, + pub(crate) source: Source, /// A built library to read a crate's API from instead of building it. library: Option, - release: bool, + /// The Cargo profile a crate's library is built with. + profile: String, quiet: bool, } @@ -102,7 +109,7 @@ impl Project { /// parse, or when its input can't be resolved. pub fn discover(dir: impl AsRef) -> Result { let dir = dir.as_ref(); - if let Some(config) = discover_config(&dir.join(CONFIG_FILE_NAME)) { + if let Some(config) = discover(&dir.join(CONFIG_FILE_NAME)) { return Self::locate(Some(config.as_str()), None, None); } if dir.join("Cargo.toml").is_file() { @@ -145,7 +152,7 @@ impl Project { Some(p) => ProjectConfig::from_file(Utf8Path::new(p))?, None => current_dir() .ok() - .and_then(|cwd| discover_config(&cwd.join(CONFIG_FILE_NAME))) + .and_then(|cwd| discover(&cwd.join(CONFIG_FILE_NAME))) .map(|p| ProjectConfig::from_file(&p)) .transpose()? .unwrap_or_default(), @@ -170,7 +177,7 @@ impl Project { config, source, library: None, - release: false, + profile: "dev".into(), quiet: false, }; if let Some(library) = library { @@ -200,10 +207,11 @@ impl Project { Ok(self) } - /// Build a crate's library in release mode instead of debug. + /// Build a crate's library with the Cargo profile `profile` (default + /// `dev`). #[must_use] - pub fn release(mut self, release: bool) -> Self { - self.release = release; + pub fn profile(mut self, profile: impl Into) -> Self { + self.profile = profile.into(); self } @@ -215,14 +223,12 @@ impl Project { } /// Whether Cargo runs with `--quiet`. - #[must_use] - pub fn is_quiet(&self) -> bool { + pub(crate) fn is_quiet(&self) -> bool { self.quiet } /// The producer crate, when the input is one. - #[must_use] - pub fn krate(&self) -> Option<&CargoCrate> { + pub(crate) fn krate(&self) -> Option<&CargoCrate> { match &self.source { Source::Crate(k) => Some(k), _ => None, @@ -232,11 +238,7 @@ impl Project { /// The library a Rust producer's API is read from: the one named with /// [`library`](Self::library), else the crate's, built now. /// - /// # Errors - /// - /// Returns an error for an IDL input, from inside the crate's own build - /// script, or when the build fails. - pub fn library_path(&self) -> Result { + pub(crate) fn library_path(&self) -> Result { match &self.source { Source::Idl(idl) => Err(miette!("{idl} is an IDL; it has no library to read")), Source::Library(lib) => Ok(lib.clone()), @@ -252,9 +254,7 @@ impl Project { krate.name ); } - krate - .build_library(self.release, self.quiet) - .map_err(|e| miette!("{e:#}")) + krate.build_library(&self.profile, self.quiet) } } } @@ -284,7 +284,7 @@ impl Project { Source::Crate(krate) => { rust_only_keys(package)?; let path = self.library_path()?; - let api = read_api(&path, &krate.lib_name)?; + let (api, _) = read_api(&path, Some(&krate.lib_name))?; Ok(Definition { api, identity: crate_identity(krate, package), @@ -293,21 +293,7 @@ impl Project { } Source::Library(path) => { rust_only_keys(package)?; - let frames = library::read_frames(path).map_err(|e| miette!("{e:#}"))?; - let prefixes: std::collections::BTreeSet<&str> = - frames.iter().map(|f| f.prefix.as_str()).collect(); - let prefix = match prefixes.len() { - 0 => return Err(no_metadata(path, None)), - 1 => prefixes.into_iter().next().unwrap_or_default().to_string(), - _ => { - return Err(miette!( - "{path} holds the APIs of several crates ({}); pass the producer \ - crate so WeaveFFI knows which one to bind", - prefixes.into_iter().collect::>().join(", ") - )) - } - }; - let api = meta::assemble(&frames, &prefix).map_err(|e| miette!("{path}: {e}"))?; + let (api, prefix) = read_api(path, None)?; let mut merged = package.clone(); merged.c_prefix = Some(prefix.clone()); merged.library = Some(library::library_name(path)); @@ -329,28 +315,6 @@ impl Project { pub fn model(&self) -> Result { self.definition()?.validate().map_err(Report::new) } - - /// Generate the `[project] targets` (else every target) into the - /// `[project] out` directory, writing only changed files and removing - /// stale ones. - /// - /// # Errors - /// - /// Returns the errors of [`model`](Self::model), an unknown target name, - /// or a failure to write the output. - pub fn generate(&self) -> Result { - let model = self.model()?; - let out = self.config.out_dir(None); - let targets = self.config.select_targets(None)?; - std::fs::create_dir_all(out.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to create output directory: {out}"))?; - let mut orchestrator = Orchestrator::new(); - for target in &targets { - orchestrator = orchestrator.with_target(target.as_ref()); - } - orchestrator.run(&model, &out).map_err(|e| miette!("{e:#}")) - } } /// What `input` is: a crate (a directory with a `Cargo.toml`, or the @@ -361,7 +325,7 @@ fn source_of(input: &Utf8Path) -> Result { if !manifest.is_file() { bail!( "{input} is a directory without a Cargo.toml; the input is a Rust producer crate \ - or an IDL file (yml|yaml|json|toml)" + or an IDL file (yml|yaml|json)" ); } Some(manifest) @@ -371,9 +335,7 @@ fn source_of(input: &Utf8Path) -> Result { None }; match manifest { - Some(m) => Ok(Source::Crate(Box::new( - CargoCrate::resolve(&m).map_err(|e| miette!("{e:#}"))?, - ))), + Some(m) => Ok(Source::Crate(Box::new(CargoCrate::resolve(&m)?))), None => { idl_format(input)?; Ok(Source::Idl(input.to_path_buf())) @@ -386,14 +348,13 @@ fn idl_format(path: &Utf8Path) -> Result<&'static str> { match path.extension().unwrap_or("") { "yml" | "yaml" => Ok("yaml"), "json" => Ok("json"), - "toml" => Ok("toml"), "rs" => Err(miette!( "{path} is Rust source; WeaveFFI reads a Rust producer's API from its built \ library, so pass the crate (its directory or Cargo.toml) instead" )), other => Err(miette!( "unsupported input {path}{}: expected a Rust producer crate (its directory or \ - Cargo.toml) or an IDL file (yml|yaml|json|toml)", + Cargo.toml) or an IDL file (yml|yaml|json)", if other.is_empty() { String::new() } else { @@ -432,19 +393,38 @@ fn crate_identity(krate: &CargoCrate, package: &Package) -> Identity { Identity::new(&krate.name, &merged) } -/// The API the library at `path` embeds for the crate with `prefix`. -fn read_api(path: &Utf8Path, prefix: &str) -> Result { - let frames = library::read_frames(path).map_err(|e| miette!("{e:#}"))?; - let api = meta::assemble(&frames, prefix).map_err(|e| miette!("{path}: {e}"))?; +/// The API the library at `path` embeds, and its crate's C prefix: the +/// crate with `prefix`, or, without one, the only crate whose metadata the +/// library holds. +fn read_api(path: &Utf8Path, prefix: Option<&str>) -> Result<(Api, String)> { + let frames = library::read_frames(path)?; + let prefixes: std::collections::BTreeSet<&str> = + frames.iter().map(|f| f.prefix.as_str()).collect(); + let prefix = match (prefix, prefixes.len()) { + (Some(prefix), _) => prefix.to_string(), + (None, 0) => return Err(no_metadata(path, None)), + (None, 1) => prefixes + .iter() + .next() + .copied() + .unwrap_or_default() + .to_string(), + (None, _) => { + return Err(miette::miette!( + "{path} holds the APIs of several crates ({}); pass the producer crate so WeaveFFI \ + knows which one to bind", + prefixes.into_iter().collect::>().join(", ") + )) + } + }; + let api = meta::assemble(&frames, &prefix).map_err(|e| miette!("{path}: {e}"))?; if api.modules.is_empty() { - let others: std::collections::BTreeSet<&str> = - frames.iter().map(|f| f.prefix.as_str()).collect(); return Err(no_metadata( path, - Some((prefix, others.into_iter().collect())), + Some((&prefix, prefixes.into_iter().collect())), )); } - Ok(api) + Ok((api, prefix)) } /// The error for a library without (the crate's) metadata. @@ -474,17 +454,6 @@ fn in_own_build_script(krate: &CargoCrate) -> bool { }) } -/// The current directory as a UTF-8 path. -fn current_dir() -> std::io::Result { - Utf8PathBuf::from_path_buf(std::env::current_dir()?) - .map_err(|_| std::io::Error::other("current directory is not valid UTF-8")) -} - -/// The nearest `weaveffi.toml` at or above the directory of `start`. -fn discover_config(start: &Utf8Path) -> Option { - crate::config::discover(start) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/weaveffi-cli/src/cache.rs b/crates/weaveffi-cli/src/record.rs similarity index 65% rename from crates/weaveffi-cli/src/cache.rs rename to crates/weaveffi-cli/src/record.rs index 481905ac..c71548ab 100644 --- a/crates/weaveffi-cli/src/cache.rs +++ b/crates/weaveffi-cli/src/record.rs @@ -1,4 +1,4 @@ -//! Per-target generation records for stale-file cleanup. +//! Generation records: what each target's last generation wrote. //! //! After generating a target, the orchestrator writes //! `{out_dir}/.weaveffi-cache/{target}.json`: the path of every file the @@ -9,50 +9,42 @@ use std::collections::BTreeSet; -use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::{IntoDiagnostic, Result, WrapErr}; use serde::{Deserialize, Serialize}; -const CACHE_DIR: &str = ".weaveffi-cache"; +const RECORD_DIR: &str = ".weaveffi-cache"; /// What one generation of one target produced. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Record { +pub(crate) struct Record { /// Every file the target produced, relative to the output directory /// (with `/` separators). - pub files: BTreeSet, + pub(crate) files: BTreeSet, } fn record_path(out_dir: &Utf8Path, target: &str) -> Utf8PathBuf { - out_dir.join(CACHE_DIR).join(format!("{target}.json")) + out_dir.join(RECORD_DIR).join(format!("{target}.json")) } /// Read the record of the last generation of `target` under `out_dir`, if /// any (a missing or unreadable record means "never generated"). -#[must_use] -pub fn read_record(out_dir: &Utf8Path, target: &str) -> Option { +pub(crate) fn read_record(out_dir: &Utf8Path, target: &str) -> Option { let text = std::fs::read_to_string(record_path(out_dir, target)).ok()?; serde_json::from_str(&text).ok() } /// Persist the record of a generation of `target`. -/// -/// # Errors -/// -/// Returns an error when the cache directory cannot be created or the record -/// cannot be written. -/// -/// # Panics -/// -/// Panics if the record cannot be serialized, which does not happen. -pub fn write_record(out_dir: &Utf8Path, target: &str, record: &Record) -> Result<()> { +pub(crate) fn write_record(out_dir: &Utf8Path, target: &str, record: &Record) -> Result<()> { let path = record_path(out_dir, target); - let dir = out_dir.join(CACHE_DIR); + let dir = out_dir.join(RECORD_DIR); std::fs::create_dir_all(dir.as_std_path()) - .with_context(|| format!("failed to create cache directory: {dir}"))?; - let json = serde_json::to_string_pretty(record).expect("record serializes"); + .into_diagnostic() + .wrap_err_with(|| format!("failed to create the record directory {dir}"))?; + let json = serde_json::to_string_pretty(record).into_diagnostic()?; std::fs::write(path.as_std_path(), json) - .with_context(|| format!("failed to write cache file: {path}")) + .into_diagnostic() + .wrap_err_with(|| format!("failed to write the generation record {path}")) } #[cfg(test)] diff --git a/crates/weaveffi-cli/src/targets/c/buffer.rs b/crates/weaveffi-cli/src/targets/c/buffer.rs index 92eb8632..792b5f89 100644 --- a/crates/weaveffi-cli/src/targets/c/buffer.rs +++ b/crates/weaveffi-cli/src/targets/c/buffer.rs @@ -12,10 +12,13 @@ //! spliced in with the prefix substituted; everything after them is rendered //! from the [`Model`]. -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use crate::cabi::c_param_name; -use crate::codegen::common::DocCommentStyle; +use crate::codegen::codecs; +use crate::codegen::common::{self, DocCommentStyle}; +use crate::codegen::docs::{ApiNames, Doc}; +use crate::codegen::errors; use crate::codegen::CodeWriter; use crate::utils::{render_prelude, render_trailer, CommentStyle}; use weaveffi_model::model::{EnumBinding, FieldBinding, InterfaceBinding, Model}; @@ -52,13 +55,14 @@ struct Codecs<'a> { users: Vec>, /// Record or rich enum name to an index into `users`. user_index: HashMap<&'a str, usize>, - /// Optional, list, and map shapes, innermost first, without duplicates. + /// Optional, list, and map shapes, innermost first, without duplicates + /// (see [`codecs::composites`]). composites: Vec, } /// The header file name for a library: `{library}_buffer.h`. #[must_use] -pub fn buffer_header_name(library: &str) -> String { +pub(crate) fn buffer_header_name(library: &str) -> String { format!("{library}_buffer.h") } @@ -68,7 +72,11 @@ pub fn buffer_header_name(library: &str) -> String { /// `header_name` is the main header the helper includes; `file_name` is the /// helper's own name, used in its trailer. #[must_use] -pub fn render_buffer_header(model: &Model, header_name: &str, file_name: &str) -> Option { +pub(crate) fn render_buffer_header( + model: &Model, + header_name: &str, + file_name: &str, +) -> Option { if !model.has_buffers() { return None; } @@ -105,14 +113,19 @@ struct Unit { impl<'a> Codecs<'a> { fn new(model: &'a Model) -> Self { let prefix = model.prefix(); + let names = ApiNames::new(model); + let doc = |doc: &Option, deprecated: &Option| { + Doc::new(doc, deprecated) + .with_deprecation(|ident| names.c_name(ident).map(str::to_string)) + }; let mut users = Vec::new(); let mut user_index = HashMap::new(); for m in &model.modules { for s in &m.structs { user_index.insert(s.name.as_str(), users.len()); users.push(UserType { - name: format!("{prefix}_{}_{}", m.path, s.name), - doc: with_deprecation(&s.doc, &s.deprecated), + name: s.c_tag.clone(), + doc: doc(&s.doc, &s.deprecated), body: Body::Fields(&s.fields), }); } @@ -120,68 +133,34 @@ impl<'a> Codecs<'a> { user_index.insert(e.name.as_str(), users.len()); users.push(UserType { name: e.c_tag.clone(), - doc: with_deprecation(&e.doc, &e.deprecated), + doc: doc(&e.doc, &e.deprecated), body: Body::Variants(e), }); } } - for m in &model.modules { - let Some(err) = m.errors.as_ref() else { - continue; - }; - for code in err.codes.iter().filter(|c| !c.fields.is_empty()) { + for table in errors::tables(model, "Error") { + for row in table.codes.iter().filter(|r| !r.code.fields.is_empty()) { users.push(UserType { - name: code.payload_tag(), - doc: Some(format!( - "The fields a `{}` error carries in `payload_ptr`/`payload_len`.", - code.c_const + name: row.code.payload_tag(), + doc: Some(common::wrap( + &format!( + "The fields of a `{}` error (`{}` in the language bindings), \ + which it carries in `payload_ptr`/`payload_len`.", + row.code.c_const, row.type_name + ), + 76, )), - body: Body::Fields(&code.fields), + body: Body::Fields(&row.code.fields), }); } } - let mut composites = Vec::new(); - let mut seen = HashSet::new(); - for u in &users { - match u.body { - Body::Fields(fields) => { - for f in fields { - collect(&f.ty, &mut composites, &mut seen); - } - } - Body::Variants(e) => { - for f in e.variants.iter().flat_map(|v| &v.fields) { - collect(&f.ty, &mut composites, &mut seen); - } - } - } - } - let mut boundary = |ty: &Ty| { - let ty = ty.iterator_elem().unwrap_or(ty); - if ty.is_buffered() { - collect(ty, &mut composites, &mut seen); - } - }; - for m in &model.modules { - for f in m.callables() { - f.params.iter().for_each(|p| boundary(&p.ty)); - f.ret.iter().for_each(&mut boundary); - } - for cb in &m.callback_interfaces { - for meth in &cb.methods { - meth.params.iter().for_each(|p| boundary(&p.ty)); - meth.ret.iter().for_each(&mut boundary); - } - } - } - Codecs { model, prefix, users, user_index, - composites, + composites: codecs::composites(model), } } @@ -206,7 +185,7 @@ impl<'a> Codecs<'a> { match ty { Ty::Record(n) | Ty::RichEnum(n) => Some(self.user(n).name.clone()), Ty::Optional(_) | Ty::List(_) | Ty::Map(_, _) => { - Some(format!("{}_{}", self.prefix, mangle(self.model, ty))) + Some(format!("{}_{}", self.prefix, codecs::stem(ty))) } _ => None, } @@ -771,47 +750,6 @@ impl<'a> Codecs<'a> { } } -/// A type's doc comment with its deprecation note appended. The struct -/// itself carries no deprecation attribute: the codecs in this header use it, -/// and every such use would warn. -fn with_deprecation(doc: &Option, deprecated: &Option) -> Option { - match (doc, deprecated) { - (_, None) => doc.clone(), - (None, Some(msg)) => Some(format!("Deprecated: {msg}")), - (Some(doc), Some(msg)) => Some(format!("{doc}\n\nDeprecated: {msg}")), - } -} - -/// Record every optional, list, and map shape inside `ty`, innermost first. -fn collect(ty: &Ty, out: &mut Vec, seen: &mut HashSet) { - match ty { - Ty::Optional(inner) | Ty::List(inner) => collect(inner, out, seen), - Ty::Map(k, v) => { - collect(k, out, seen); - collect(v, out, seen); - } - _ => return, - } - if seen.insert(ty.clone()) { - out.push(ty.clone()); - } -} - -/// The identifier fragment naming `ty` inside a composite's codec stem: -/// `string`, `kv_Entry` (a user type with its module path), `opt_i64`, -/// `list_string`, `map_string_i64`. -fn mangle(model: &Model, ty: &Ty) -> String { - match ty { - Ty::Optional(inner) => format!("opt_{}", mangle(model, inner)), - Ty::List(inner) => format!("list_{}", mangle(model, inner)), - Ty::Map(k, v) => format!("map_{}_{}", mangle(model, k), mangle(model, v)), - _ => match ty.user_name() { - Some(name) => format!("{}_{name}", model.owner(name).path), - None => prim(ty).snake().to_string(), - }, - } -} - /// The wire primitive of a scalar, string, or bytes type. fn prim(ty: &Ty) -> Prim { match ty.wire() { diff --git a/crates/weaveffi-cli/src/targets/c/header.rs b/crates/weaveffi-cli/src/targets/c/header.rs index 0fe0ee32..bdf5dd1c 100644 --- a/crates/weaveffi-cli/src/targets/c/header.rs +++ b/crates/weaveffi-cli/src/targets/c/header.rs @@ -1,27 +1,21 @@ //! Rendering of the `{library}.h` header. //! -//! The per-declaration rendering is shared with the C++ backend via -//! [`crate::cabi`]; this module adds only the header framing (include guard, -//! includes, and the value-buffer convention comment). +//! The declarations come from [`crate::cabi`]; this module adds the framing +//! (include guard, includes, visibility macros, and the calling-convention +//! comment). use std::fmt::Write; -use crate::cabi; +use crate::cabi::{self, Decls}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; use weaveffi_model::model::Model; -/// Render the complete header from the shared [`Model`]. -/// -/// The per-declaration rendering is shared with the C++ backend via -/// [`crate::cabi`]; this function only adds the header framing -/// (include guard, includes, the value-buffer convention comment). The -/// C symbol prefix is read from [`Model::prefix`], so every name already -/// agrees with the symbols baked into the model. Parameter names that collide -/// with a C or C++ keyword are escaped inside `cabi` itself (see -/// [`crate::cabi::c_param_name`]). -pub fn render_c_header_from_model(model: &Model, filename: &str) -> String { +/// Render the complete header for `model`; `filename` is the header's own +/// name, which its trailer and the convention comment mention. +pub(crate) fn render_c_header_from_model(model: &Model, filename: &str) -> String { let prefix = model.prefix(); let guard = format!("{}_H", prefix.to_uppercase()); + let decls = Decls::new(model); let mut out = String::with_capacity(2048 + model.modules.len() * 4096); out.push_str(&render_prelude(CommentStyle::DoubleSlash)); let _ = write!(out, "#ifndef {guard}\n#define {guard}\n\n"); @@ -30,30 +24,42 @@ pub fn render_c_header_from_model(model: &Model, filename: &str) -> String { out.push_str("#include \n\n"); cabi::render_visibility_macros(&mut out, prefix); out.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"); - cabi::render_runtime_decls(&mut out, model); + decls.runtime(&mut out); let helper = format!( "{}_buffer.h", filename.strip_suffix(".h").unwrap_or(filename) ); - out.push_str("/*\n"); - out.push_str(" * Strings, bytes, and value buffers cross as (ptr, len) pairs. A\n"); - out.push_str(" * parameter named \"v\" expands to a view borrowed for the call:\n"); - out.push_str(" * const uint8_t* v_ptr, size_t v_len\n"); - out.push_str(" * Strings are UTF-8 without a NUL terminator; NULL with length 0 is\n"); - out.push_str(" * the empty value. A returned string, bytes, or buffer is\n"); - out.push_str(" * `const uint8_t*` with a trailing `size_t* out_len`, owned by the\n"); - out.push_str(&format!( - " * caller and released with {prefix}_free_bytes.\n" - )); - out.push_str(" * Records, rich enums, lists, maps, and optionals are serialized\n"); - out.push_str(" * in the little-endian value buffer format; when generated,\n"); - out.push_str(&format!( - " * {helper} declares a C struct and codec for each.\n" - )); - out.push_str(" */\n\n"); - - cabi::render_decls(&mut out, &model.modules, prefix, true); - + let _ = write!( + out, + "/*\n \ + * How values cross. A parameter `v` becomes, by its type:\n \ + * integer, float, bool, C-style enum T v\n \ + * optional of one of those (T?) bool has_v, T v (v is 0 and ignored\n \ + * when has_v is false)\n \ + * [i8] [i16] [i32] [i64] [u16] [u32] const T* v_ptr, size_t v_len\n \ + * [u64] [f32] [f64] (typed arrays) (v_len counts elements; v_ptr is\n \ + * aligned for T, NULL when v_len is 0)\n \ + * string, bytes, and every other const uint8_t* v_ptr, size_t v_len\n \ + * record, enum, list, map, optional\n \ + * interface const Tag* v (borrowed; NULL for an\n \ + * absent optional)\n \ + * callback interface void* v_ctx, const Tag_vtable* v_vtable\n \ + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is the\n \ + * empty value. Everything in the (ptr, len) row past bytes is a value\n \ + * buffer in the little-endian format; when generated, {helper}\n \ + * declares a C struct and codec for each such type.\n \ + *\n \ + * A return of type T is the C return, except: T? returns `bool` (present)\n \ + * with the value in a trailing `T* out_value`; a typed array returns `T*`\n \ + * with its element count in `size_t* out_len`, released with\n \ + * {prefix}_free_bytes((uint8_t*)ptr, len * sizeof(T)); a string, bytes, or\n \ + * buffer returns `const uint8_t*` with `size_t* out_len`, released with\n \ + * {prefix}_free_bytes(ptr, len); an interface returns an owned reference,\n \ + * released with its `_destroy`. Every producer run is 8-aligned. Every\n \ + * sync call ends with `{prefix}_error* out_err`.\n \ + */\n\n" + ); + decls.declarations(&mut out); out.push_str("\n#ifdef __cplusplus\n}\n#endif\n\n"); let _ = write!(out, "#endif // {guard}\n\n"); out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); diff --git a/crates/weaveffi-cli/src/targets/c/mod.rs b/crates/weaveffi-cli/src/targets/c/mod.rs index d86d30e1..b9bb67fb 100644 --- a/crates/weaveffi-cli/src/targets/c/mod.rs +++ b/crates/weaveffi-cli/src/targets/c/mod.rs @@ -17,14 +17,15 @@ mod buffer; mod header; mod package; -use crate::backend::{LanguageBackend, OutputFile}; +use crate::codegen::OutputFile; use crate::package::{per_platform_libraries, Artifact, PackageContext, PackagedFile}; -use camino::Utf8Path; +use crate::targets::{Linkage, Target}; +use miette::Result; use serde::{Deserialize, Serialize}; use weaveffi_model::model::Model; -pub use buffer::{buffer_header_name, render_buffer_header}; -pub use header::render_c_header_from_model; +pub(crate) use buffer::{buffer_header_name, render_buffer_header}; +pub(crate) use header::render_c_header_from_model; use package::{render_packaged_cmake, render_packaged_readme}; @@ -61,37 +62,43 @@ fn buffer_file(model: &Model, config: &CConfig) -> Option<(String, String)> { render_buffer_header(model, &header_name(model), &name).map(|contents| (name, contents)) } -/// The C header backend. -pub struct CGenerator; +/// The C header target. +pub struct CGenerator { + config: CConfig, +} -impl LanguageBackend for CGenerator { - type Config = CConfig; +impl From for CGenerator { + fn from(config: CConfig) -> Self { + Self { config } + } +} +impl Target for CGenerator { fn name(&self) -> &'static str { "c" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { + fn render(&self, model: &Model) -> Vec { let header = header_name(model); let mut files = vec![OutputFile::new( - out_dir.join("c").join(&header), + &header, render_c_header_from_model(model, &header), )]; - if let Some((name, contents)) = buffer_file(model, config) { - files.push(OutputFile::new(out_dir.join("c").join(name), contents)); + if let Some((name, contents)) = buffer_file(model, &self.config) { + files.push(OutputFile::new(name, contents)); } files } + fn linkage(&self) -> Linkage { + Linkage::Link + } + /// One `{library}-{version}-c.tar.gz` holding the headers under /// `include/`, every platform's library under `lib//`, and a /// `CMakeLists.txt` exposing the host's library as an imported target. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let header_name = header_name(model); let lib = &ctx.binaries.lib_name; let mut files = vec![ @@ -107,10 +114,85 @@ impl LanguageBackend for CGenerator { } files.extend(per_platform_libraries(ctx.binaries, "lib", |_| true)); let stem = format!("{lib}-{}", model.identity.version); - Some(vec![Artifact::tar_gz( + Ok(vec![Artifact::tar_gz( format!("c/{stem}-c.tar.gz"), stem, files, )]) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::test_model; + use crate::package::ArtifactKind; + use crate::platform::{BinarySet, NativeBinary, Platform}; + + const SHOP: &str = r#" +version: "0.12.0" +modules: + - name: shop + structs: + - name: Item + fields: [{ name: name, type: string }] + functions: + - { name: first, params: [], return: Item } +"#; + + #[test] + fn package_bundles_headers_cmake_and_every_platform() { + let model = test_model(SHOP); + let mut binaries = BinarySet::new("kv"); + for p in [Platform::MacosArm64, Platform::LinuxX64] { + binaries.insert(NativeBinary::new(p, format!("/prebuilt/{}/lib", p.id()))); + } + let artifacts = CGenerator::from(CConfig::default()) + .package(&model, &PackageContext::new(&binaries)) + .expect("c packages"); + assert_eq!(artifacts.len(), 1); + let artifact = &artifacts[0]; + assert_eq!(artifact.path, "c/kv-0.1.0-c.tar.gz"); + assert!(matches!(&artifact.kind, ArtifactKind::TarGz { prefix } if prefix == "kv-0.1.0")); + let mut paths: Vec<&str> = artifact.files.iter().map(|f| f.path.as_str()).collect(); + paths.sort_unstable(); + assert_eq!( + paths, + [ + "CMakeLists.txt", + "README.md", + "include/kv.h", + "include/kv_buffer.h", + "lib/darwin-arm64/libkv.dylib", + "lib/linux-x64/libkv.so", + ] + ); + } + + #[test] + fn enums_cross_as_int32_with_anonymous_constants() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + enums: + - name: Mode + variants: [{ name: Fast, value: 0 }, { name: Slow, value: 7 }] + errors: + - name: Oops + codes: [{ name: Broke, code: 3, message: broke }] + functions: + - { name: pick, params: [{ name: m, type: "Mode?" }], return: Mode, throws: Oops } +"#, + ); + let h = render_c_header_from_model(&model, "kv.h"); + assert!(h.contains( + "typedef int32_t kv_m_Mode;\nenum { kv_m_Mode_Fast = 0, kv_m_Mode_Slow = 7 };" + )); + assert!(h.contains("typedef int32_t kv_m_Oops;\nenum { kv_m_Oops_Broke = 3 };")); + assert!(!h.contains("typedef enum")); + assert!(h.contains("kv_m_Mode kv_m_pick(bool has_m, kv_m_Mode m, kv_error* out_err);")); + assert!(h.contains("Fails with a `kv_m_Oops` code")); + } +} diff --git a/crates/weaveffi-cli/src/targets/c/runtime/buffer_runtime.h b/crates/weaveffi-cli/src/targets/c/runtime/buffer_runtime.h index d12ed889..b069c0c3 100644 --- a/crates/weaveffi-cli/src/targets/c/runtime/buffer_runtime.h +++ b/crates/weaveffi-cli/src/targets/c/runtime/buffer_runtime.h @@ -7,6 +7,8 @@ * followed by the value, lists as a `uint32_t` count followed by the * elements, maps as a `uint32_t` count followed by alternating keys and * values, and objects as a `uint64_t` token holding one strong reference. + * Every count and length is a `uint32_t`, so a writer fails on a string, + * bytes, list, or map past UINT32_MAX elements. * ------------------------------------------------------------------------- */ /* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. @@ -316,9 +318,64 @@ static inline uint8_t* {{p}}_reader_get_run({{p}}_reader* r, size_t* out_len) { return copy; } +/* Whether the `n` bytes at `s` are well-formed UTF-8: no stray or missing + continuation bytes, overlong forms, surrogates, or code points past + U+10FFFF. */ +static inline bool {{p}}_reader_utf8(const uint8_t* s, size_t n) { + size_t i = 0; + while (i < n) { + uint8_t c = s[i]; + if (c < 0x80) { + i++; + continue; + } + size_t len; + uint32_t cp; + uint32_t min; + if ((c & 0xE0) == 0xC0) { + len = 2; + cp = c & 0x1F; + min = 0x80; + } else if ((c & 0xF0) == 0xE0) { + len = 3; + cp = c & 0x0F; + min = 0x800; + } else if ((c & 0xF8) == 0xF0) { + len = 4; + cp = c & 0x07; + min = 0x10000; + } else { + return false; + } + if (n - i < len) { + return false; + } + for (size_t k = 1; k < len; k++) { + uint8_t cc = s[i + k]; + if ((cc & 0xC0) != 0x80) { + return false; + } + cp = (cp << 6) | (uint32_t)(cc & 0x3F); + } + if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + return false; + } + i += len; + } + return true; +} + +/* A string must be well-formed UTF-8: anything else fails the reader (the + decoder then returns false, the buffer analog of runtime code -3). */ static inline {{p}}_str {{p}}_reader_get_string({{p}}_reader* r) { {{p}}_str v; v.ptr = (const char*){{p}}_reader_get_run(r, &v.len); + if (v.ptr != NULL && !{{p}}_reader_utf8((const uint8_t*)v.ptr, v.len)) { + free((void*)v.ptr); + v.ptr = NULL; + v.len = 0; + r->failed = true; + } return v; } diff --git a/crates/weaveffi-cli/src/targets/cpp/callbacks.rs b/crates/weaveffi-cli/src/targets/cpp/callbacks.rs index 90546ffe..9a30c912 100644 --- a/crates/weaveffi-cli/src/targets/cpp/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/cpp/callbacks.rs @@ -1,66 +1,68 @@ -//! Callback interfaces: the abstract class the consumer implements, the -//! trampolines that adapt an implementation to the C vtable, and the -//! process-wide static vtable, satisfying -//! [`weaveffi_model::plan::CallbackProtocol`]. +//! Callback interfaces: the abstract class the consumer implements, and its +//! `detail::Callbacks` specialization holding the trampolines that adapt +//! an implementation to the C vtable and the process-wide static vtable. +//! +//! Each trampoline receives its arguments per the method's [`ArgPass`] +//! slots, hands its return back per the method's [`CallbackRetPass`], and +//! reports any exception through `out_err` per the method's +//! [`ErrorStrategy`] (through the runtime's `detail::callback`), so nothing +//! unwinds through the C frame. use crate::codegen::common::DocCommentStyle; use crate::codegen::CodeWriter; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding}; -use weaveffi_model::plan::{ArgPass, RetPass}; +use crate::lang; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; use weaveffi_model::ty::Ty; -use crate::targets::cpp::codec::{read_fn, write_stmt}; -use crate::targets::cpp::entities::{doc_text, report_fn}; +use crate::targets::cpp::entities::type_doc; use crate::targets::cpp::types::{ - cpp_cb_param_decl, cpp_fn_name, cpp_ident, cpp_type, render_param_decls, slot_name, - trampoline_struct, vtable_accessor, + cpp_cb_param_decl, cpp_error_class, cpp_fn_name, cpp_ident, cpp_type, error_class, slot_decl, + slot_name, Ctx, }; -/// The code a trampoline reports for any failure other than a declared -/// domain code. -const FOREIGN_ERROR_CODE: i32 = -4; - -/// Append a callback interface: the abstract class, then (in `detail`) its -/// trampolines and static vtable. `domain` is the error domain in scope for -/// the interface's module, which its `throws` methods may report. +/// Append a callback interface: the abstract class, then its +/// `detail::Callbacks` specialization. pub(crate) fn render_callback_interface( w: &mut CodeWriter, + ctx: &Ctx<'_>, cb: &CallbackInterfaceBinding, - domain: Option<&ErrorBinding>, - prefix: &str, ) { - render_callback_class(w, cb, domain); - render_trampolines(w, cb, domain, prefix); + render_callback_class(w, ctx, cb); + render_trampolines(w, ctx, cb); +} + +/// The `@throws` paragraph of a callback method that declares errors. +fn throws_note(error: &ErrorStrategy) -> Option { + match error { + ErrorStrategy::Trap => None, + ErrorStrategy::Untyped => { + Some("@throws std::exception to report a failure, with `what()` as its message.".into()) + } + ErrorStrategy::Domain(name) => Some(format!( + "@throws {} (or one of its codes' classes) to report one of the domain's codes, \ + fields included; any other exception reports a plain failure.", + cpp_error_class(name) + )), + } } /// Append the abstract class a consumer subclasses: a virtual destructor and /// one pure virtual method per IDL method. Strings arrive as views valid for -/// the call, buffered values by const reference, and objects by value as -/// wrappers the implementation owns; every family can be returned. -fn render_callback_class( - w: &mut CodeWriter, - cb: &CallbackInterfaceBinding, - domain: Option<&ErrorBinding>, -) { +/// the call, typed arrays, bytes, and buffered values by const reference, +/// optional scalars by value, and objects by value as wrappers the +/// implementation owns. +fn render_callback_class(w: &mut CodeWriter, ctx: &Ctx<'_>, cb: &CallbackInterfaceBinding) { let name = &cb.name; - let throws = match domain.filter(|_| cb.methods.iter().any(|m| m.throws)) { - Some(eb) => format!( - " A method that declares errors may throw a `{}` code's exception, \ - which reaches the producer with its fields; any other exception \ - reaches it as a callback failure (code -4).", - eb.type_name - ), - None => " An exception a method throws reaches the producer as a callback \ - failure (code -4)." - .to_string(), - }; let usage = format!( "Subclass it and pass a `std::shared_ptr<{name}>` where the API takes a \ `{name}`. The producer may call the methods from any thread, so they must \ - be thread-safe, until it releases the implementation (also from any \ - thread).{throws}" + be thread-safe, and releases the implementation (also from any thread) \ + once it's done with it. An exception a method throws never unwinds into \ + the producer: a method that declares errors reports it as described on \ + the method, and any other method reports a callback failure (-4)." ); - let doc = match doc_text(cb.doc.as_deref(), cb.deprecated.as_deref()) { + let doc = match type_doc(ctx, &cb.doc, &cb.deprecated) { Some(d) => format!("{d}\n\n{usage}"), None => usage, }; @@ -71,15 +73,11 @@ fn render_callback_class( w.line(format!("virtual ~{name}() = default;")); for m in &cb.methods { w.blank(); - let mut doc = doc_text(m.doc.as_deref(), m.deprecated.as_deref()); - if let Some(eb) = domain.filter(|_| m.throws) { - let tag = format!( - "@throws {} to report one of the domain's codes to the producer.", - eb.type_name - ); + let mut doc = type_doc(ctx, &m.doc, &m.deprecated); + if let Some(note) = throws_note(&m.error) { doc = Some(match doc { - Some(d) => format!("{d}\n\n{tag}"), - None => tag, + Some(d) => format!("{d}\n\n{note}"), + None => note, }); } w.doc(&doc, DocCommentStyle::Javadoc); @@ -87,7 +85,7 @@ fn render_callback_class( let params: Vec = m .params .iter() - .map(|p| cpp_cb_param_decl(&p.ty, &cpp_ident(&p.name))) + .map(|p| cpp_cb_param_decl(&p.ty, &p.pass, &cpp_ident(&p.name))) .collect(); w.line(format!( "virtual {ret} {}({}) = 0;", @@ -100,230 +98,215 @@ fn render_callback_class( w.blank(); } -/// Append, in `detail`, the trampolines and the static vtable of a callback -/// interface. +/// The trampoline name of a method: its vtable field name, kept clear of +/// the `vtable()` accessor beside it. +fn trampoline_name(m: &CallbackMethodBinding) -> String { + lang::escape_member(&cpp_ident(&m.abi.symbol), &["vtable"]) +} + +/// Append, in `detail`, the `Callbacks` specialization of a callback +/// interface: one trampoline per method and the static vtable. /// -/// `ctx` is a heap-allocated `std::shared_ptr` box made when the +/// `ctx` is a heap-allocated `std::shared_ptr` box made when the /// implementation is passed to the producer; the vtable's `free` deletes -/// it. Each trampoline receives its arguments per the callback protocol, -/// calls the method, and hands the return back through the C return or the -/// `out_ptr`/`out_len` slots (a run from `{prefix}_alloc`). Any exception is -/// reported through `out_err` and nothing unwinds through the C frame. -fn render_trampolines( - w: &mut CodeWriter, - cb: &CallbackInterfaceBinding, - domain: Option<&ErrorBinding>, - prefix: &str, -) { +/// it. The vtable's `flags` are 0: C++ implementations may be called from +/// any thread. +fn render_trampolines(w: &mut CodeWriter, ctx: &Ctx<'_>, cb: &CallbackInterfaceBinding) { let name = &cb.name; - let strukt = trampoline_struct(name); let vtable = &cb.vtable_tag; w.line("namespace detail {"); w.blank(); w.line(format!( - "/** Trampolines adapting a `{name}` implementation to `{vtable}`. */" + "/** Adapts a `{name}` implementation to `{vtable}`. */" )); - w.block(format!("struct {strukt} {{"), "};", |w| { + w.line("template <>"); + w.block(format!("struct Callbacks<{name}> {{"), "};", |w| { for m in &cb.methods { - render_trampoline(w, cb, m, domain.filter(|_| m.throws), prefix); + render_trampoline(w, ctx, cb, m); } - w.line("/** Deletes the implementation box once the producer releases it. */"); - w.block("static void free_ctx(void* ctx) {", "}", |w| { - w.line(format!( - "delete static_cast*>(ctx);" - )); - }); + w.line(format!( + "/** The vtable every `{name}` implementation is passed with. */" + )); + w.block( + format!("static const {vtable}& vtable() noexcept {{"), + "}", + |w| { + let mut entries = vec![ + format!("sizeof({vtable})"), + "0".to_string(), + format!("&release<{name}>"), + ]; + entries.extend( + cb.methods + .iter() + .map(|m| format!("&{}", trampoline_name(m))), + ); + w.block(format!("static const {vtable} table = {{"), "};", |w| { + for entry in &entries { + w.line(format!("{entry},")); + } + }); + w.line("return table;"); + }, + ); }); w.blank(); - w.line(format!( - "/** The vtable every `{name}` implementation is passed with. */" - )); - w.block( - format!("inline const {vtable}& {}() {{", vtable_accessor(name)), - "}", - |w| { - w.block(format!("static const {vtable} vtable = {{"), "};", |w| { - w.line(format!("static_cast(sizeof({vtable})),")); - w.line("0,"); - w.line(format!("&{strukt}::free_ctx,")); - for m in &cb.methods { - w.line(format!("&{strukt}::{},", cpp_ident(&m.name))); - } - }); - w.line("return vtable;"); - }, - ); - w.blank(); w.line("} // namespace detail"); w.blank(); } -/// Append one trampoline: a static function with the vtable entry's exact C -/// signature. `domain` is set when the method declares errors. +/// The expression handing one argument to the implementation, per its +/// [`ArgPass`]: a string viewed in place, a typed array or bytes copied, an +/// optional scalar or enum lifted from its flag and value, a decoded buffer +/// or adopted object moved in, and a direct value converted. +fn trampoline_arg(ty: &Ty, pass: &ArgPass, local: &str) -> String { + match pass { + ArgPass::Buffer { .. } | ArgPass::Object { .. } => format!("std::move({local})"), + ArgPass::String { ptr, len } => { + format!("borrow_string({}, {})", slot_name(ptr), slot_name(len)) + } + ArgPass::Bytes { ptr, len } => { + format!("borrow_bytes({}, {})", slot_name(ptr), slot_name(len)) + } + ArgPass::Slice { ptr, len, .. } => { + format!("borrow_slice({}, {})", slot_name(ptr), slot_name(len)) + } + ArgPass::OptDirect { has, value, inner } => format!( + "lift_optional<{}>({}, {})", + cpp_type(inner), + slot_name(has), + slot_name(value) + ), + ArgPass::Direct { slot } => match ty { + Ty::Enum(e) => format!("static_cast<{e}>({})", slot_name(slot)), + _ => slot_name(slot), + }, + ArgPass::Callback { .. } => { + unreachable!("callback interfaces are never callback-method parameters") + } + } +} + +/// Append one trampoline: a static function with the vtable entry's exact +/// C signature. fn render_trampoline( w: &mut CodeWriter, + ctx: &Ctx<'_>, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding, - domain: Option<&ErrorBinding>, - prefix: &str, ) { let iface = &cb.name; - let method = cpp_fn_name(&m.name); - let c_ret = m.abi_ret.render_c(prefix); - let slots = &m.abi_params; - let ctx = slot_name(&slots[0]); - let out_err = slot_name(&slots[slots.len() - 1]); - let ret_pass = RetPass::of(m.ret.as_ref()); - let returns_run = matches!(ret_pass, RetPass::String | RetPass::Bytes | RetPass::Buffer); - let (out_ptr, out_len) = if returns_run { - ( - slot_name(&slots[slots.len() - 3]), - slot_name(&slots[slots.len() - 2]), - ) - } else { - (String::new(), String::new()) - }; - let params = render_param_decls(slots, prefix).join(", "); + let c_ret = m.abi.ret.render_c(ctx.prefix); + let params: Vec = m + .abi + .params + .iter() + .map(|p| slot_decl(p, ctx.prefix)) + .collect(); + let local = |name: &str| format!("{}_arg", cpp_ident(name)); w.block( - format!("static {c_ret} {}({params}) {{", cpp_ident(&m.name)), + format!("static {c_ret} {}({}) {{", trampoline_name(m), params.join(", ")), "}", |w| { // Object arguments each transfer one strong reference. Adopting // can't throw, so it comes first, which releases every reference // even when another argument fails to decode. for p in &m.params { - if let ArgPass::Object { slot, nullable } = p.arg_pass() { - let class = p.ty.interface_name().expect("objects name an interface"); + if let ArgPass::Object { + slot, + nullable, + interface, + } = &p.pass + { let slot = slot_name(slot); - let var = format!("{}_arg", p.name); - if nullable { - w.line(format!("std::optional<{class}> {var};")); - w.line(format!("if ({slot} != nullptr) {var}.emplace(adopt, {slot});")); + let arg = local(&p.name); + if *nullable { + w.line(format!( + "std::optional<{interface}> {arg} = adopt_optional<{interface}>({slot});" + )); } else { - w.line(format!("{class} {var}(adopt, {slot});")); + w.line(format!("{interface} {arg}(adopt, {slot});")); } } } - w.line("try {"); + let errors = error_class(&m.error); + let returns = !matches!( + m.ret_pass, + CallbackRetPass::Void + | CallbackRetPass::Slice { .. } + | CallbackRetPass::String { .. } + | CallbackRetPass::Bytes { .. } + | CallbackRetPass::Buffer { .. } + ); + let lead = if returns { "return " } else { "" }; + w.line(format!("{lead}callback<{errors}>(out_err, [&] {{")); w.scope(|w| { - w.line(format!( - "{iface}& impl = **static_cast*>({ctx});" - )); // Buffers carrying object tokens decode first, so a failure // decoding another argument still adopts their references. let mut order: Vec<_> = m.params.iter().collect(); order.sort_by_key(|p| !p.ty.contains_object()); for p in order { - if let ArgPass::Buffer { ptr, len } = p.arg_pass() { + if let ArgPass::Buffer { ptr, len } = &p.pass { w.line(format!( - "{} {}_arg = detail::decode({}, {}, &{});", + "auto {} = decode<{}>({}, {});", + local(&p.name), cpp_type(&p.ty), - p.name, slot_name(ptr), - slot_name(len), - read_fn(&p.ty) + slot_name(len) )); } } - let args: Vec = m.params.iter().map(trampoline_arg).collect(); - let call = format!("impl.{method}({})", args.join(", ")); - match (&m.ret, ret_pass.clone()) { - (None, _) => { - w.line(format!("{call};")); - } - (Some(Ty::Enum(_)), _) => { - w.line(format!( - "return static_cast<{c_ret}>(static_cast({call}));" - )); - } - (Some(_), RetPass::Direct) => { - w.line(format!("return {call};")); - } - (Some(_), RetPass::Object { nullable: false, .. }) => { - // A moved-from wrapper returns null, which the - // producer rejects (-3). - w.line(format!("return {call}.clone_handle();")); - } - (Some(ty), RetPass::Object { nullable: true, .. }) => { - w.line(format!("{} ret = {call};", cpp_type(ty))); - w.line("return ret.has_value() ? ret->clone_handle() : nullptr;"); - } - (Some(ty), RetPass::String | RetPass::Bytes) => { - w.line(format!("{} ret = {call};", cpp_type(ty))); - w.line(format!( - "detail::hand_over(ret.data(), ret.size(), {out_ptr}, {out_len});" - )); + let args: Vec = m + .params + .iter() + .map(|p| trampoline_arg(&p.ty, &p.pass, &local(&p.name))) + .collect(); + let call = format!( + "implementation<{iface}>(ctx).{}({})", + cpp_fn_name(&m.name), + args.join(", ") + ); + let line = match &m.ret_pass { + CallbackRetPass::Void => format!("{call};"), + CallbackRetPass::Direct => match &m.ret { + Some(Ty::Enum(_)) => format!("return static_cast({call});"), + _ => format!("return {call};"), + }, + CallbackRetPass::OptDirect { out_value } => { + format!("return give_optional({call}, {});", slot_name(out_value)) } - (Some(ty), RetPass::Buffer) => { - w.line(format!("{} ret = {call};", cpp_type(ty))); - w.line("detail::BufferWriter ret_buf;"); - w.line(write_stmt(ty, "ret", "ret_buf")); - w.line(format!( - "detail::hand_over(ret_buf.data(), ret_buf.size(), {out_ptr}, {out_len});" - )); + CallbackRetPass::Slice { + out_ptr, out_len, .. + } => format!( + "give_slice({call}, {}, {});", + slot_name(out_ptr), + slot_name(out_len) + ), + CallbackRetPass::String { out_ptr, out_len } + | CallbackRetPass::Bytes { out_ptr, out_len } => format!( + "give({call}, {}, {});", + slot_name(out_ptr), + slot_name(out_len) + ), + CallbackRetPass::Buffer { out_ptr, out_len } => format!( + "give(encode({call}), {}, {});", + slot_name(out_ptr), + slot_name(out_len) + ), + // A moved-from wrapper returns null, which the producer + // rejects (-3). + CallbackRetPass::Object { + nullable: false, .. + } => format!("return {call}.clone_handle();"), + CallbackRetPass::Object { nullable: true, .. } => { + format!("return clone_of({call});") } - (Some(_), RetPass::Void) => unreachable!("a return type is never void"), - } + }; + w.line(line); }); - if let Some(eb) = domain { - w.line(format!("}} catch (const {}& e) {{", eb.type_name)); - w.scope(|w| { - w.line(format!("{}(e, {out_err});", report_fn(eb))); - }); - } - w.line("} catch (const std::exception& e) {"); - w.scope(|w| { - w.line(format!( - "{prefix}_error_set({out_err}, {FOREIGN_ERROR_CODE}, e.what());" - )); - }); - w.line("} catch (...) {"); - w.scope(|w| { - w.line(format!( - "{prefix}_error_set({out_err}, {FOREIGN_ERROR_CODE}, \"{iface}::{method} threw a non-standard exception\");" - )); - }); - w.line("}"); - match ret_pass { - RetPass::Direct => { - w.line(format!("return {c_ret}{{}};")); - } - RetPass::Object { .. } => { - w.line("return nullptr;"); - } - _ => {} - } + w.line("});"); }, ); w.blank(); } - -/// The expression handing one argument to the implementation: a string -/// viewed in place, bytes copied, a decoded buffer or adopted object moved -/// in, and a direct value converted. -fn trampoline_arg(p: &weaveffi_model::model::ParamBinding) -> String { - match p.arg_pass() { - ArgPass::Buffer { .. } | ArgPass::Object { .. } => format!("std::move({}_arg)", p.name), - ArgPass::String { ptr, len } => format!( - "detail::borrow_string({}, {})", - slot_name(ptr), - slot_name(len) - ), - ArgPass::Bytes { ptr, len } => format!( - "detail::borrow_bytes({}, {})", - slot_name(ptr), - slot_name(len) - ), - ArgPass::Direct { slot } => match &p.ty { - Ty::Enum(e) => format!( - "static_cast<{e}>(static_cast({}))", - slot_name(slot) - ), - _ => slot_name(slot), - }, - ArgPass::Callback { .. } => { - unreachable!("callback interfaces are never callback-method parameters") - } - } -} diff --git a/crates/weaveffi-cli/src/targets/cpp/calls.rs b/crates/weaveffi-cli/src/targets/cpp/calls.rs index 6a8110aa..01f61866 100644 --- a/crates/weaveffi-cli/src/targets/cpp/calls.rs +++ b/crates/weaveffi-cli/src/targets/cpp/calls.rs @@ -1,6 +1,7 @@ //! Callable rendering: free functions and interface members in the sync, -//! iterator, and async call shapes, each marshalled per the shared passing -//! plans ([`ArgPass`], [`RetPass`], `IteratorProtocol`, `AsyncProtocol`). +//! iterator, and async call shapes, each marshalled per the passing +//! contracts the model stores ([`ArgPass`], [`RetPass`], [`ResultPass`], +//! [`ItemPass`]) and failing per its [`ErrorStrategy`]. //! //! Interface members are rendered twice: a declaration inside the class body //! (so the class is complete before any record that holds it by value) and an @@ -8,67 +9,203 @@ //! Free functions are defined inline inside their module namespace, which is //! rendered last. +use std::collections::HashSet; + use crate::codegen::common::DocCommentStyle; +use crate::codegen::docs::Doc; use crate::codegen::CodeWriter; -use heck::ToUpperCamelCase; -use weaveffi_model::model::{ - AbiFn, AsyncBinding, CallShape, ErrorBinding, FnBinding, IteratorBinding, ModuleBinding, - ParamBinding, -}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; -use weaveffi_model::ty::Ty; +use weaveffi_model::abi::AbiParam; +use weaveffi_model::model::{AsyncBinding, FnBinding, IteratorBinding, ModuleBinding}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{ParamTy, Ty}; -use crate::targets::cpp::codec::{read_fn, write_stmt}; -use crate::targets::cpp::entities::{check_fn, make_error_fn}; use crate::targets::cpp::types::{ - cpp_fn_name, cpp_ident, cpp_namespace_path, cpp_param_decl, cpp_type, render_param_decls, - slot_name, vtable_accessor, + cpp_error_class, cpp_fn_name, cpp_ident, cpp_namespace_path, cpp_param_decl, cpp_ret_type, + cpp_type, error_class, pointee, slot_decl, slot_name, Ctx, }; -// ── Error routing ── +// ── Receiving values ── -/// The domain a callable reports typed errors from: the one in scope when -/// it's declared `throws`, else `None` (its failures are bugs). -fn throwing_domain<'a>(f: &FnBinding, error: Option<&'a ErrorBinding>) -> Option<&'a ErrorBinding> { - match f.error_strategy() { - ErrorStrategy::Throws => error, - ErrorStrategy::Trap => None, +/// The C slots a value arrives in, whatever the position (a return, an +/// async result, an iterator item): the common shape of [`RetPass`], +/// [`ResultPass`], and [`ItemPass`], with each slot as the C++ expression +/// holding it. +pub(crate) enum Recv<'a> { + /// A scalar, `bool`, or C-style enum. + Direct(String), + /// An optional scalar, `bool`, or C-style enum: presence and value. + OptDirect(String, String), + /// An owned typed array: pointer and element count. + Slice(String, String), + /// An owned UTF-8 run: pointer and byte length. + String(String, String), + /// An owned byte run: pointer and byte length. + Bytes(String, String), + /// An owned value buffer: pointer and byte length. + Buffer(String, String), + /// One strong object reference. + Object { + /// The pointer. + ptr: String, + /// Whether null is a legal "none". + nullable: bool, + /// The interface's name. + interface: &'a str, + }, +} + +/// The C++ value of a received `ty` held in C slots `recv`, adopting what +/// they own: strings, bytes, typed arrays, and buffers are copied or decoded +/// and then released, an object is adopted into its wrapper, and a C-style +/// enum converts from its discriminant. +pub(crate) fn lift(ty: &Ty, recv: Recv<'_>) -> String { + match recv { + Recv::Direct(v) => match ty { + Ty::Enum(n) => format!("static_cast<{n}>({v})"), + _ => v, + }, + Recv::OptDirect(has, v) => { + let inner = match ty { + Ty::Optional(inner) => cpp_type(inner), + other => cpp_type(other), + }; + format!("detail::lift_optional<{inner}>({has}, {v})") + } + Recv::Slice(p, n) => format!("detail::take_slice({p}, {n})"), + Recv::String(p, n) => format!("detail::take_string({p}, {n})"), + Recv::Bytes(p, n) => format!("detail::take_bytes({p}, {n})"), + Recv::Buffer(p, n) => format!("detail::take<{}>({p}, {n})", cpp_type(ty)), + Recv::Object { + ptr, + nullable: true, + interface, + } => format!("detail::adopt_optional<{interface}>({ptr})"), + Recv::Object { + ptr, + nullable: false, + interface, + } => format!("{interface}(adopt, {ptr})"), } } -/// The `detail::check*` helper a wrapper calls after the C call returns: the -/// domain's (throwing its typed exceptions) for a throwing callable, the -/// `InternalError` check otherwise. -fn check_helper(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - throwing_domain(f, error).map_or_else(|| "detail::check".to_string(), check_fn) +/// A sync return's [`Recv`]: `result` is the C return, and the out slots +/// are locals named after them. +fn ret_recv<'a>(pass: &'a RetPass, result: &str) -> Option> { + let result = result.to_string(); + Some(match pass { + RetPass::Void | RetPass::Iterator(_) => return None, + RetPass::Direct => Recv::Direct(result), + RetPass::OptDirect { out_value } => Recv::OptDirect(result, slot_name(out_value)), + RetPass::Slice { out_len, .. } => Recv::Slice(result, slot_name(out_len)), + RetPass::String { out_len } => Recv::String(result, slot_name(out_len)), + RetPass::Bytes { out_len } => Recv::Bytes(result, slot_name(out_len)), + RetPass::Buffer { out_len } => Recv::Buffer(result, slot_name(out_len)), + RetPass::Object { + nullable, + interface, + .. + } => Recv::Object { + ptr: result, + nullable: *nullable, + interface, + }, + }) } -/// The expression an async completion uses to turn its boxed `err` into the -/// `std::exception_ptr` it settles the promise with. -fn make_error_call(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match throwing_domain(f, error) { - Some(eb) => format!( - "{}(err->code, message, err->payload_ptr, err->payload_len)", - make_error_fn(eb) - ), - None => "detail::make_internal_error(err->code, message)".to_string(), +/// An async result's [`Recv`]: the completion's parameters. +fn result_recv(pass: &ResultPass) -> Option> { + let s = slot_name; + Some(match pass { + ResultPass::Void => return None, + ResultPass::Direct { result } => Recv::Direct(s(result)), + ResultPass::OptDirect { has, value } => Recv::OptDirect(s(has), s(value)), + ResultPass::Slice { ptr, len, .. } => Recv::Slice(s(ptr), s(len)), + ResultPass::String { ptr, len } => Recv::String(s(ptr), s(len)), + ResultPass::Bytes { ptr, len } => Recv::Bytes(s(ptr), s(len)), + ResultPass::Buffer { ptr, len } => Recv::Buffer(s(ptr), s(len)), + ResultPass::Object { + result, + nullable, + interface, + .. + } => Recv::Object { + ptr: s(result), + nullable: *nullable, + interface, + }, + }) +} + +/// An iterator item's [`Recv`]: the out slots of `_next`, as locals named +/// after them. +fn item_recv(pass: &ItemPass) -> Recv<'_> { + let s = slot_name; + match pass { + ItemPass::Direct { out_item } => Recv::Direct(s(out_item)), + ItemPass::OptDirect { out_has, out_item } => Recv::OptDirect(s(out_has), s(out_item)), + ItemPass::Slice { + out_item, out_len, .. + } => Recv::Slice(s(out_item), s(out_len)), + ItemPass::String { out_item, out_len } => Recv::String(s(out_item), s(out_len)), + ItemPass::Bytes { out_item, out_len } => Recv::Bytes(s(out_item), s(out_len)), + ItemPass::Buffer { out_item, out_len } => Recv::Buffer(s(out_item), s(out_len)), + ItemPass::Object { + out_item, + nullable, + interface, + .. + } => Recv::Object { + ptr: s(out_item), + nullable: *nullable, + interface, + }, } } -// ── Parameter and return marshalling ── +/// Append a zero-initialized local for each out slot (`T* out_value` is a +/// `T out_value{}`) and return the arguments that pass their addresses. +fn out_locals(w: &mut CodeWriter, slots: &[&AbiParam], prefix: &str) -> Vec { + slots + .iter() + .map(|slot| { + let name = slot_name(slot); + w.line(format!("{} {name}{{}};", pointee(slot, prefix))); + format!("&{name}") + }) + .collect() +} + +// ── Locals ── + +/// The names a wrapper body declares, kept apart from its parameters: a +/// local gets a trailing underscore while its preferred name is taken. +struct Locals(HashSet); + +impl Locals { + fn new(f: &FnBinding) -> Self { + Self(f.params.iter().map(|p| cpp_ident(&p.name)).collect()) + } -/// The wrapper class behind an object-passed type: the interface itself, or -/// the interface inside `Interface?`. -fn object_class(ty: &Ty) -> &str { - ty.interface_name() - .expect("object passing only applies to interfaces") + fn fresh(&mut self, base: &str) -> String { + let mut name = base.to_string(); + while self.0.contains(&name) { + name.push('_'); + } + self.0.insert(name.clone()); + name + } } +// ── Parameters ── + /// Append the setup statements for one parameter and return the C argument -/// expressions its ABI slots receive, dispatching on its [`ArgPass`]. +/// expressions its slots receive, per its [`ArgPass`]. /// -/// * A string passes its UTF-8 pointer and length (no terminator, so -/// interior NULs survive); bytes likewise. +/// * A typed array passes its vector's storage and element count; bytes +/// likewise; a string passes its UTF-8 pointer and length (no +/// terminator, so interior NULs survive). +/// * An optional scalar passes its presence and its value (zero when +/// absent). /// * A buffered value is encoded into a local `detail::BufferWriter` that /// outlives the call. /// * An object is borrowed: the wrapper's pointer is passed and the wrapper @@ -77,87 +214,55 @@ fn object_class(ty: &Ty) -> &str { /// box that becomes `ctx`; the producer deletes it through the vtable's /// `free`. A `std::unique_ptr` owns the box until the argument list /// releases it, so a throw while marshalling a later parameter frees it. -/// An optional callback interface passes a null vtable for an empty -/// pointer. -fn emit_param_setup(w: &mut CodeWriter, p: &ParamBinding, prefix: &str) -> Vec { - let name = cpp_ident(&p.name); - match p.arg_pass() { - ArgPass::Buffer { .. } => { - let buf = format!("{name}_buf"); - w.line(format!("detail::BufferWriter {buf};")); - w.line(write_stmt(&p.ty, &name, &buf)); - vec![format!("{buf}.data()"), format!("{buf}.size()")] +/// An optional callback interface passes a null vtable for none. +fn emit_param_setup( + w: &mut CodeWriter, + locals: &mut Locals, + ty: &ParamTy, + pass: &ArgPass, + name: &str, +) -> Vec { + match pass { + ArgPass::Direct { .. } => match ty { + ParamTy::Value(Ty::Enum(_)) => vec![format!("static_cast({name})")], + _ => vec![name.to_string()], + }, + ArgPass::OptDirect { .. } => vec![ + format!("{name}.has_value()"), + format!("detail::value_or_zero({name})"), + ], + ArgPass::Slice { .. } | ArgPass::Bytes { .. } => { + vec![format!("{name}.data()"), format!("{name}.size()")] } ArgPass::String { .. } => vec![format!("detail::utf8({name})"), format!("{name}.size()")], - ArgPass::Bytes { .. } => vec![format!("{name}.data()"), format!("{name}.size()")], - ArgPass::Object { nullable: true, .. } => { - vec![format!("{name}.has_value() ? {name}->handle() : nullptr")] + ArgPass::Buffer { .. } => { + let buf = locals.fresh(&format!("{name}_buf")); + w.line(format!("const auto {buf} = detail::encode({name});")); + vec![format!("{buf}.data()"), format!("{buf}.size()")] } + ArgPass::Object { nullable: true, .. } => vec![format!("detail::handle_of({name})")], ArgPass::Object { nullable: false, .. } => vec![format!("{name}.handle()")], - ArgPass::Callback { nullable, .. } => { - let iface = - p.ty.callback_interface_name() - .expect("callback passing only applies to callback interfaces"); - let vtable = format!("&detail::{}()", vtable_accessor(iface)); - let ctx = format!("{name}_ctx"); - if nullable { - w.line(format!( - "const auto* {name}_vtable = {name} ? {vtable} : nullptr;" - )); - w.line(format!( - "std::unique_ptr> {ctx}({name} ? new std::shared_ptr<{iface}>(std::move({name})) : nullptr);" - )); - vec![ - format!("static_cast({ctx}.release())"), - format!("{name}_vtable"), - ] + ArgPass::Callback { + nullable, + interface, + .. + } => { + let ctx = locals.fresh(&format!("{name}_ctx")); + let vtable = format!("&detail::Callbacks<{interface}>::vtable()"); + if *nullable { + let vt = locals.fresh(&format!("{name}_vtable")); + w.line(format!("auto {ctx} = detail::lend(std::move({name}));")); + w.line(format!("const auto* {vt} = {ctx} ? {vtable} : nullptr;")); + vec![format!("{ctx}.release()"), vt] } else { w.line(format!( - "if (!{name}) throw std::invalid_argument(\"{name}: null callback interface\");" + "auto {ctx} = detail::lend_required(std::move({name}), \"{name}\");" )); - w.line(format!( - "auto {ctx} = std::make_unique>(std::move({name}));" - )); - vec![format!("static_cast({ctx}.release())"), vtable] + vec![format!("{ctx}.release()"), vtable] } } - ArgPass::Direct { slot } => match &p.ty { - Ty::Enum(_) => vec![format!( - "static_cast<{}>(static_cast({name}))", - slot.ty.render_c(prefix) - )], - _ => vec![name], - }, - } -} - -/// The C++ value of a returned element held in C slots `value` (and `len` -/// for strings, bytes, and buffers), per its [`RetPass`]: strings, bytes, and -/// buffers are copied or decoded and then released, an object is adopted, -/// and a direct value converts. `Void` has no value. -fn received_value(ty: &Ty, value: &str, len: &str) -> String { - match RetPass::of(Some(ty)) { - RetPass::Buffer => format!("detail::take({value}, {len}, &{})", read_fn(ty)), - RetPass::String => format!("detail::take_string({value}, {len})"), - RetPass::Bytes => format!("detail::take_bytes({value}, {len})"), - RetPass::Object { - nullable: false, .. - } => { - format!("{}(adopt, {value})", object_class(ty)) - } - RetPass::Object { nullable: true, .. } => { - let class = object_class(ty); - format!( - "{value} != nullptr ? std::optional<{class}>(std::in_place, adopt, {value}) : std::nullopt" - ) - } - RetPass::Direct => match ty { - Ty::Enum(n) => format!("static_cast<{n}>({value})"), - _ => value.to_string(), - }, - RetPass::Void => unreachable!("void returns carry no value"), } } @@ -187,41 +292,18 @@ pub(crate) enum FnKind<'a> { }, } -impl<'a> FnKind<'a> { - /// The owning class, for member kinds. - fn class(self) -> Option<&'a str> { - match self { - FnKind::Free => None, - FnKind::Method { class } | FnKind::Static { class } | FnKind::Ctor { class } => { - Some(class) - } - } - } -} - -/// The name of the range class an iterator-returning callable yields: -/// `{PascalName}Iterator` for a free function and -/// `{Class}{PascalName}Iterator` for an interface member. -pub(crate) fn iterator_class_name(f: &FnBinding, kind: FnKind<'_>) -> String { - let pascal = f.name.to_upper_camel_case(); - match kind.class() { - Some(class) => format!("{class}{pascal}Iterator"), - None => format!("{pascal}Iterator"), - } -} - /// The C++ return type of a callable: the mapped type (or `void`) for sync, -/// `std::future` for async, and the range class for an iterator. `None` -/// for a constructor. +/// `Range` for an iterator, and `std::future` for async. `None` for a +/// constructor. fn return_type(f: &FnBinding, kind: FnKind<'_>) -> Option { if matches!(kind, FnKind::Ctor { .. }) { return None; } - let value = || f.ret.as_ref().map_or("void".to_string(), cpp_type); - Some(match &f.shape { - CallShape::Sync(_) => value(), - CallShape::Async(_) => format!("std::future<{}>", value()), - CallShape::Iterator(_) => iterator_class_name(f, kind), + let value = f.ret.as_ref().map_or("void".to_string(), cpp_ret_type); + Some(if f.is_async() { + format!("std::future<{value}>") + } else { + value }) } @@ -233,9 +315,9 @@ fn param_decls(f: &FnBinding, with_defaults: bool) -> String { let mut decls: Vec = f .params .iter() - .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name))) + .map(|p| cpp_param_decl(&p.ty, &p.pass, &cpp_ident(&p.name))) .collect(); - if f.is_async() && f.cancellable { + if f.cancellable() { let default = if with_defaults { " = CancelToken::none()" } else { @@ -246,53 +328,57 @@ fn param_decls(f: &FnBinding, with_defaults: bool) -> String { decls.join(", ") } +/// The `@throws` line of a callable that declares errors. +fn throws_tag(error: &ErrorStrategy) -> Option { + match error { + ErrorStrategy::Trap => None, + ErrorStrategy::Untyped => Some( + "@throws Error with code -1 and the producer's message when the call fails.".into(), + ), + ErrorStrategy::Domain(name) => Some(format!( + "@throws {} (or one of its codes' classes) when the call fails with one of the \ + domain's codes.", + cpp_error_class(name) + )), + } +} + /// Append the doc comment and any `[[deprecated]]` attribute of a callable: /// its IDL doc, `@param` for each documented parameter, what an iterator or /// async call returns, and the exceptions it throws. -fn emit_callable_attrs( - w: &mut CodeWriter, - f: &FnBinding, - kind: FnKind<'_>, - error: Option<&ErrorBinding>, -) { - let mut sections: Vec = f.doc.iter().cloned().collect(); +fn emit_callable_attrs(w: &mut CodeWriter, ctx: &Ctx<'_>, f: &FnBinding) { + let spell = |s: &str| ctx.spell(s); + let doc = Doc::new(&f.doc, &f.deprecated); + let mut sections: Vec = doc.text(spell).into_iter().collect(); let mut tags: Vec = f .params .iter() .filter_map(|p| { - p.doc - .as_ref() + Doc::new(&p.doc, &None) + .text(spell) .map(|d| format!("@param {} {d}", cpp_ident(&p.name))) }) .collect(); - if f.is_async() && f.cancellable { + if f.cancellable() { tags.push("@param cancel_token Cancels the call; the future then throws Cancelled.".into()); } - match &f.shape { - CallShape::Iterator(_) => tags.push(format!( - "@return A lazy `{}` range that pulls one element per step and releases the \ - producer iterator when exhausted or destroyed.", - iterator_class_name(f, kind) - )), - CallShape::Async(_) => tags.push( + if f.iterator().is_some() { + tags.push("@return A lazy, single-pass range; each step makes one producer call.".into()); + } else if f.is_async() { + tags.push( "@return A future settled from a producer thread; get() rethrows the call's error." .into(), - ), - CallShape::Sync(_) => {} - } - if let Some(eb) = throwing_domain(f, error) { - tags.push(format!( - "@throws {} when the call fails with one of the domain's codes.", - eb.type_name - )); + ); } + tags.extend(throws_tag(&f.error)); if !tags.is_empty() { sections.push(tags.join("\n")); } - let doc = (!sections.is_empty()).then(|| sections.join("\n\n")); - w.doc(&doc, DocCommentStyle::Javadoc); - if let Some(msg) = &f.deprecated { - w.line(format!("[[deprecated(\"{}\")]]", msg.replace('"', "\\\""))); + let text = (!sections.is_empty()).then(|| sections.join("\n\n")); + w.doc(&text, DocCommentStyle::Javadoc); + if let Some(msg) = doc.deprecation(spell) { + let escaped = msg.replace('\\', "\\\\").replace('"', "\\\""); + w.line(format!("[[deprecated(\"{escaped}\")]]")); } } @@ -300,12 +386,12 @@ fn emit_callable_attrs( /// comment and deprecation marker. pub(crate) fn render_member_decl( w: &mut CodeWriter, + ctx: &Ctx<'_>, f: &FnBinding, cpp_name: &str, kind: FnKind<'_>, - error: Option<&ErrorBinding>, ) { - emit_callable_attrs(w, f, kind, error); + emit_callable_attrs(w, ctx, f); let params = param_decls(f, true); match (kind, return_type(f, kind)) { // Explicit, so no argument list converts into a new producer object @@ -331,22 +417,17 @@ pub(crate) fn render_member_decl( /// methods skip it: their receiver could only come from a checked call. pub(crate) fn render_definition( w: &mut CodeWriter, + ctx: &Ctx<'_>, f: &FnBinding, cpp_name: &str, kind: FnKind<'_>, - error: Option<&ErrorBinding>, - prefix: &str, ) { if matches!(kind, FnKind::Free) { - emit_callable_attrs(w, f, kind, error); + emit_callable_attrs(w, ctx, f); } let params = param_decls(f, matches!(kind, FnKind::Free)); let header = match (kind, return_type(f, kind)) { - // `raw_` starts null so a throw from the error check leaves - // nothing for the destructor to release. - (FnKind::Ctor { class }, _) => { - format!("inline {class}::{class}({params}) : raw_(nullptr) {{") - } + (FnKind::Ctor { class }, _) => format!("inline {class}::{class}({params}) {{"), (FnKind::Method { class }, Some(ret)) => { format!("inline {ret} {class}::{cpp_name}({params}) const {{") } @@ -360,272 +441,130 @@ pub(crate) fn render_definition( if !matches!(kind, FnKind::Method { .. }) { w.line("check_library();"); } + let mut locals = Locals::new(f); let mut c_args = Vec::new(); if matches!(kind, FnKind::Method { .. }) { - c_args.push("raw_".to_string()); + c_args.push("raw_.get()".to_string()); } for p in &f.params { - c_args.extend(emit_param_setup(w, p, prefix)); + c_args.extend(emit_param_setup( + w, + &mut locals, + &p.ty, + &p.pass, + &cpp_ident(&p.name), + )); } - match &f.shape { - CallShape::Sync(abi) => emit_sync_body(w, f, abi, kind, c_args, error, prefix), - CallShape::Iterator(it) => { - emit_iterator_launch_body(w, f, it, kind, c_args, error, prefix); - } - CallShape::Async(a) => emit_async_body(w, f, a, c_args, error, prefix), + if let Some(a) = f.async_binding() { + emit_async_body(w, ctx, f, a, &mut locals, c_args); + } else if let Some(it) = f.iterator() { + emit_iterator_body(w, ctx, f, it, &mut locals, c_args); + } else { + emit_sync_body(w, ctx, f, kind, &mut locals, c_args); } }); w.blank(); } /// Append a synchronous callable's call, error check, and return. A -/// constructor stores the returned reference in `raw_`. +/// constructor adopts the returned reference. fn emit_sync_body( w: &mut CodeWriter, + ctx: &Ctx<'_>, f: &FnBinding, - abi: &AbiFn, kind: FnKind<'_>, + locals: &mut Locals, mut c_args: Vec, - error: Option<&ErrorBinding>, - prefix: &str, ) { - // A string, bytes, or buffered return carries a trailing `size_t* out_len`. - let has_out_len = matches!( - RetPass::of(f.ret.as_ref()), - RetPass::Buffer | RetPass::String | RetPass::Bytes - ); - if has_out_len { - w.line("size_t out_len = 0;"); - c_args.push("&out_len".into()); - } - c_args.push("&err".into()); - w.line(format!("{prefix}_error err{{}};")); - let call = format!("{}({})", abi.symbol, c_args.join(", ")); - if f.ret.is_none() { - w.line(format!("{call};")); - } else { - w.line(format!("auto result = {call};")); - } - w.line(format!("{}(err);", check_helper(f, error))); - match (kind, &f.ret) { - (FnKind::Ctor { .. }, _) => { - w.line("raw_ = result;"); + let prefix = ctx.prefix; + let err = locals.fresh("err"); + w.line(format!("{prefix}_error {err}{{}};")); + c_args.extend(out_locals(w, &f.ret_pass.out_slots(), prefix)); + c_args.push(format!("&{err}")); + let call = format!("{}({})", f.abi.symbol, c_args.join(", ")); + let check = format!("detail::check<{}>({err});", error_class(&f.error)); + let ret = f.ret.as_ref().and_then(|r| r.value()); + match (kind, ret, &f.ret_pass) { + (_, None, _) | (_, _, RetPass::Void) => { + w.line(format!("{call};")); + w.line(check); } - (_, Some(ret)) => { - w.line(format!( - "return {};", - received_value(ret, "result", "out_len") - )); + (FnKind::Ctor { .. }, Some(_), _) => { + let result = locals.fresh("result"); + w.line(format!("auto* {result} = {call};")); + w.line(check); + w.line(format!("raw_.reset({result});")); + } + (_, Some(ty), pass) => { + let result = locals.fresh("result"); + w.line(format!("auto {result} = {call};")); + w.line(check); + let recv = ret_recv(pass, &result).expect("a value return has slots"); + w.line(format!("return {};", lift(ty, recv))); } - (_, None) => {} } } // ── Iterators ── -/// Append the lazy range class an iterator-returning callable yields. -/// -/// The range is move-only, owns the producer iterator, and pulls exactly one -/// element per step (`weaveffi_model::plan::IteratorProtocol`): -/// -/// * `begin()`/`end()` expose a single-pass input iterator with a sentinel -/// end, so `for (auto&& item : fn())` streams in constant memory. -/// * Each element is received per its family (strings, bytes, and buffers -/// copied or decoded then released; objects adopted). -/// * `_destroy` runs exactly once: eagerly on exhaustion or a `next` error, -/// from the destructor otherwise. -/// * Errors follow the callable's [`ErrorStrategy`]. -pub(crate) fn render_iterator_range( +/// Append an iterator-returning callable's body: call the launcher, check +/// its error, and wrap the producer iterator in a `Range` with two +/// captureless lambdas: one pulling an element through `_next` (receiving +/// it per its [`ItemPass`] and failing per the callable's +/// [`ErrorStrategy`]), and one releasing the iterator with `_destroy`. +fn emit_iterator_body( w: &mut CodeWriter, + ctx: &Ctx<'_>, f: &FnBinding, it: &IteratorBinding, - cpp_name: &str, - kind: FnKind<'_>, - error: Option<&ErrorBinding>, - prefix: &str, + locals: &mut Locals, + mut c_args: Vec, ) { + let prefix = ctx.prefix; let elem = cpp_type(&it.elem); - let class = iterator_class_name(f, kind); let tag = &it.iter_tag; - let destroy = &it.destroy_symbol; - let owner = match kind.class() { - Some(c) => format!("{c}::{cpp_name}()"), - None => format!("{cpp_name}()"), - }; - - w.doc( - &Some(format!( - "A lazy, move-only range over the `{elem}` elements `{owner}` produces.\n\n\ - Each step pulls one element from the producer, so results stream in \ - constant memory. The range releases the producer iterator exactly once: \ - when it's exhausted, or from the destructor when iteration stops early." - )), - DocCommentStyle::Javadoc, - ); - w.line(format!("class {class} {{")); - w.scope(|w| { - w.line(format!("{tag}* handle_;")); - w.blank(); - }); - w.line("public:"); + let errors = error_class(&f.error); + let err = locals.fresh("err"); + let iter = locals.fresh("iter"); + w.line(format!("{prefix}_error {err}{{}};")); + c_args.push(format!("&{err}")); + w.line(format!( + "{tag}* {iter} = {}({});", + f.abi.symbol, + c_args.join(", ") + )); + w.line(format!("detail::check<{errors}>({err});")); + // The lambdas are captureless, but their names stay clear of the + // wrapper's parameters and locals so nothing shadows. + let raw = locals.fresh("raw"); + let item = locals.fresh("item"); + let next_err = locals.fresh("next_err"); + w.line(format!( + "return Range<{elem}>(adopt, {iter}, [](void* {raw}, std::optional<{elem}>& {item}) {{" + )); w.scope(|w| { - w.line("/** Adopts a producer iterator. */"); - w.line(format!( - "explicit {class}(adopt_t, {tag}* h) noexcept : handle_(h) {{}}" - )); - w.blank(); - w.line("/** Releases the producer iterator if it's still held. */"); - w.block(format!("~{class}() {{"), "}", |w| { - w.line(format!("if (handle_ != nullptr) {destroy}(handle_);")); - }); - w.blank(); - w.line(format!("{class}(const {class}&) = delete;")); - w.line(format!("{class}& operator=(const {class}&) = delete;")); - w.blank(); - w.line("/** Transfers `other`'s iterator; `other` becomes empty. */"); - w.line(format!( - "{class}({class}&& other) noexcept : handle_(other.handle_) {{ other.handle_ = nullptr; }}" - )); - w.blank(); - w.line("/** Releases the current iterator and takes over `other`'s. */"); + w.line(format!("{prefix}_error {next_err}{{}};")); + let mut args = vec![format!("static_cast<{tag}*>({raw})")]; + args.extend(out_locals(w, &it.item.slots(), prefix)); + args.push(format!("&{next_err}")); w.block( - format!("{class}& operator=({class}&& other) noexcept {{"), + format!("if ({}({}) == 0) {{", it.next.symbol, args.join(", ")), "}", |w| { - w.block("if (this != &other) {", "}", |w| { - w.line(format!("if (handle_ != nullptr) {destroy}(handle_);")); - w.line("handle_ = other.handle_;"); - w.line("other.handle_ = nullptr;"); - }); - w.line("return *this;"); + w.line(format!("detail::check<{errors}>({next_err});")); + w.line("return false;"); }, ); - w.blank(); - render_iterator_next(w, f, it, error, prefix); - w.line("/** Sentinel type marking the end of the range. */"); - w.line("struct sentinel {};"); - w.blank(); - w.line("/** Single-pass input iterator; each increment pulls one element. */"); - w.line("class iterator {"); - w.scope(|w| { - w.line(format!("{class}* range_;")); - w.line(format!("std::optional<{elem}> current_;")); - w.blank(); - }); - w.line("public:"); - w.scope(|w| { - w.line("using iterator_category = std::input_iterator_tag;"); - w.line(format!("using value_type = {elem};")); - w.line("using difference_type = std::ptrdiff_t;"); - w.line(format!("using pointer = {elem}*;")); - w.line(format!("using reference = {elem}&;")); - w.blank(); - w.line("/** Binds to `range` and pulls the first element. */"); - w.line(format!( - "explicit iterator({class}* range) : range_(range), current_(range->next()) {{}}" - )); - w.line("reference operator*() { return *current_; }"); - w.line("pointer operator->() { return &*current_; }"); - w.line("iterator& operator++() { current_ = range_->next(); return *this; }"); - w.line("void operator++(int) { current_ = range_->next(); }"); - w.line("bool operator==(sentinel) const { return !current_.has_value(); }"); - w.line("bool operator!=(sentinel) const { return current_.has_value(); }"); - }); - w.line("};"); - w.blank(); - w.line("/** Begins iteration by pulling the first element. */"); - w.line("iterator begin() { return iterator(this); }"); - w.blank(); - w.line("/** The past-the-end sentinel. */"); - w.line("sentinel end() const { return sentinel{}; }"); - }); - w.line("};"); - w.blank(); -} - -/// Append an iterator-returning callable's launch: call the launcher, check -/// `out_err`, and wrap the returned pointer in the range class. -fn emit_iterator_launch_body( - w: &mut CodeWriter, - f: &FnBinding, - it: &IteratorBinding, - kind: FnKind<'_>, - mut c_args: Vec, - error: Option<&ErrorBinding>, - prefix: &str, -) { - c_args.push("&err".into()); - w.line(format!("{prefix}_error err{{}};")); - w.line(format!( - "{}* iter = {}({});", - it.iter_tag, - it.launch.symbol, - c_args.join(", ") - )); - w.line(format!("{}(err);", check_helper(f, error))); - w.line(format!( - "return {}(adopt, iter);", - iterator_class_name(f, kind) - )); -} - -/// Append the range class's `next()`: one producer `_next` call yielding the -/// received element, or `std::nullopt` on exhaustion, destroying the -/// iterator exactly once on exhaustion or error. -fn render_iterator_next( - w: &mut CodeWriter, - f: &FnBinding, - it: &IteratorBinding, - error: Option<&ErrorBinding>, - prefix: &str, -) { - let elem = cpp_type(&it.elem); - let destroy = &it.destroy_symbol; - let item_ty = it.item_ctype().render_c(prefix); - let raises = match throwing_domain(f, error) { - Some(eb) => eb.type_name.clone(), - None => "InternalError".to_string(), - }; - w.doc( - &Some(format!( - "Pulls the next element, or `std::nullopt` once exhausted (which releases \ - the producer iterator). A producer error releases the iterator and throws \ - {raises}." - )), - DocCommentStyle::Javadoc, - ); - w.block(format!("std::optional<{elem}> next() {{"), "}", |w| { - w.line("if (handle_ == nullptr) return std::nullopt;"); - w.line(format!("{prefix}_error err{{}};")); - w.line(format!("{item_ty} item{{}};")); - let mut args = vec!["handle_".to_string(), "&item".to_string()]; - if matches!( - RetPass::of(Some(&it.elem)), - RetPass::Buffer | RetPass::String | RetPass::Bytes - ) { - w.line("size_t item_len = 0;"); - args.push("&item_len".to_string()); - } - args.push("&err".to_string()); - w.line(format!( - "int32_t has_item = {}({});", - it.next.symbol, - args.join(", ") - )); - w.block("if (err.code != 0 || has_item == 0) {", "}", |w| { - w.line(format!("{destroy}(handle_);")); - w.line("handle_ = nullptr;"); - w.line(format!("{}(err);", check_helper(f, error))); - w.line("return std::nullopt;"); - }); - // A nullable object element yields an engaged outer optional holding - // an empty inner one for null, which is distinct from exhaustion. w.line(format!( - "return std::optional<{elem}>(std::in_place, {});", - received_value(&it.elem, "item", "item_len") + "{item}.emplace({});", + lift(&it.elem, item_recv(&it.item)) )); + w.line("return true;"); }); - w.blank(); + w.line(format!( + "}}, [](void* {raw}) {{ {}(static_cast<{tag}*>({raw})); }});", + it.destroy_symbol + )); } // ── Async ── @@ -633,85 +572,68 @@ fn render_iterator_next( /// Append an asynchronous callable's body: a `std::future` wrapper. /// /// The parameters are marshalled first, then a heap-allocated promise -/// travels through the C `context` and the completion adopts it back into a -/// `std::unique_ptr`, settling it exactly once. A failure settles the -/// promise with the typed domain exception (a throwing call), `Cancelled` -/// for -5, or `InternalError`. The completion owns everything it receives: -/// it releases the boxed error and any string or buffer result after copying -/// and adopts an object result. A cancellable call passes its token's native -/// handle (null for `CancelToken::none()`); the producer takes its own -/// reference. +/// travels through the C `context`, and the completion settles it exactly +/// once through `detail::settle`, which adopts the promise and the boxed +/// error, raises the call's exception for a failure, and otherwise receives +/// the result per its [`ResultPass`]. A cancellable call passes its token's +/// native handle (null for `CancelToken::none()`); the producer takes its +/// own reference. fn emit_async_body( w: &mut CodeWriter, + ctx: &Ctx<'_>, f: &FnBinding, a: &AsyncBinding, + locals: &mut Locals, mut c_args: Vec, - error: Option<&ErrorBinding>, - prefix: &str, ) { - let value = f.ret.as_ref().map_or("void".to_string(), cpp_type); - let promise = format!("std::promise<{value}>"); - let cb_params = render_param_decls(&a.callback_params, prefix).join(", "); - if f.cancellable { + let value = f + .ret + .as_ref() + .and_then(|r| r.value()) + .map_or("void".to_string(), cpp_type); + let promise = locals.fresh("promise"); + let future = locals.fresh("future"); + let params: Vec = a + .callback_params + .iter() + .map(|p| slot_decl(p, ctx.prefix)) + .collect(); + if a.cancellable() { c_args.push("cancel_token.handle()".to_string()); } - w.line(format!("auto promise = std::make_unique<{promise}>();")); - w.line("auto future = promise->get_future();"); - c_args.push(format!("[]({cb_params}) {{")); - w.line(format!("{}({}", a.launch.symbol, c_args.join(", "))); + w.line(format!( + "auto {promise} = std::make_unique>();" + )); + w.line(format!("auto {future} = {promise}->get_future();")); + c_args.push(format!("[]({}) {{", params.join(", "))); + w.line(format!("{}({}", f.abi.symbol, c_args.join(", "))); w.scope(|w| { - w.line(format!( - "std::unique_ptr<{promise}> p(static_cast<{promise}*>(context));" - )); - // The completion runs on a producer thread inside a C frame, so a - // decode failure (of the result or an error payload) settles the - // promise instead of unwinding. - w.line("try {"); - w.scope(|w| { - w.line("if (err != nullptr && err->code != 0) {"); - w.scope(|w| { - w.line("std::string message = detail::error_message(*err);"); - w.line(format!("p->set_exception({});", make_error_call(f, error))); - }); - w.line("} else {"); - w.scope(|w| match &f.ret { - Some(ret) => { - let slots = &a.callback_params[2..]; - let slot = |i: usize| slots.get(i).map(slot_name).unwrap_or_default(); - w.line(format!( - "p->set_value({});", - received_value(ret, &slot(0), &slot(1)) - )); - } - None => { - w.line("p->set_value();"); - } - }); - w.line("}"); - }); - w.line("} catch (...) {"); - w.scope(|w| { - w.line("p->set_exception(std::current_exception());"); - }); - w.line("}"); - w.line(format!("{prefix}_error_free(err);")); + let settle = format!("detail::settle<{}, {value}>", error_class(&f.error)); + match ( + f.ret.as_ref().and_then(|r| r.value()), + result_recv(&a.result), + ) { + (Some(ty), Some(recv)) => { + w.line(format!( + "{settle}(context, err, [&] {{ return {}; }});", + lift(ty, recv) + )); + } + _ => { + w.line(format!("{settle}(context, err, [] {{}});")); + } + } }); - w.line("}, static_cast(promise.release()));"); - w.line("return future;"); + w.line(format!("}}, {promise}.release());")); + w.line(format!("return {future};")); } // ── Module namespaces ── /// Append one module's nested namespace holding its free functions -/// (`namespace kv::stats { ... }`). An iterator-returning function is -/// preceded by its range class. A module without functions emits nothing; -/// its types live at the namespace root. -pub(crate) fn render_cpp_module_ns( - w: &mut CodeWriter, - module: &ModuleBinding, - error: Option<&ErrorBinding>, - prefix: &str, -) { +/// (`namespace kv::stats { ... }`). A module without functions emits +/// nothing; its types live at the namespace root. +pub(crate) fn render_cpp_module_ns(w: &mut CodeWriter, ctx: &Ctx<'_>, module: &ModuleBinding) { if module.functions.is_empty() { return; } @@ -719,11 +641,7 @@ pub(crate) fn render_cpp_module_ns( w.line(format!("namespace {ns} {{")); w.blank(); for f in &module.functions { - let name = cpp_fn_name(&f.name); - if let CallShape::Iterator(it) = &f.shape { - render_iterator_range(w, f, it, &name, FnKind::Free, error, prefix); - } - render_definition(w, f, &name, FnKind::Free, error, prefix); + render_definition(w, ctx, f, &cpp_fn_name(&f.name), FnKind::Free); } w.line(format!("}} // namespace {ns}")); w.blank(); diff --git a/crates/weaveffi-cli/src/targets/cpp/codec.rs b/crates/weaveffi-cli/src/targets/cpp/codec.rs index 3dab0268..0a2c0ac3 100644 --- a/crates/weaveffi-cli/src/targets/cpp/codec.rs +++ b/crates/weaveffi-cli/src/targets/cpp/codec.rs @@ -1,221 +1,85 @@ -//! Value-buffer codecs: one writer and one reader function per record, rich -//! enum, and distinct composite type (`[Entry]`, `{string:i64}`, `Store?`), -//! plus the read expressions and write statements that call them. +//! Value-buffer codecs of the API's own value types: one `detail::write` +//! overload and one `detail::read` overload per record and rich enum. //! -//! A primitive, C-style enum, or object token is read or written inline (one -//! `BufferReader`/`BufferWriter` call); everything else delegates to its -//! named function in `detail`, so no loop is ever inlined at a call site. -//! Every dispatch goes through [`Ty::wire`], and primitives dispatch on -//! [`Prim::snake`](weaveffi_model::ty::Prim::snake) (`write_i32`, -//! `read_string`). - -use std::collections::HashSet; +//! Primitives, C-style enums, objects, and every optional, list, and map +//! shape are encoded by the generic overloads in the runtime +//! (`runtime/buffer.hpp`), which dispatch on the C++ type, so no composite +//! needs a function of its own: `detail::write(w, v.tags)` and +//! `detail::read>(r)` resolve to the `std::vector` +//! templates, which call back into these overloads for user element types. use crate::codegen::CodeWriter; -use weaveffi_model::model::{EnumBinding, Model, StructBinding}; -use weaveffi_model::ty::{Ty, WireType}; +use weaveffi_model::model::{EnumBinding, StructBinding}; +use crate::targets::cpp::entities::ValueDef; use crate::targets::cpp::types::{cpp_ident, cpp_type}; -/// The identifier fragment naming `ty` in its codec functions: a -/// primitive's spelling (`i64`), a user type's name (`Entry`), or the -/// composite's shape (`opt_i64`, `list_string`, `map_string_Store`). -fn mangle(ty: &Ty) -> String { - match ty { - Ty::Optional(inner) => format!("opt_{}", mangle(inner)), - Ty::List(inner) => format!("list_{}", mangle(inner)), - Ty::Map(k, v) => format!("map_{}_{}", mangle(k), mangle(v)), - Ty::Prim(p) => p.snake().to_string(), - _ => ty - .user_name() - .expect("only optionals, lists, maps, primitives, and user types are mangled") - .to_string(), - } -} - -/// `detail::write_{stem}`: the writer of a record, rich enum, or composite. -pub(crate) fn write_fn(ty: &Ty) -> String { - format!("detail::write_{}", mangle(ty)) -} - -/// `detail::read_{stem}`: the reader of a record, rich enum, or composite. -pub(crate) fn read_fn(ty: &Ty) -> String { - format!("detail::read_{}", mangle(ty)) -} - -/// The expression reading one `ty` value from the reader `r`. An object -/// token is adopted into a new wrapper, whose destructor releases it if a -/// later read fails. -pub(crate) fn read_expr(ty: &Ty, r: &str) -> String { - match ty.wire() { - WireType::Prim(p) => format!("{r}.read_{}()", p.snake()), - WireType::Enum(n) => format!("static_cast<{n}>({r}.read_i32())"), - WireType::Object(n) => format!("{r}.read_object<{n}>()"), - WireType::User(_) | WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("{}({r})", read_fn(ty)) - } - } -} - -/// The statement writing `expr` (one `ty` value) into the writer `w`. An -/// object is written as a fresh reference from its `_clone`. -pub(crate) fn write_stmt(ty: &Ty, expr: &str, w: &str) -> String { - match ty.wire() { - WireType::Prim(p) => format!("{w}.write_{}({expr});", p.snake()), - WireType::Enum(_) => format!("{w}.write_i32(static_cast({expr}));"), - WireType::Object(_) => format!("{w}.write_object({expr});"), - WireType::User(_) | WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("{}({w}, {expr});", write_fn(ty)) - } - } -} - -/// Every value type and distinct composite type the API's value buffers -/// carry, records and rich enums first, then composites in first-use order: -/// inside records, rich-enum variants, and error payloads, and at every -/// buffered call boundary (parameters, returns, iterator elements, and -/// callback parameters and returns). -fn codec_types(model: &Model) -> Vec { - let mut out = Vec::new(); - for m in &model.modules { - out.extend(m.structs.iter().map(|s| Ty::Record(s.name.clone()))); - out.extend( - m.enums - .iter() - .filter(|e| e.is_rich()) - .map(|e| Ty::RichEnum(e.name.clone())), - ); - } - let mut seen = HashSet::new(); - for m in &model.modules { - let fields = m - .structs - .iter() - .flat_map(|s| &s.fields) - .chain( - m.enums - .iter() - .flat_map(|e| &e.variants) - .flat_map(|v| &v.fields), - ) - .chain( - m.errors - .iter() - .flat_map(|e| &e.codes) - .flat_map(|c| &c.fields), - ); - for f in fields { - collect(&f.ty, &mut out, &mut seen); - } - } - let mut boundary = |ty: &Ty| { - let ty = ty.iterator_elem().unwrap_or(ty); - if ty.is_buffered() { - collect(ty, &mut out, &mut seen); - } - }; - for m in &model.modules { - for f in m.callables() { - f.params.iter().for_each(|p| boundary(&p.ty)); - f.ret.iter().for_each(&mut boundary); - } - for cb in &m.callback_interfaces { - for meth in &cb.methods { - meth.params.iter().for_each(|p| boundary(&p.ty)); - meth.ret.iter().for_each(&mut boundary); - } - } - } - out -} - -/// Record every optional, list, and map shape inside `ty`, innermost first. -fn collect(ty: &Ty, out: &mut Vec, seen: &mut HashSet) { - match ty { - Ty::Optional(inner) | Ty::List(inner) => collect(inner, out, seen), - Ty::Map(k, v) => { - collect(k, out, seen); - collect(v, out, seen); - } - _ => return, - } - if seen.insert(ty.clone()) { - out.push(ty.clone()); - } -} - -/// Append, inside `detail`, every codec the API needs: a declaration of each -/// first (so a codec may call any other regardless of nesting), then the -/// definitions. Requires every value type to be complete. -pub(crate) fn render_codecs(w: &mut CodeWriter, model: &Model) { - let types = codec_types(model); - if types.is_empty() { +/// Append, inside `detail`, the codec overloads of every value type: a +/// declaration of each first (so a codec may call any other regardless of +/// nesting), then the definitions. Requires every value type to be +/// complete. +pub(crate) fn render_codecs(w: &mut CodeWriter, defs: &[ValueDef<'_>]) { + if defs.is_empty() { return; } w.line("namespace detail {"); w.blank(); - for ty in &types { - let cpp = cpp_type(ty); - let stem = mangle(ty); + for def in defs { + let name = def.name(); + w.line(format!( + "inline void write(BufferWriter& w, const {name}& v);" + )); w.line(format!( - "inline void write_{stem}(BufferWriter& w, const {cpp}& v);" + "inline {name} read(BufferReader& r, type_tag<{name}>);" )); - w.line(format!("inline {cpp} read_{stem}(BufferReader& r);")); } w.blank(); - for ty in &types { - match ty { - Ty::Record(name) => { - let s = model - .modules - .iter() - .flat_map(|m| &m.structs) - .find(|s| &s.name == name) - .expect("a record type names a declared record"); - render_record_codec(w, s); - } - Ty::RichEnum(name) => render_rich_enum_codec(w, model.enumeration(name)), - _ => render_composite_codec(w, ty), + for def in defs { + match def { + ValueDef::Record(s) => render_record_codec(w, s), + ValueDef::Rich(e) => render_rich_enum_codec(w, e), } } w.line("} // namespace detail"); w.blank(); } -/// Append a record's writer and reader. The reader aggregate-initializes -/// the record from a braced list, whose elements C++ evaluates in order, so -/// the fields are read in wire order. +/// The braced `read(r)` list of `fields`, which C++ evaluates in order, +/// so the fields are read in wire order. +fn read_fields<'a>(fields: impl Iterator) -> Vec { + fields + .map(|ty| format!("read<{}>(r)", cpp_type(ty))) + .collect() +} + +/// Append a record's writer and reader (validation guarantees a record has +/// fields). The reader aggregate-initializes the record from a braced list. fn render_record_codec(w: &mut CodeWriter, s: &StructBinding) { let name = &s.name; - w.line(format!("/** Writes one `{name}` to a value buffer. */")); w.block( - format!("inline void write_{name}(BufferWriter& w, const {name}& v) {{"), + format!("inline void write(BufferWriter& w, const {name}& v) {{"), "}", |w| { - if s.fields.is_empty() { - w.line("(void)w;"); - w.line("(void)v;"); - } for f in &s.fields { - w.line(write_stmt(&f.ty, &format!("v.{}", cpp_ident(&f.name)), "w")); + w.line(format!("write(w, v.{});", cpp_ident(&f.name))); } }, ); w.blank(); - w.line(format!("/** Reads one `{name}` from a value buffer. */")); + let reads = read_fields(s.fields.iter().map(|f| &f.ty)); w.block( - format!("inline {name} read_{name}(BufferReader& r) {{"), + format!("inline {name} read(BufferReader& r, type_tag<{name}>) {{"), "}", |w| { - if s.fields.is_empty() { - w.line("(void)r;"); - w.line(format!("return {name}{{}};")); + if reads.len() <= 2 { + w.line(format!("return {name}{{{}}};", reads.join(", "))); return; } w.line(format!("return {name}{{")); w.scope(|w| { - for f in &s.fields { - w.line(format!("{},", read_expr(&f.ty, "r"))); + for (i, read) in reads.iter().enumerate() { + let comma = if i + 1 < reads.len() { "," } else { "" }; + w.line(format!("{read}{comma}")); } }); w.line("};"); @@ -228,27 +92,22 @@ fn render_record_codec(w: &mut CodeWriter, s: &StructBinding) { /// active variant's fields in wire order. fn render_rich_enum_codec(w: &mut CodeWriter, e: &EnumBinding) { let name = &e.name; - w.line(format!("/** Writes one `{name}` to a value buffer. */")); w.block( - format!("inline void write_{name}(BufferWriter& w, const {name}& v) {{"), + format!("inline void write(BufferWriter& w, const {name}& v) {{"), "}", |w| { - w.block("switch (v.value.index()) {", "}", |w| { - for (i, variant) in e.variants.iter().enumerate() { - w.block(format!("case {i}: {{"), "}", |w| { - w.line(format!("w.write_i32({});", variant.value)); - if !variant.fields.is_empty() { - w.line(format!( - "const {name}::{}& p = std::get<{i}>(v.value);", - cpp_ident(&variant.name) - )); - for f in &variant.fields { - w.line(write_stmt( - &f.ty, - &format!("p.{}", cpp_ident(&f.name)), - "w", - )); - } + w.line("write(w, static_cast(v.tag()));"); + w.block("switch (v.tag()) {", "}", |w| { + for variant in &e.variants { + let vn = cpp_ident(&variant.name); + if variant.fields.is_empty() { + w.line(format!("case {name}::Tag::{vn}: break;")); + continue; + } + w.block(format!("case {name}::Tag::{vn}: {{"), "}", |w| { + w.line(format!("const auto& p = std::get<{name}::{vn}>(v.value);")); + for f in &variant.fields { + w.line(format!("write(w, p.{});", cpp_ident(&f.name))); } w.line("break;"); }); @@ -257,32 +116,19 @@ fn render_rich_enum_codec(w: &mut CodeWriter, e: &EnumBinding) { }, ); w.blank(); - w.line(format!("/** Reads one `{name}` from a value buffer. */")); w.block( - format!("inline {name} read_{name}(BufferReader& r) {{"), + format!("inline {name} read(BufferReader& r, type_tag<{name}>) {{"), "}", |w| { - w.line("int32_t tag = r.read_i32();"); - w.block("switch (tag) {", "}", |w| { + w.block("switch (read(r)) {", "}", |w| { for variant in &e.variants { let vn = cpp_ident(&variant.name); - if variant.fields.is_empty() { - w.line(format!( - "case {}: return {name}{{{name}::{vn}{{}}}};", - variant.value - )); - continue; - } - w.line(format!("case {}:", variant.value)); - w.scope(|w| { - w.line(format!("return {name}{{{name}::{vn}{{")); - w.scope(|w| { - for f in &variant.fields { - w.line(format!("{},", read_expr(&f.ty, "r"))); - } - }); - w.line("}};"); - }); + let reads = read_fields(variant.fields.iter().map(|f| &f.ty)); + w.line(format!( + "case {}: return {name}{{{name}::{vn}{{{}}}}};", + variant.value, + reads.join(", ") + )); } w.line("default: break;"); }); @@ -291,76 +137,3 @@ fn render_rich_enum_codec(w: &mut CodeWriter, e: &EnumBinding) { ); w.blank(); } - -/// Append one composite type's writer and reader. A map rejects a repeated -/// key, which would otherwise drop an entry silently. -fn render_composite_codec(w: &mut CodeWriter, ty: &Ty) { - let cpp = cpp_type(ty); - let stem = mangle(ty); - w.line(format!("/** Writes one `{ty}` to a value buffer. */")); - w.block( - format!("inline void write_{stem}(BufferWriter& w, const {cpp}& v) {{"), - "}", - |w| match ty { - Ty::Optional(inner) => { - w.line("w.write_option_flag(v.has_value());"); - w.line(format!( - "if (v.has_value()) {}", - write_stmt(inner, "*v", "w") - )); - } - Ty::List(inner) => { - w.line("w.write_len(v.size());"); - w.line(format!( - "for (const auto& item : v) {}", - write_stmt(inner, "item", "w") - )); - } - Ty::Map(k, val) => { - w.line("w.write_len(v.size());"); - w.block("for (const auto& entry : v) {", "}", |w| { - w.line(write_stmt(k, "entry.first", "w")); - w.line(write_stmt(val, "entry.second", "w")); - }); - } - _ => unreachable!("only optionals, lists, and maps are composites"), - }, - ); - w.blank(); - w.line(format!("/** Reads one `{ty}` from a value buffer. */")); - w.block( - format!("inline {cpp} read_{stem}(BufferReader& r) {{"), - "}", - |w| match ty { - Ty::Optional(inner) => { - w.line("if (!r.read_option_flag()) return std::nullopt;"); - w.line(format!("return {cpp}({});", read_expr(inner, "r"))); - } - Ty::List(inner) => { - w.line("size_t n = r.read_count();"); - w.line(format!("{cpp} v;")); - w.line("v.reserve(r.reserve_hint(n));"); - w.line(format!( - "for (size_t i = 0; i < n; ++i) v.push_back({});", - read_expr(inner, "r") - )); - w.line("return v;"); - } - Ty::Map(k, val) => { - w.line("size_t n = r.read_count();"); - w.line(format!("{cpp} v;")); - w.line("v.reserve(r.reserve_hint(n));"); - w.block("for (size_t i = 0; i < n; ++i) {", "}", |w| { - w.line(format!("{} key = {};", cpp_type(k), read_expr(k, "r"))); - w.line(format!("{} value = {};", cpp_type(val), read_expr(val, "r"))); - w.line( - "if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail(\"repeated map key\");", - ); - }); - w.line("return v;"); - } - _ => unreachable!("only optionals, lists, and maps are composites"), - }, - ); - w.blank(); -} diff --git a/crates/weaveffi-cli/src/targets/cpp/entities.rs b/crates/weaveffi-cli/src/targets/cpp/entities.rs index 50452e4b..9dcab350 100644 --- a/crates/weaveffi-cli/src/targets/cpp/entities.rs +++ b/crates/weaveffi-cli/src/targets/cpp/entities.rs @@ -1,56 +1,58 @@ //! Declared-entity rendering: plain enums, value types (records and rich -//! enums), typed error domains, and interface RAII classes, plus the -//! dependency ordering that keeps by-value members complete before use. +//! enums), error domains, and interface wrapper classes, plus the dependency +//! ordering that keeps by-value members complete before use. use std::collections::HashMap; use crate::codegen::common::DocCommentStyle; +use crate::codegen::docs::Doc; +use crate::codegen::errors::ErrorTable; use crate::codegen::CodeWriter; +use crate::lang; use weaveffi_model::model::{ - CallShape, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, + CallShape, EnumBinding, FieldBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use weaveffi_model::ty::{Family, Ty}; +use weaveffi_model::ty::Ty; -use crate::targets::cpp::calls::{ - iterator_class_name, render_definition, render_iterator_range, render_member_decl, FnKind, -}; -use crate::targets::cpp::codec::{read_expr, write_stmt}; -use crate::targets::cpp::types::{cpp_error_class, cpp_ident, cpp_member_name, cpp_type}; +use crate::targets::cpp::calls::{render_definition, render_member_decl, FnKind}; +use crate::targets::cpp::types::{cpp_ident, cpp_member_name, cpp_type, defaultable, Ctx}; -/// A doc comment with a trailing `@deprecated` line when the declaration is +/// A type's doc comment, with a trailing `@deprecated` paragraph when it's /// deprecated. Types carry the deprecation in their docs rather than as an -/// attribute, so the generated code that marshals them compiles warning-free. -pub(crate) fn doc_text(doc: Option<&str>, deprecated: Option<&str>) -> Option { - match (doc, deprecated) { - (Some(d), Some(msg)) => Some(format!("{d}\n\n@deprecated {msg}")), - (None, Some(msg)) => Some(format!("@deprecated {msg}")), - (d, None) => d.map(str::to_string), +/// attribute, so the generated code that marshals them compiles +/// warning-free. +pub(crate) fn type_doc( + ctx: &Ctx<'_>, + doc: &Option, + deprecated: &Option, +) -> Option { + let doc = Doc::new(doc, deprecated); + let spell = |s: &str| ctx.spell(s); + let note = doc.deprecation(spell).map(|m| format!("@deprecated {m}")); + match (doc.text(spell), note) { + (Some(d), Some(n)) => Some(format!("{d}\n\n{n}")), + (d, n) => d.or(n), } } -/// `expr` handed on to a constructor: moved, unless it's a direct value -/// (a scalar or enum), which copies. -fn pass_on(ty: &Ty, expr: &str) -> String { - if ty.family() == Family::Direct { - expr.to_string() - } else { - format!("std::move({expr})") - } +/// A field's or variant's doc comment, rewritten. +fn field_doc(ctx: &Ctx<'_>, doc: &Option) -> Option { + Doc::new(doc, &None).text(|s| ctx.spell(s)) } // ── Enums ── /// Append one module's C-style enums as `enum class {Name} : int32_t`. Rich /// enums are value types, rendered with the records. -pub(crate) fn render_cpp_enums(w: &mut CodeWriter, module: &ModuleBinding) { +pub(crate) fn render_cpp_enums(w: &mut CodeWriter, ctx: &Ctx<'_>, module: &ModuleBinding) { for e in module.enums.iter().filter(|e| !e.is_rich()) { w.doc( - &doc_text(e.doc.as_deref(), e.deprecated.as_deref()), + &type_doc(ctx, &e.doc, &e.deprecated), DocCommentStyle::Javadoc, ); w.block(format!("enum class {} : int32_t {{", e.name), "};", |w| { for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::Javadoc); + w.doc(&field_doc(ctx, &v.doc), DocCommentStyle::Javadoc); w.line(format!("{} = {},", cpp_ident(&v.name), v.value)); } }); @@ -98,10 +100,10 @@ impl ValueDef<'_> { } /// Append the type's definition. - pub(crate) fn render(&self, w: &mut CodeWriter) { + pub(crate) fn render(&self, w: &mut CodeWriter, ctx: &Ctx<'_>) { match self { - ValueDef::Record(s) => render_cpp_record(w, s), - ValueDef::Rich(e) => render_cpp_rich_enum(w, e), + ValueDef::Record(s) => render_cpp_record(w, ctx, s), + ValueDef::Rich(e) => render_cpp_rich_enum(w, ctx, e), } } } @@ -173,32 +175,99 @@ pub(crate) fn value_types_in_order(modules: &[ModuleBinding]) -> Vec, f: &FieldBinding) -> String { + let init = if defaultable(ctx.model, &f.ty) { + "{}" + } else { + "" + }; + format!("{} {}{init};", cpp_type(&f.ty), cpp_ident(&f.name)) +} + +/// Whether `ty` is a scalar, `bool`, or C-style enum: trivially copied, and +/// zero when value-initialized. +fn is_scalar(ty: &Ty) -> bool { + match ty { + Ty::Prim(p) => p.is_scalar(), + Ty::Enum(_) => true, + _ => false, + } +} + +/// Append the hidden-friend `operator==` and `operator!=` of a value +/// struct: memberwise, in declaration order. Floating-point members compare +/// as IEEE values, and interface members by identity. +fn render_equality(w: &mut CodeWriter, name: &str, fields: &[FieldBinding]) { + w.line("/** Memberwise equality; interface members compare by identity. */"); + if fields.is_empty() { + w.line(format!( + "friend bool operator==(const {name}&, const {name}&) noexcept {{ return true; }}" + )); + } else { + let cmp: Vec = fields + .iter() + .map(|f| { + let n = cpp_ident(&f.name); + format!("a.{n} == b.{n}") + }) + .collect(); + let head = format!("friend bool operator==(const {name}& a, const {name}& b) {{"); + if cmp.len() <= 2 { + w.line(format!("{head} return {}; }}", cmp.join(" && "))); + } else { + w.block(head, "}", |w| { + w.line(format!("return {}", cmp[0])); + w.scope(|w| { + for (i, c) in cmp.iter().enumerate().skip(1) { + let end = if i + 1 == cmp.len() { ";" } else { "" }; + w.line(format!("&& {c}{end}")); + } + }); + }); + } + } + w.line(format!( + "friend bool operator!=(const {name}& a, const {name}& b) {{ return !(a == b); }}" + )); +} + +/// Render a record as a plain C++ aggregate: typed members in declaration +/// (and wire) order with default member initializers, and memberwise +/// equality. An interface-typed member is the RAII wrapper held by value, +/// so copying the record shares the object and destroying it releases one +/// reference. +fn render_cpp_record(w: &mut CodeWriter, ctx: &Ctx<'_>, s: &StructBinding) { w.doc( - &doc_text(s.doc.as_deref(), s.deprecated.as_deref()), + &type_doc(ctx, &s.doc, &s.deprecated), DocCommentStyle::Javadoc, ); w.block(format!("struct {} {{", s.name), "};", |w| { for f in &s.fields { - w.doc(&f.doc, DocCommentStyle::Javadoc); - w.line(format!("{} {};", cpp_type(&f.ty), cpp_ident(&f.name))); + w.doc(&field_doc(ctx, &f.doc), DocCommentStyle::Javadoc); + w.line(member_decl(ctx, f)); + } + if !s.fields.is_empty() { + w.blank(); } + render_equality(w, &s.name, &s.fields); }); w.blank(); } -/// Render a rich enum as a `std::variant`-backed sum type: one payload struct -/// per variant, a `value` member holding the active payload, a nested `Tag` -/// enum mirroring the wire discriminants, and a `tag()` reader. Construct one -/// as `Shape{Shape::Circle{2.0}}`. -fn render_cpp_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { +/// Render a rich enum as a `std::variant`-backed sum type: one payload +/// struct per variant, a `value` member holding the active payload, a nested +/// `Tag` enum mirroring the wire discriminants, a `tag()` reader, and +/// equality. Construct one as `Shape{Shape::Circle{2.0}}`. +fn render_cpp_rich_enum(w: &mut CodeWriter, ctx: &Ctx<'_>, e: &EnumBinding) { let name = &e.name; w.doc( - &doc_text(e.doc.as_deref(), e.deprecated.as_deref()), + &type_doc(ctx, &e.doc, &e.deprecated), DocCommentStyle::Javadoc, ); w.block(format!("struct {name} {{"), "};", |w| { @@ -212,12 +281,17 @@ fn render_cpp_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { }); w.blank(); for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::Javadoc); - w.block(format!("struct {} {{", cpp_ident(&v.name)), "};", |w| { + let vn = cpp_ident(&v.name); + w.doc(&field_doc(ctx, &v.doc), DocCommentStyle::Javadoc); + w.block(format!("struct {vn} {{"), "};", |w| { for f in &v.fields { - w.doc(&f.doc, DocCommentStyle::Javadoc); - w.line(format!("{} {};", cpp_type(&f.ty), cpp_ident(&f.name))); + w.doc(&field_doc(ctx, &f.doc), DocCommentStyle::Javadoc); + w.line(member_decl(ctx, f)); } + if !v.fields.is_empty() { + w.blank(); + } + render_equality(w, &vn, &v.fields); }); w.blank(); } @@ -234,55 +308,49 @@ fn render_cpp_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { )); w.line("return tags[value.index()];"); }); + w.blank(); + w.line("/** Equal when the same variant is active with equal payloads. */"); + w.line(format!( + "friend bool operator==(const {name}& a, const {name}& b) {{ return a.value == b.value; }}" + )); + w.line(format!( + "friend bool operator!=(const {name}& a, const {name}& b) {{ return !(a == b); }}" + )); }); w.blank(); } -// ── Typed error domains ── - -/// `detail::make_{path}_error`: builds the exception for a code, message, -/// and payload of the domain. -pub(crate) fn make_error_fn(eb: &ErrorBinding) -> String { - format!("detail::make_{}_error", eb.owner_path) -} - -/// `detail::check_{path}`: throws the domain's exception for a failed call. -pub(crate) fn check_fn(eb: &ErrorBinding) -> String { - format!("detail::check_{}", eb.owner_path) -} +// ── Error domains ── -/// `detail::report_{path}_error`: reports a domain exception a callback -/// implementation threw through the vtable entry's `out_err`. -pub(crate) fn report_fn(eb: &ErrorBinding) -> String { - format!("detail::report_{}_error", eb.owner_path) +/// The member name of an error code's field: [`cpp_ident`], with a trailing +/// underscore when it would hide a member every exception has (`code()`, +/// `what()`). +pub(crate) fn error_field(name: &str) -> String { + lang::escape_member(&cpp_ident(name), &["code", "what"]) } -/// Append one module's typed error domain: a domain exception derived from -/// `Error`, one subclass per declared code (with typed members for the -/// code's payload fields), and the `detail` helpers that map a failed call's -/// `out_err` to the typed exception. With `reports`, also the helper that -/// reports a thrown domain exception from a callback trampoline. +/// Append one error domain: its class derived from `Error`, one subclass +/// per declared code (with typed members for the code's payload fields), +/// and its `detail::Errors` policy. /// -/// Domain codes are positive and the runtime owns every negative code, so -/// the mapping sends -5 to `Cancelled` and any other negative code to the -/// root `Error` before consulting the domain's codes; an undeclared positive -/// code falls back to the domain class itself. -pub(crate) fn render_domain_error( - w: &mut CodeWriter, - module: &ModuleBinding, - eb: &ErrorBinding, - prefix: &str, - reports: bool, -) { - let domain = &eb.type_name; +/// The policy's `raise` throws `Cancelled` for -5 and the root `Error` for +/// any other negative (runtime) code, the code's class for a declared code, +/// and the domain class itself for a positive code these bindings don't +/// know (domains are open: a newer producer may add codes). Its `report` +/// turns a domain exception a callback implementation threw into the +/// producer's error slot, payload included; any other exception is a plain +/// failure (-1). +pub(crate) fn render_domain_error(w: &mut CodeWriter, ctx: &Ctx<'_>, table: &ErrorTable<'_>) { + let domain = &table.type_name; + let prefix = ctx.prefix; w.line(format!( - "/** The errors the `{}` module's throwing calls report; catch a code's subclass or this. */", - module.dot_path + "/** The `{}` error domain of module `{}`: catch a code's class or this. */", + table.domain.name, table.module.dot_path )); w.line(format!("class {domain} : public Error {{")); w.line("public:"); w.scope(|w| { - w.line("/** Builds an error carrying a domain `code` and `message`. */"); + w.line("/** Builds an error carrying one of the domain's `code`s and its `message`. */"); w.line(format!( "{domain}(int32_t code, const std::string& message) : Error(code, message) {{}}" )); @@ -290,9 +358,12 @@ pub(crate) fn render_domain_error( w.line("};"); w.blank(); - for code in &eb.codes { - let class = cpp_error_class(&code.name); - let doc = code.doc.clone().unwrap_or_else(|| code.message.clone()); + for row in &table.codes { + let code = row.code; + let class = &row.type_name; + let doc = Doc::new(&code.doc, &None) + .text(|s| ctx.spell(s)) + .unwrap_or_else(|| code.message.clone()); w.doc( &Some(format!("{doc}\n\nCode {}.", code.value)), DocCommentStyle::Javadoc, @@ -301,18 +372,27 @@ pub(crate) fn render_domain_error( w.line("public:"); w.scope(|w| { for f in &code.fields { - w.doc(&f.doc, DocCommentStyle::Javadoc); - w.line(format!("{} {};", cpp_type(&f.ty), cpp_ident(&f.name))); + w.doc(&field_doc(ctx, &f.doc), DocCommentStyle::Javadoc); + w.line(format!("{} {};", cpp_type(&f.ty), error_field(&f.name))); } if !code.fields.is_empty() { w.blank(); } - let mut params = vec!["const std::string& message".to_string()]; - let mut inits = vec![format!("{domain}({}, message)", code.value)]; + // The message parameter steps aside for a field named `message`. + let mut message = "message".to_string(); + while code.fields.iter().any(|f| error_field(&f.name) == message) { + message.push('_'); + } + let mut params = vec![format!("const std::string& {message}")]; + let mut inits = vec![format!("{domain}({}, {message})", code.value)]; for f in &code.fields { - let name = cpp_ident(&f.name); + let name = error_field(&f.name); params.push(format!("{} {name}", cpp_type(&f.ty))); - inits.push(format!("{name}({})", pass_on(&f.ty, &name))); + if is_scalar(&f.ty) { + inits.push(format!("{name}({name})")); + } else { + inits.push(format!("{name}(std::move({name}))")); + } } w.line("/** Builds the error with the producer's `message` and the code's fields. */"); let explicit = if code.fields.is_empty() { @@ -330,220 +410,81 @@ pub(crate) fn render_domain_error( w.blank(); } - let path = &eb.owner_path; w.line("namespace detail {"); w.blank(); w.line(format!( - "/** The exception for a failed `{domain}` call: its code's subclass, Cancelled, or the root Error. */" + "/** Raises and reports `{domain}` and its codes' classes. */" )); - w.block( - format!( - "inline std::exception_ptr make_{path}_error(int32_t code, const std::string& message, const uint8_t* payload_ptr, size_t payload_len) {{" - ), - "}", - |w| { - w.line("if (code == -5) return std::make_exception_ptr(Cancelled(message));"); - w.line("if (code < 0) return std::make_exception_ptr(Error(code, message));"); - if eb.codes.iter().all(|c| c.fields.is_empty()) { - w.line("(void)payload_ptr;"); - w.line("(void)payload_len;"); - } - w.block("switch (code) {", "}", |w| { - for code in &eb.codes { - let class = cpp_error_class(&code.name); - if code.fields.is_empty() { - w.line(format!( - "case {}: return std::make_exception_ptr({class}(message));", - code.value - )); - continue; - } - w.block(format!("case {}: {{", code.value), "}", |w| { - // Locals, because constructor arguments evaluate in - // an unspecified order and the fields are read in - // wire order. - w.line("BufferReader r(payload_ptr, payload_len);"); - let mut args = vec!["message".to_string()]; - for f in &code.fields { - let var = format!("f_{}", f.name); - w.line(format!("{} {var} = {};", cpp_type(&f.ty), read_expr(&f.ty, "r"))); - args.push(pass_on(&f.ty, &var)); + w.line("template <>"); + w.block(format!("struct Errors<{domain}> {{"), "};", |w| { + w.block( + format!("[[noreturn]] static void raise(const {prefix}_error& err) {{"), + "}", + |w| { + w.line("if (err.code < 0) raise_runtime(err);"); + w.block("switch (err.code) {", "}", |w| { + for row in &table.codes { + let code = row.code; + let class = &row.type_name; + if code.fields.is_empty() { + w.line(format!("case {}: throw {class}(message(err));", code.value)); + } else { + let fields: Vec = + code.fields.iter().map(|f| cpp_type(&f.ty)).collect(); + w.line(format!( + "case {}: raise_with_fields<{class}, {}>(err);", + code.value, + fields.join(", ") + )); } - w.line("r.expect_end();"); + } + w.line(format!("default: throw {domain}(err.code, message(err));")); + }); + }, + ); + w.blank(); + w.block( + format!("static void report({prefix}_error* out_err) noexcept {{"), + "}", + |w| { + w.line("try {"); + w.scope(|w| { + w.line("throw;"); + }); + for row in table.codes.iter().filter(|r| !r.code.fields.is_empty()) { + let fields: Vec = row + .code + .fields + .iter() + .map(|f| format!("e.{}", error_field(&f.name))) + .collect(); + w.line(format!("}} catch (const {}& e) {{", row.type_name)); + w.scope(|w| { w.line(format!( - "return std::make_exception_ptr({class}({}));", - args.join(", ") + "report_with_fields(out_err, e, {});", + fields.join(", ") )); }); } - w.line(format!( - "default: return std::make_exception_ptr({domain}(code, message));" - )); - }); - }, - ); - w.blank(); - w.line(format!( - "/** Throws the typed `{domain}` exception if `err` carries a nonzero code. */" - )); - w.block( - format!("inline void check_{path}({prefix}_error& err) {{"), - "}", - |w| { - w.line("if (err.code == 0) return;"); - // The error owns the payload, so the exception (which decodes - // it) is built before error_clear releases it. - w.line(format!( - "std::exception_ptr ex = make_{path}_error(err.code, error_message(err), err.payload_ptr, err.payload_len);" - )); - w.line(format!("{prefix}_error_clear(&err);")); - w.line("std::rethrow_exception(ex);"); - }, - ); + w.line(format!("}} catch (const {domain}& e) {{")); + w.scope(|w| { + w.line("set_error(out_err, e.code(), e.what());"); + }); + w.line("} catch (...) {"); + w.scope(|w| { + w.line("report_current(out_err, -1);"); + }); + w.line("}"); + }, + ); + }); w.blank(); - if reports { - render_report_fn(w, eb, prefix); - } w.line("} // namespace detail"); w.blank(); } -/// Append the helper a throwing callback method's trampoline uses to report a -/// `{domain}` exception: a declared code goes back with its fields encoded as -/// the payload; an exception whose code the domain doesn't declare (or a -/// code with fields thrown as the bare domain class, which has no fields to -/// send) is a callback failure, -4. -fn render_report_fn(w: &mut CodeWriter, eb: &ErrorBinding, prefix: &str) { - let domain = &eb.type_name; - w.line(format!( - "/** Reports a `{domain}` a callback implementation threw through the vtable entry's `out_err`. */" - )); - w.block( - format!( - "inline void report_{}_error(const {domain}& e, {prefix}_error* out_err) {{", - eb.owner_path - ), - "}", - |w| { - w.block("switch (e.code()) {", "}", |w| { - for code in &eb.codes { - let class = cpp_error_class(&code.name); - if code.fields.is_empty() { - w.line(format!( - "case {}: {prefix}_error_set(out_err, {}, e.what()); return;", - code.value, code.value - )); - continue; - } - w.block(format!("case {}: {{", code.value), "}", |w| { - w.line(format!( - "const auto* typed = dynamic_cast(&e);" - )); - w.line("if (typed == nullptr) break;"); - w.line("BufferWriter payload;"); - for f in &code.fields { - w.line(write_stmt( - &f.ty, - &format!("typed->{}", cpp_ident(&f.name)), - "payload", - )); - } - w.line(format!( - "{prefix}_error_set(out_err, {}, e.what());", - code.value - )); - w.line(format!( - "{prefix}_error_set_payload(out_err, payload.data(), payload.size());" - )); - w.line("return;"); - }); - } - w.line("default: break;"); - }); - w.line(format!("{prefix}_error_set(out_err, -4, e.what());")); - }, - ); - w.blank(); -} - // ── Interfaces ── -/// Append the reference-counting RAII skeleton of an interface class: the -/// `raw_type` alias of the C tag, the tagged constructor adopting one strong -/// reference, a destructor releasing it, copies that take a new reference -/// through `_clone`, moves that transfer the pointer, and the -/// `handle()`/`clone_handle()` readers. -fn render_raii_skeleton(w: &mut CodeWriter, i: &InterfaceBinding) { - let name = &i.name; - let tag = &i.c_tag; - let clone = &i.clone_symbol; - let destroy = &i.destroy_symbol; - - w.line("/** The C type this class wraps. */"); - w.line(format!("using raw_type = {tag};")); - w.blank(); - w.line(format!( - "/** Adopts one strong reference to a producer object: `{name}(adopt, raw)`. */" - )); - w.line(format!( - "explicit {name}(adopt_t, {tag}* h) noexcept : raw_(h) {{}}" - )); - w.blank(); - w.line("/** Releases this wrapper's reference; the object is dropped with its last one. */"); - w.block(format!("~{name}() {{"), "}", |w| { - w.line(format!("if (raw_ != nullptr) {destroy}(raw_);")); - }); - w.blank(); - w.line("/** Copies share the object: the copy takes a new strong reference. */"); - w.line(format!( - "{name}(const {name}& other) : raw_({clone}(other.raw_)) {{}}" - )); - w.blank(); - w.line("/** Takes a new reference to `other`'s object and releases the current one. */"); - w.block( - format!("{name}& operator=(const {name}& other) {{"), - "}", - |w| { - w.block("if (this != &other) {", "}", |w| { - w.line(format!("{tag}* h = {clone}(other.raw_);")); - w.line(format!("if (raw_ != nullptr) {destroy}(raw_);")); - w.line("raw_ = h;"); - }); - w.line("return *this;"); - }, - ); - w.blank(); - w.line("/** Transfers `other`'s reference; `other` becomes empty. */"); - w.line(format!( - "{name}({name}&& other) noexcept : raw_(other.raw_) {{ other.raw_ = nullptr; }}" - )); - w.blank(); - w.line("/** Releases the current reference and takes over `other`'s. */"); - w.block( - format!("{name}& operator=({name}&& other) noexcept {{"), - "}", - |w| { - w.block("if (this != &other) {", "}", |w| { - w.line(format!("if (raw_ != nullptr) {destroy}(raw_);")); - w.line("raw_ = other.raw_;"); - w.line("other.raw_ = nullptr;"); - }); - w.line("return *this;"); - }, - ); - w.blank(); - w.line("/** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */"); - w.line(format!( - "const {tag}* handle() const noexcept {{ return raw_; }}" - )); - w.blank(); - w.line("/** A new strong reference to the object, which the caller owns. */"); - w.line(format!( - "{tag}* clone_handle() const {{ return {clone}(raw_); }}" - )); - w.blank(); -} - /// The C++ name and declaration kind of each interface member. The /// synchronous constructor named `new` becomes the C++ constructor; every /// other constructor becomes a static factory named after it. @@ -551,7 +492,7 @@ fn member_kinds(i: &InterfaceBinding) -> Vec<(&FnBinding, String, FnKind<'_>)> { let class = i.name.as_str(); let mut members = Vec::new(); for c in &i.constructors { - if c.name == "new" && matches!(c.shape, CallShape::Sync(_)) { + if c.name == "new" && matches!(c.shape, CallShape::Sync) && c.iterator().is_none() { members.push((c, class.to_string(), FnKind::Ctor { class })); } else { members.push((c, cpp_member_name(&c.name), FnKind::Static { class })); @@ -566,68 +507,78 @@ fn member_kinds(i: &InterfaceBinding) -> Vec<(&FnBinding, String, FnKind<'_>)> { members } -/// Append the forward declarations an interface needs before any class body: -/// the wrapper class and the range class of every iterator-returning member. -pub(crate) fn render_cpp_interface_forward_decls(w: &mut CodeWriter, i: &InterfaceBinding) { - w.line(format!("class {};", i.name)); - for (f, _, kind) in member_kinds(i) { - if matches!(f.shape, CallShape::Iterator(_)) { - w.line(format!("class {};", iterator_class_name(f, kind))); - } - } -} - -/// Append an interface's class definition: the RAII skeleton plus the -/// *declarations* of its members. The member bodies follow every value type -/// and codec ([`render_cpp_interface_members`]); the class itself comes -/// first so records can hold it by value. -pub(crate) fn render_cpp_interface_class( - w: &mut CodeWriter, - i: &InterfaceBinding, - error: Option<&ErrorBinding>, -) { +/// Append an interface's class: the reference-counting core (a +/// `detail::Handle` with the class's `traits`, so the class itself follows +/// the Rule of Zero), the `adopt` constructor, the `handle()` and +/// `clone_handle()` readers, identity equality, and the *declarations* of +/// its members. The member bodies follow every value type and codec +/// ([`render_cpp_interface_members`]); the class comes first so records can +/// hold it by value. +pub(crate) fn render_cpp_interface_class(w: &mut CodeWriter, ctx: &Ctx<'_>, i: &InterfaceBinding) { + let name = &i.name; + let tag = &i.c_tag; w.doc( - &doc_text(i.doc.as_deref(), i.deprecated.as_deref()), + &type_doc(ctx, &i.doc, &i.deprecated), DocCommentStyle::Javadoc, ); - w.line(format!("class {} {{", i.name)); - w.scope(|w| { - w.line(format!("{}* raw_;", i.c_tag)); - w.blank(); - }); + w.line(format!("class {name} {{")); w.line("public:"); w.scope(|w| { - render_raii_skeleton(w, i); + w.line("/** The C type this class wraps. */"); + w.line(format!("using raw_type = {tag};")); + w.blank(); + w.line(format!( + "/** Adopts one strong reference to a producer object: `{name}(adopt, raw)`. */" + )); + w.line(format!( + "explicit {name}(adopt_t, raw_type* raw) noexcept : raw_(raw) {{}}" + )); + w.blank(); for (f, cpp_name, kind) in member_kinds(i) { - render_member_decl(w, f, &cpp_name, kind, error); + render_member_decl(w, ctx, f, &cpp_name, kind); } + w.line("/** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */"); + w.line("const raw_type* handle() const noexcept { return raw_.get(); }"); + w.blank(); + w.line("/** A new strong reference to the object, which the caller owns. */"); + w.line("raw_type* clone_handle() const noexcept { return raw_.clone(); }"); + w.blank(); + w.line("/** Whether both wrap the same producer object. */"); + w.line(format!( + "friend bool operator==(const {name}& a, const {name}& b) noexcept {{ return a.raw_.get() == b.raw_.get(); }}" + )); + w.line(format!( + "friend bool operator!=(const {name}& a, const {name}& b) noexcept {{ return !(a == b); }}" + )); + }); + w.blank(); + w.line("private:"); + w.scope(|w| { + w.block("struct traits {", "};", |w| { + w.line(format!("using raw_type = {tag};")); + w.line(format!( + "static raw_type* clone(const raw_type* raw) noexcept {{ return {}(raw); }}", + i.clone_symbol + )); + w.line(format!( + "static void destroy(raw_type* raw) noexcept {{ {}(raw); }}", + i.destroy_symbol + )); + }); + w.blank(); + w.line("detail::Handle raw_;"); }); w.line("};"); w.blank(); } -/// Append the range classes of an interface's iterator-returning members. -pub(crate) fn render_cpp_interface_iterators( - w: &mut CodeWriter, - i: &InterfaceBinding, - error: Option<&ErrorBinding>, - prefix: &str, -) { - for (f, cpp_name, kind) in member_kinds(i) { - if let CallShape::Iterator(it) = &f.shape { - render_iterator_range(w, f, it, &cpp_name, kind, error, prefix); - } - } -} - /// Append the out-of-line `inline` definitions of an interface's members. pub(crate) fn render_cpp_interface_members( w: &mut CodeWriter, + ctx: &Ctx<'_>, i: &InterfaceBinding, - error: Option<&ErrorBinding>, - prefix: &str, ) { for (f, cpp_name, kind) in member_kinds(i) { - render_definition(w, f, &cpp_name, kind, error, prefix); + render_definition(w, ctx, f, &cpp_name, kind); } } diff --git a/crates/weaveffi-cli/src/targets/cpp/mod.rs b/crates/weaveffi-cli/src/targets/cpp/mod.rs index e92990be..b11a7625 100644 --- a/crates/weaveffi-cli/src/targets/cpp/mod.rs +++ b/crates/weaveffi-cli/src/targets/cpp/mod.rs @@ -4,40 +4,39 @@ //! header `{library}.h` (the same file the [`c`](super::c) target emits), a //! wrapper header `{library}.hpp` that includes it, a `CMakeLists.txt` //! exporting an INTERFACE target, and a README. Everything lives in -//! `namespace {prefix}` by default. Implements [`LanguageBackend`]; the -//! shared driver bridges it into the generator pipeline. +//! `namespace {prefix}` by default. //! -//! The generated surface follows C ABI revision 4: +//! The generated surface follows C ABI revision 5, reading every transport +//! decision from the model's passing contracts: //! //! * Strings cross as a UTF-8 pointer and length: parameters are //! `std::string_view`, returns are `std::string` copied out of the //! producer's run and released with `{prefix}_free_bytes`. -//! * Records are plain value structs; rich enums are `std::variant`-backed -//! sum types with one payload struct per variant. Both cross as value -//! buffers through one generated writer and reader per record, rich enum, -//! and distinct composite type (`[Entry]`, `{string:i64}`, `Store?`) in -//! `detail`. -//! * Interfaces are RAII classes holding one strong reference: the destructor -//! calls `_destroy`, copying calls `_clone`, and moving transfers the -//! pointer. A parameter is borrowed; a returned object, an async result, -//! an iterator element, and an object argument handed to a callback are -//! adopted. Inside a value buffer an object is a token minted with -//! `_clone`. -//! * Callback interfaces are abstract classes passed as `std::shared_ptr` -//! (empty for an optional `Cb?`). The wrapper boxes the pointer as `ctx` -//! and hands the producer a static vtable with the `{size, flags, free}` -//! header. Methods return any family (strings, bytes, and buffers through -//! a `{prefix}_alloc` run); a `throws` method reports the module's domain -//! exception with its fields as the payload, and any other exception as -//! -4. -//! * Free functions live in a nested namespace per IDL module -//! (`kvstore::kv::stats::summarize`). -//! * An `iter` callable returns a move-only lazy range; async callables -//! return `std::future`, and a cancellable one takes a trailing -//! `const CancelToken&`. -//! * A throwing call throws its module's domain exception (`KvError` and a -//! subclass per code), the root `Error` for a runtime code, or `Cancelled`. -//! A call that declares no errors throws `InternalError` (the trap policy). +//! * Optional scalars and enums (OptDirect) are `std::optional` passed +//! as a presence flag and a value; numeric lists (Slice) are +//! `std::vector` whose storage is passed as is, and returned typed +//! arrays are copied into a vector. +//! * Records are aggregates with default member initializers and memberwise +//! `operator==`; rich enums are `std::variant`-backed sum types. Both +//! cross as value buffers through one `detail::write` and `detail::read` +//! overload each; optionals, vectors, and maps are encoded by generic +//! templates in the runtime, never by a function per composite. +//! * Interfaces are classes holding a Rule-of-Zero `detail::Handle` (one +//! strong reference: copies `_clone`, destruction `_destroy`), compared +//! by identity. +//! * Callback interfaces are abstract classes passed as `std::shared_ptr`. +//! A `detail::Callbacks` specialization holds the trampolines and the +//! static vtable (flags 0); each method reports exceptions per its own +//! error strategy. +//! * Every exception derives from `{namespace}::Error`: an error domain's +//! class and its codes' classes (an unknown positive code is the domain +//! class: domains are open), `Error` itself for `throws: any` and runtime +//! codes, `InternalError` for a call that declares no errors (the trap +//! policy), `Cancelled`, and `LoadError`. One `detail::Errors` +//! policy per exception class raises and reports them. +//! * An `iter` callable returns the generic lazy `Range`; async +//! callables return `std::future`, and a cancellable one takes a +//! trailing `const CancelToken&`. //! * `check_library()` verifies the producer's ABI revision and every //! top-level module's contract table once, and throws `LoadError` naming //! the first declaration the library lacks or changed. Every free @@ -51,29 +50,32 @@ mod package; mod runtime; mod types; -use crate::backend::{LanguageBackend, OutputFile}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{per_platform_libraries, Artifact, PackageContext, PackagedFile}; use crate::platform::Platform; use crate::targets::c::{header_name as c_header_name, render_c_header_from_model}; +use crate::targets::{Linkage, Target}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use camino::Utf8Path; +use camino::Utf8PathBuf; +use miette::Result; use serde::{Deserialize, Serialize}; use weaveffi_model::model::{EnumBinding, Model}; use weaveffi_model::pkg::Identity; +use crate::codegen::errors; use crate::targets::cpp::callbacks::render_callback_interface; use crate::targets::cpp::calls::render_cpp_module_ns; use crate::targets::cpp::codec::render_codecs; use crate::targets::cpp::entities::{ - render_cpp_enums, render_cpp_interface_class, render_cpp_interface_forward_decls, - render_cpp_interface_iterators, render_cpp_interface_members, render_domain_error, - value_types_in_order, + render_cpp_enums, render_cpp_interface_class, render_cpp_interface_members, + render_domain_error, value_types_in_order, }; use crate::targets::cpp::package::{ render_cmake, render_packaged_cmake, render_packaged_readme, render_readme, CmakeNames, }; use crate::targets::cpp::runtime::{render_buffer_runtime, render_prelude_runtime}; +use crate::targets::cpp::types::Ctx; /// Per-target configuration for [`CppGenerator`]. #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -113,7 +115,15 @@ impl CppConfig { /// C++ backend: emits a wrapper header (`{library}.hpp` by default), a copy /// of the C header it includes, a `CMakeLists.txt`, and a README. -pub struct CppGenerator; +pub struct CppGenerator { + config: CppConfig, +} + +impl From for CppGenerator { + fn from(config: CppConfig) -> Self { + Self { config } + } +} /// The names every rendered file agrees on, resolved once per run. struct Names { @@ -145,15 +155,22 @@ impl Names { } } -impl LanguageBackend for CppGenerator { - type Config = CppConfig; - +impl Target for CppGenerator { fn name(&self) -> &'static str { "cpp" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { - let dir = out_dir.join("cpp"); + fn linkage(&self) -> Linkage { + Linkage::Link + } + + fn fixed_files(&self) -> &'static [&'static str] { + &["CMakeLists.txt", "README.md"] + } + + fn render(&self, model: &Model) -> Vec { + let config = &self.config; + let dir = Utf8PathBuf::new(); let names = Names::new(model, config); let cmake = names.cmake(&model.identity, config); vec![ @@ -171,12 +188,8 @@ impl LanguageBackend for CppGenerator { /// `include/`, the desktop libraries under `lib//`, and a /// `CMakeLists.txt` that links the host's library into the wrapper's /// interface target. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let identity = &model.identity; let names = Names::new(model, config); let cmake = names.cmake(identity, config); @@ -198,11 +211,11 @@ impl LanguageBackend for CppGenerator { // packages) are not bundled here. let desktop = per_platform_libraries(ctx.binaries, "lib", Platform::is_desktop); if desktop.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } files.extend(desktop); let stem = format!("{lib}-{}", identity.version); - Some(vec![Artifact::tar_gz( + Ok(vec![Artifact::tar_gz( format!("cpp/{stem}-cpp.tar.gz"), stem, files, @@ -216,43 +229,47 @@ impl LanguageBackend for CppGenerator { /// Layout inside `namespace {namespace}`, in an order that keeps every type /// complete before it's held by value or marshalled: /// -/// 1. the runtime: error types, the adopt tag, `CancelToken`, helpers, -/// `check_library()` with the expected contract tables, and the -/// value-buffer reader and writer (when any value crosses as a buffer); +/// 1. the runtime: exceptions, the generic `detail` machinery, `Range`, +/// `CancelToken`, `check_library()` with the expected contract tables, +/// and the value-buffer codecs (when any value crosses as a buffer); /// 2. C-style enums; -/// 3. forward declarations of every value type, interface class, -/// callback-interface class, and member range class; -/// 4. interface classes (the RAII skeleton plus member *declarations*), so -/// records can hold objects by value; +/// 3. forward declarations of every value type, interface class, and +/// callback-interface class; +/// 4. interface classes (the handle plus member *declarations*), so records +/// can hold objects by value; /// 5. value types (records and rich enums) in dependency order; -/// 6. the codecs of every value and composite type; -/// 7. typed exception domains; -/// 8. callback-interface abstract classes, trampolines, and vtables; -/// 9. the range classes of iterator-returning members; -/// 10. the out-of-line definitions of every interface member; and -/// 11. one nested namespace per module holding its free functions. +/// 6. the codec overloads of every value type; +/// 7. error domains and their `detail::Errors` policies; +/// 8. callback-interface abstract classes and their `detail::Callbacks`; +/// 9. the out-of-line definitions of every interface member; and +/// 10. one nested namespace per module holding its free functions. pub(crate) fn render_cpp_header( model: &Model, namespace: &str, c_header: &str, filename: &str, ) -> String { - let prefix = model.prefix(); + let ctx = Ctx::new(model); let mut includes = vec![ "cstddef", "cstdint", "cstring", "exception", + "future", + "iterator", "memory", "new", "optional", "stdexcept", "string", "string_view", - "unordered_map", + "type_traits", "utility", "vector", ]; + if model.has_buffers() { + includes.push("unordered_map"); + } if model .modules .iter() @@ -260,12 +277,6 @@ pub(crate) fn render_cpp_header( { includes.push("variant"); } - if model.has_async() { - includes.push("future"); - } - if model.has_iterators() { - includes.push("iterator"); - } includes.sort_unstable(); let mut w = CodeWriter::four_space(); @@ -281,12 +292,14 @@ pub(crate) fn render_cpp_header( w.line(format!("namespace {namespace} {{")); w.blank(); render_prelude_runtime(&mut w, model); + w.blank(); if model.has_buffers() { - render_buffer_runtime(&mut w, prefix); + render_buffer_runtime(&mut w, ctx.prefix); + w.blank(); } for module in &model.modules { - render_cpp_enums(&mut w, module); + render_cpp_enums(&mut w, &ctx, module); } // Forward declarations let member declarations name any type before its @@ -302,7 +315,7 @@ pub(crate) fn render_cpp_header( forward = true; } for i in &module.interfaces { - render_cpp_interface_forward_decls(&mut w, i); + w.line(format!("class {};", i.name)); forward = true; } for cb in &module.callback_interfaces { @@ -316,40 +329,27 @@ pub(crate) fn render_cpp_header( for module in &model.modules { for i in &module.interfaces { - render_cpp_interface_class(&mut w, i, model.error_domain(module)); + render_cpp_interface_class(&mut w, &ctx, i); } } - for def in value_types_in_order(&model.modules) { - def.render(&mut w); + let values = value_types_in_order(&model.modules); + for def in &values { + def.render(&mut w, &ctx); } - render_codecs(&mut w, model); - - // A domain reports exceptions back to the producer only when a throwing - // callback method has it in scope. - for module in &model.modules { - if let Some(eb) = &module.errors { - let reports = model.callback_interfaces().any(|(m, cb)| { - model.error_domain(m).is_some_and(|d| d.c_tag == eb.c_tag) - && cb.methods.iter().any(|meth| meth.throws) - }); - render_domain_error(&mut w, module, eb, prefix, reports); - } - } - for (module, cb) in model.callback_interfaces() { - render_callback_interface(&mut w, cb, model.error_domain(module), prefix); + render_codecs(&mut w, &values); + for table in errors::tables(model, "Error") { + render_domain_error(&mut w, &ctx, &table); } - for module in &model.modules { - for i in &module.interfaces { - render_cpp_interface_iterators(&mut w, i, model.error_domain(module), prefix); - } + for (_, cb) in model.callback_interfaces() { + render_callback_interface(&mut w, &ctx, cb); } for module in &model.modules { for i in &module.interfaces { - render_cpp_interface_members(&mut w, i, model.error_domain(module), prefix); + render_cpp_interface_members(&mut w, &ctx, i); } } for module in &model.modules { - render_cpp_module_ns(&mut w, module, model.error_domain(module), prefix); + render_cpp_module_ns(&mut w, &ctx, module); } w.line(format!("}} // namespace {namespace}")); w.blank(); diff --git a/crates/weaveffi-cli/src/targets/cpp/runtime.rs b/crates/weaveffi-cli/src/targets/cpp/runtime.rs index 81fb6acc..3512a8d2 100644 --- a/crates/weaveffi-cli/src/targets/cpp/runtime.rs +++ b/crates/weaveffi-cli/src/targets/cpp/runtime.rs @@ -1,23 +1,31 @@ -//! The fixed runtime of the generated header: the error types, the adopt -//! tag, the cancel token, the string and callback-return helpers, the -//! load-time library check, and the value-buffer reader and writer. +//! The fixed runtime of the generated header, written from real C++ sources +//! under `runtime/` with `{{PLACEHOLDER}}` substitution: //! -//! The code lives in real C++ sources under `runtime/`, written into the -//! header with `{{PLACEHOLDER}}` substitution. The library check embeds the -//! contract table of every top-level module (from -//! [`weaveffi_model::contract::entries`]) as `(id, hash, path)` triples, so +//! * `prelude.hpp`: the exception hierarchy, the adopt tag, the generic +//! machinery every wrapper delegates to (`detail::Errors` and +//! `detail::check`, `detail::settle` for async completions, +//! `detail::callback` for trampolines, `detail::Handle` for +//! objects, the run, slice, and optional helpers), `CancelToken`, the +//! generic `Range` every iterator returns, and `check_library()` with +//! the expected contract tables; +//! * `buffer.hpp` (when any value crosses as a value buffer): the +//! value-buffer reader and writer and the overloaded `detail::write` and +//! `detail::read` templates for primitives, enums, objects, optionals, +//! vectors, and maps. +//! +//! The library check embeds the contract rows of every top-level module +//! (from the shared [`contract`] emitter) as `(id, hash, path)` triples, so //! a mismatch names the declaration. +use crate::codegen::contract; use crate::codegen::CodeWriter; -use weaveffi_model::contract; -use weaveffi_model::model::{contract_symbol, Model, ABI_VERSION}; +use weaveffi_model::model::{Model, ABI_VERSION}; -/// The error types, adopt tag, cancel token, helpers, and `check_library()`. -/// `{{CONTRACTS}}` marks where the expected tables go and `{{CHECKS}}` where -/// `check_library()` compares them. +/// The runtime prelude. `{{CONTRACTS}}` marks where the expected tables go +/// and `{{CHECKS}}` where `check_library()` compares them. const PRELUDE: &str = include_str!("runtime/prelude.hpp"); -/// The value-buffer reader, writer, and release guard. +/// The value-buffer runtime. const BUFFER: &str = include_str!("runtime/buffer.hpp"); /// The `detail` array holding a top-level module's expected contract. @@ -29,10 +37,9 @@ fn contract_array(module: &str) -> String { /// ABI revision and every top-level module's contract table against the /// declarations this header was generated with. pub(crate) fn render_prelude_runtime(w: &mut CodeWriter, model: &Model) { - let prefix = model.prefix(); let library = &model.identity.library; let text = PRELUDE - .replace("{{PREFIX}}", prefix) + .replace("{{PREFIX}}", model.prefix()) .replace("{{MACRO}}", &model.identity.macro_prefix()) .replace("{{LIBRARY}}", library) .replace("{{ABI}}", &ABI_VERSION.to_string()); @@ -42,29 +49,31 @@ pub(crate) fn render_prelude_runtime(w: &mut CodeWriter, model: &Model) { let (middle, tail) = rest .split_once("{{CHECKS}}") .expect("the prelude marks the contract checks"); - let roots: Vec<_> = model - .roots() - .map(|root| (root, contract::entries(model, root))) - .filter(|(_, entries)| !entries.is_empty()) + let tables: Vec<_> = contract::tables(model) + .into_iter() + .filter(|t| !t.rows.is_empty()) .collect(); w.raw(head); - for (root, entries) in &roots { + for table in &tables { w.line(format!( "/** The declarations of module `{}` this header was generated with. */", - root.dot_path + table.root.dot_path )); w.block( format!( "inline constexpr ContractEntry {}[] = {{", - contract_array(&root.name) + contract_array(&table.root.name) ), "};", |w| { - for e in entries { + for row in &table.rows { w.line(format!( - "{{{:#018x}ull, {:#018x}ull, \"{}\"}},", - e.id, e.hash, e.path + "{{{}ull, {}ull, \"{}\"}}, // {}", + contract::hex(row.id), + contract::hex(row.hash), + row.path, + row.signature )); } }, @@ -74,12 +83,12 @@ pub(crate) fn render_prelude_runtime(w: &mut CodeWriter, model: &Model) { w.raw(middle); // The checks sit inside `check_library()`'s initializing lambda. w.indent().indent(); - for (root, _) in &roots { + for table in &tables { w.block( format!( "if (std::string why = detail::contract_mismatch({}, detail::{}); !why.empty()) {{", - contract_symbol(prefix, &root.name), - contract_array(&root.name) + table.symbol, + contract_array(&table.root.name) ), "}", |w| { diff --git a/crates/weaveffi-cli/src/targets/cpp/runtime/buffer.hpp b/crates/weaveffi-cli/src/targets/cpp/runtime/buffer.hpp index a04f2e4e..4cb099ee 100644 --- a/crates/weaveffi-cli/src/targets/cpp/runtime/buffer.hpp +++ b/crates/weaveffi-cli/src/targets/cpp/runtime/buffer.hpp @@ -2,18 +2,10 @@ namespace detail { /** * Serializes values into the value-buffer wire format: little-endian, packed - * with no alignment, lengths and element counts as u32. + * with no alignment, lengths and element counts as u32. The `write` + * overloads below encode one value of each type. */ class BufferWriter { - std::vector buf_; - - template - void append_le(T v) { - for (size_t i = 0; i < sizeof(T); ++i) { - buf_.push_back(static_cast(v >> (8 * i))); - } - } - public: /** Pointer to the encoded bytes. */ const uint8_t* data() const noexcept { return buf_.data(); } @@ -21,207 +13,317 @@ class BufferWriter { /** Number of encoded bytes. */ size_t size() const noexcept { return buf_.size(); } - void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } - void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } - void write_u8(uint8_t v) { buf_.push_back(v); } - void write_i16(int16_t v) { append_le(static_cast(v)); } - void write_u16(uint16_t v) { append_le(v); } - void write_i32(int32_t v) { append_le(static_cast(v)); } - void write_u32(uint32_t v) { append_le(v); } - void write_i64(int64_t v) { append_le(static_cast(v)); } - void write_u64(uint64_t v) { append_le(v); } - - void write_f32(float v) { - uint32_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - void write_f64(double v) { - uint64_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } + /** Appends one byte. */ + void byte(uint8_t v) { buf_.push_back(v); } - /** Writes a byte length or an element count as a u32. */ - void write_len(size_t n) { - if (static_cast(n) > UINT32_MAX) throw std::length_error("value buffer length exceeds u32"); - append_le(static_cast(n)); + /** Appends an unsigned integer, little-endian. */ + template + void le(U v) { + for (size_t i = 0; i < sizeof(U); ++i) buf_.push_back(static_cast(v >> (8 * i))); } - void write_string(std::string_view v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); + /** Appends a byte length or an element count as a u32, failing with -3 past u32. */ + void len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + le(static_cast(n)); } - void write_bytes(const std::vector& v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); + /** Appends `n` raw bytes. */ + void bytes(const void* data, size_t n) { + const uint8_t* p = static_cast(data); + buf_.insert(buf_.end(), p, p + n); } - /** Writes an optional's presence flag: 0 absent, 1 present. */ - void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } - - /** - * Writes an object token: a fresh strong reference from the wrapper's - * `_clone`, which the reader adopts while `v` keeps its own. - */ - template - void write_object(const T& v) { - write_u64(static_cast(reinterpret_cast(v.clone_handle()))); - } +private: + std::vector buf_; }; /** * Decodes values from the value-buffer wire format. A malformed buffer is a * producer bug (both sides are generated from one API), so every decode * failure throws InternalError with the marshalling code -3. Object tokens - * adopted before a failure live in RAII wrappers that release them on - * unwind. + * adopted before a failure live in wrappers that release them on unwind. */ class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - void require(size_t n, const char* what) const { - if (len_ - pos_ < n) fail(what); - } - - template - T read_le(const char* what) { - require(sizeof(T), what); - uint64_t v = 0; - for (size_t i = 0; i < sizeof(T); ++i) { - v |= static_cast(data_[pos_ + i]) << (8 * i); - } - pos_ += sizeof(T); - return static_cast(v); - } - public: + /** Reads from the `len` bytes at `data` (null when empty). */ + BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0) {} + /** Throws the marshalling failure (-3) for a malformed buffer. */ [[noreturn]] static void fail(const char* what) { throw InternalError(-3, std::string("malformed value buffer: ") + what); } - /** Reads from the `len` bytes at `data` (null when empty). */ - BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} - /** Bytes not yet consumed. */ size_t remaining() const noexcept { return len_ - pos_; } - bool read_bool() { - uint8_t b = read_le("bool"); - if (b > 1) fail("bool byte out of range"); - return b != 0; + /** Consumes `n` bytes, returning where they start. */ + const uint8_t* take(size_t n, const char* what) { + if (remaining() < n) fail(what); + const uint8_t* p = data_ + pos_; + pos_ += n; + return p; } - int8_t read_i8() { return read_le("i8"); } - uint8_t read_u8() { return read_le("u8"); } - int16_t read_i16() { return read_le("i16"); } - uint16_t read_u16() { return read_le("u16"); } - int32_t read_i32() { return read_le("i32"); } - uint32_t read_u32() { return read_le("u32"); } - int64_t read_i64() { return read_le("i64"); } - uint64_t read_u64() { return read_le("u64"); } - - float read_f32() { - uint32_t bits = read_le("f32"); - float v = 0; - std::memcpy(&v, &bits, sizeof(v)); + /** Reads an unsigned integer, little-endian. */ + template + U le(const char* what) { + const uint8_t* p = take(sizeof(U), what); + U v = 0; + for (size_t i = 0; i < sizeof(U); ++i) v = static_cast(v | static_cast(static_cast(p[i]) << (8 * i))); return v; } - double read_f64() { - uint64_t bits = read_le("f64"); - double v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; + /** Reads a 0/1 byte (a bool or an optional's presence flag). */ + bool flag(const char* what) { + uint8_t b = le(what); + if (b > 1) fail(what); + return b != 0; } /** Reads a byte length, rejecting one larger than the bytes remaining. */ - size_t read_len() { - uint32_t n = read_le("length"); - if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); - return static_cast(n); + size_t len() { + size_t n = le("length"); + if (n > remaining()) fail("length prefix exceeds remaining buffer"); + return n; } /** * Reads an element count. Elements may encode to zero bytes, so a count - * larger than the bytes remaining is legal; callers cap preallocation - * with reserve_hint(). + * larger than the bytes remaining is legal; preallocate with reserve_hint(). */ - size_t read_count() { return static_cast(read_le("count")); } + size_t count() { return le("count"); } /** A preallocation size for `n` elements that a malformed count can't inflate. */ size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } - std::string read_string() { - size_t n = read_len(); - std::string v(reinterpret_cast(data_) + pos_, n); - pos_ += n; - return v; - } - - std::vector read_bytes() { - size_t n = read_len(); - std::vector v(data_ + pos_, data_ + pos_ + n); - pos_ += n; - return v; - } - - bool read_option_flag() { - uint8_t b = read_le("option flag"); - if (b > 1) fail("option flag byte out of range"); - return b != 0; - } - - /** Reads an object token, adopting the strong reference it carries into a `T` wrapper. */ - template - T read_object() { - auto* raw = reinterpret_cast(static_cast(read_u64())); - if (raw == nullptr) fail("null object token"); - return T(adopt, raw); - } - /** Rejects unconsumed bytes after decoding a complete value. */ void expect_end() const { if (pos_ != len_) fail("trailing bytes after value"); } + +private: + const uint8_t* data_; + size_t len_; + size_t pos_ = 0; }; -/** Releases a producer-allocated buffer with {{PREFIX}}_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; +/** Selects a `read` overload by the type it decodes. */ +template +struct type_tag {}; + +/** Reads one `T` (declared below and per value type). */ +template +T read(BufferReader& r) { + return read(r, type_tag{}); +} + +inline void write(BufferWriter& w, bool v) { w.byte(v ? 1 : 0); } +inline void write(BufferWriter& w, int8_t v) { w.byte(static_cast(v)); } +inline void write(BufferWriter& w, uint8_t v) { w.byte(v); } +inline void write(BufferWriter& w, int16_t v) { w.le(static_cast(v)); } +inline void write(BufferWriter& w, uint16_t v) { w.le(v); } +inline void write(BufferWriter& w, int32_t v) { w.le(static_cast(v)); } +inline void write(BufferWriter& w, uint32_t v) { w.le(v); } +inline void write(BufferWriter& w, int64_t v) { w.le(static_cast(v)); } +inline void write(BufferWriter& w, uint64_t v) { w.le(v); } + +inline void write(BufferWriter& w, float v) { + uint32_t bits = 0; + std::memcpy(&bits, &v, sizeof bits); + w.le(bits); +} + +inline void write(BufferWriter& w, double v) { + uint64_t bits = 0; + std::memcpy(&bits, &v, sizeof bits); + w.le(bits); +} + +inline void write(BufferWriter& w, std::string_view v) { + w.len(v.size()); + w.bytes(v.data(), v.size()); +} + +inline void write(BufferWriter& w, const std::vector& v) { + w.len(v.size()); + w.bytes(v.data(), v.size()); +} + +inline bool read(BufferReader& r, type_tag) { return r.flag("bool"); } +inline int8_t read(BufferReader& r, type_tag) { return static_cast(r.le("i8")); } +inline uint8_t read(BufferReader& r, type_tag) { return r.le("u8"); } +inline int16_t read(BufferReader& r, type_tag) { return static_cast(r.le("i16")); } +inline uint16_t read(BufferReader& r, type_tag) { return r.le("u16"); } +inline int32_t read(BufferReader& r, type_tag) { return static_cast(r.le("i32")); } +inline uint32_t read(BufferReader& r, type_tag) { return r.le("u32"); } +inline int64_t read(BufferReader& r, type_tag) { return static_cast(r.le("i64")); } +inline uint64_t read(BufferReader& r, type_tag) { return r.le("u64"); } + +inline float read(BufferReader& r, type_tag) { + uint32_t bits = r.le("f32"); + float v = 0; + std::memcpy(&v, &bits, sizeof v); + return v; +} + +inline double read(BufferReader& r, type_tag) { + uint64_t bits = r.le("f64"); + double v = 0; + std::memcpy(&v, &bits, sizeof v); + return v; +} + +inline std::string read(BufferReader& r, type_tag) { + size_t n = r.len(); + return std::string(reinterpret_cast(r.take(n, "string")), n); +} + +inline std::vector read(BufferReader& r, type_tag>) { + size_t n = r.len(); + const uint8_t* p = r.take(n, "bytes"); + return std::vector(p, p + n); +} + +/** A C-style enum crosses as its `int32_t` discriminant. */ +template , int> = 0> +void write(BufferWriter& w, E v) { + write(w, static_cast(v)); +} + +template , int> = 0> +E read(BufferReader& r, type_tag) { + return static_cast(read(r)); +} - BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} - BufferGuard(const BufferGuard&) = delete; - BufferGuard& operator=(const BufferGuard&) = delete; +/** Whether `T` is an interface wrapper (it names the C type it wraps). */ +template +struct is_object : std::false_type {}; - ~BufferGuard() { - if (ptr != nullptr) {{PREFIX}}_free_bytes(const_cast(ptr), len); +template +struct is_object> : std::true_type {}; + +/** + * An object crosses as a token: a fresh strong reference from the wrapper's + * `_clone`, which the reader adopts while the writer's wrapper keeps its own. + */ +template ::value, int> = 0> +void write(BufferWriter& w, const T& v) { + write(w, static_cast(reinterpret_cast(v.clone_handle()))); +} + +template ::value, int> = 0> +T read(BufferReader& r, type_tag) { + auto* raw = reinterpret_cast(static_cast(read(r))); + if (raw == nullptr) BufferReader::fail("null object token"); + return T(adopt, raw); +} + +/** `T?`: a presence flag, then the value when present. */ +template +void write(BufferWriter& w, const std::optional& v) { + w.byte(v.has_value() ? 1 : 0); + if (v.has_value()) write(w, *v); +} + +template +std::optional read(BufferReader& r, type_tag>) { + if (!r.flag("option flag")) return std::nullopt; + return std::optional(read(r)); +} + +/** `[T]`: a u32 count, then the elements. */ +template +void write(BufferWriter& w, const std::vector& v) { + w.len(v.size()); + for (const auto& item : v) write(w, item); +} + +template +std::vector read(BufferReader& r, type_tag>) { + size_t n = r.count(); + std::vector v; + v.reserve(r.reserve_hint(n)); + for (size_t i = 0; i < n; ++i) v.push_back(read(r)); + return v; +} + +/** `{K: V}`: a u32 count, then each key and value. A repeated key is malformed. */ +template +void write(BufferWriter& w, const std::unordered_map& v) { + w.len(v.size()); + for (const auto& entry : v) { + write(w, entry.first); + write(w, entry.second); } -}; +} + +template +std::unordered_map read(BufferReader& r, type_tag>) { + size_t n = r.count(); + std::unordered_map v; + v.reserve(r.reserve_hint(n)); + for (size_t i = 0; i < n; ++i) { + K key = read(r); + V value = read(r); + if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); + } + return v; +} + +/** Encodes one value into a fresh buffer. */ +template +BufferWriter encode(const T& value) { + BufferWriter w; + write(w, value); + return w; +} -/** Decodes one complete `T` from a borrowed buffer with `read`, rejecting trailing bytes. */ +/** Decodes one complete `T` from a borrowed buffer, rejecting trailing bytes. */ template -T decode(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { +T decode(const uint8_t* ptr, size_t len) { BufferReader r(ptr, len); - T value = read(r); + T value = read(r); r.expect_end(); return value; } -/** Decodes one complete `T` from a producer-returned buffer with `read`, then releases the buffer. */ +/** Decodes one complete `T` from a producer-returned buffer, then releases the buffer. */ template -T take(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferGuard guard(ptr, len); - return decode(ptr, len, read); +T take(const uint8_t* ptr, size_t len) { + Run run{ptr, len}; + return decode(ptr, len); } -} // namespace detail +/** Hands a callback's buffered return to the producer as a {{PREFIX}}_alloc run. */ +inline void give(const BufferWriter& value, uint8_t** out_ptr, size_t* out_len) { + give_run(value.data(), value.size(), out_ptr, out_len); +} + +/** Throws the error class `E` of a domain code with fields, decoding them from the error's payload. */ +template +[[noreturn]] void raise_with_fields(const error& err) { + BufferReader r(err.payload_ptr, err.payload_len); + E e{message(err), read(r)...}; + r.expect_end(); + throw e; +} +/** + * Reports a domain exception from a callback: its code and message, and its + * `fields` encoded as the payload. A field that can't be encoded reports a + * plain failure (-1) instead. + */ +template +void report_with_fields(error* out_err, const Error& e, const Fields&... fields) noexcept { + try { + BufferWriter payload; + (write(payload, fields), ...); + set_error(out_err, e.code(), e.what()); + {{PREFIX}}_error_set_payload(out_err, payload.data(), payload.size()); + } catch (...) { + set_error(out_err, -1, e.what()); + } +} + +} // namespace detail diff --git a/crates/weaveffi-cli/src/targets/cpp/runtime/prelude.hpp b/crates/weaveffi-cli/src/targets/cpp/runtime/prelude.hpp index 04c37774..c4edbcf3 100644 --- a/crates/weaveffi-cli/src/targets/cpp/runtime/prelude.hpp +++ b/crates/weaveffi-cli/src/targets/cpp/runtime/prelude.hpp @@ -1,21 +1,24 @@ /** - * An error a call that declares errors reports. + * The base of every exception this library throws. * - * A positive code is a declared code of the module's error domain, thrown as - * that domain's typed subclass. A negative code is a runtime failure: -1 a - * generic producer error, -2 a producer panic, -3 a marshalling failure, and - * -4 a callback-interface implementation that failed. A cancelled async call - * (-5) throws Cancelled. + * `code()` is the failure's error code. A positive code belongs to an error + * domain and is thrown as that domain's class (or the class of the code). + * A negative code is a runtime failure: -1 an untyped error (a call declared + * `throws: any`), -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that failed, and -5 a cancelled async + * call (thrown as Cancelled). Code 0 is a failure these bindings detected + * without calling the producer (LoadError). */ class Error : public std::runtime_error { - int32_t code_; - public: /** Builds an error carrying `code` and `message`. */ Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} /** The numeric error code. */ int32_t code() const noexcept { return code_; } + +private: + int32_t code_; }; /** Thrown by an async call that was cancelled through its CancelToken (code -5). */ @@ -26,39 +29,27 @@ class Cancelled : public Error { }; /** - * Thrown by a call that declares no errors when it fails anyway: a producer - * panic (-2), a marshalling failure (-3), a callback-interface failure the - * producer let through (-4), or another runtime failure (-1). These are bugs, - * so no signature declares them; `what()` names the code and the producer's - * message. + * Thrown when a call that declares no errors fails anyway: a producer panic + * (-2), a marshalling failure (-3), a callback-interface failure the producer + * let through (-4), or another runtime failure. These are bugs, so no + * signature declares them. A value buffer these bindings can't decode is + * also an InternalError with code -3. */ -class InternalError : public std::runtime_error { - int32_t code_; - std::string message_; - +class InternalError : public Error { public: - /** Builds an internal error carrying `code` and the producer's `message`. */ - InternalError(int32_t code, const std::string& message) - : std::runtime_error("{{LIBRARY}}: runtime error " + std::to_string(code) + ": " + message), - code_(code), - message_(message) {} - - /** The runtime error code (negative). */ - int32_t code() const noexcept { return code_; } - - /** The producer's message. */ - const std::string& message() const noexcept { return message_; } + /** Builds an internal error carrying the runtime `code` and the producer's `message`. */ + InternalError(int32_t code, const std::string& message) : Error(code, message) {} }; /** * Thrown on first use when the linked {{LIBRARY}} library implements a * different C ABI revision than this header, or lacks or changed a - * declaration this header was generated with. + * declaration this header was generated with. Its code is 0. */ -class LoadError : public std::runtime_error { +class LoadError : public Error { public: /** Builds a load error describing the mismatch. */ - explicit LoadError(const std::string& message) : std::runtime_error(message) {} + explicit LoadError(const std::string& message) : Error(0, message) {} }; /** @@ -72,143 +63,499 @@ struct adopt_t { /** The adopt_t tag value: `Store s(adopt, raw)`. */ inline constexpr adopt_t adopt{}; -/** - * A cancellation token for cancellable async functions. - * - * Create one, pass it to any number of cancellable calls, and call cancel() - * from any thread. A cancelled call settles its future with Cancelled. Each - * call takes its own reference to the native token, so the token can be - * destroyed at any time; the destructor releases this wrapper's reference. - */ -class CancelToken { - {{PREFIX}}_cancel_token* handle_; - - struct none_t {}; - explicit CancelToken(none_t) noexcept : handle_(nullptr) {} - -public: - /** Creates a new, uncancelled token. */ - CancelToken() : handle_({{PREFIX}}_cancel_token_create()) { - if (handle_ == nullptr) throw std::bad_alloc(); - } +namespace detail { - /** Releases this wrapper's reference to the native token. */ - ~CancelToken() { - if (handle_ != nullptr) {{PREFIX}}_cancel_token_destroy(handle_); - } +/** The C error slot every fallible call fills. */ +using error = {{PREFIX}}_error; - CancelToken(const CancelToken&) = delete; - CancelToken& operator=(const CancelToken&) = delete; +/** The message of a failed call's error slot (empty when the producer sent none). */ +inline std::string message(const error& err) { + if (err.message_ptr == nullptr || err.message_len == 0) return std::string(); + return std::string(reinterpret_cast(err.message_ptr), err.message_len); +} - /** Transfers the native token; `other` becomes empty. */ - CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } +/** Fills a callback's error slot with `code` and a copy of `message`. */ +inline void set_error(error* out_err, int32_t code, std::string_view message) noexcept { + {{PREFIX}}_error_set(out_err, code, reinterpret_cast(message.data()), message.size()); +} - /** Releases the current token and takes over `other`'s. */ - CancelToken& operator=(CancelToken&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) {{PREFIX}}_cancel_token_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; +/** + * Reports the exception being handled through a callback's error slot as + * `code` with its `what()`. Call it only from inside a catch handler. + */ +inline void report_current(error* out_err, int32_t code) noexcept { + try { + throw; + } catch (const std::exception& e) { + set_error(out_err, code, e.what()); + } catch (...) { + set_error(out_err, code, "a callback implementation threw an exception not derived from std::exception"); } +} - /** The empty token a cancellable call uses when none is passed. */ - static const CancelToken& none() noexcept { - static const CancelToken token{none_t{}}; - return token; - } +/** + * How a call that throws `E` turns a failed error slot into an exception + * (`raise`), and how a callback method that may throw `E` reports the + * exception being handled to the producer (`report`). Specialized for + * InternalError (calls that declare no errors), Error (`throws: any`), and + * each error domain. + */ +template +struct Errors; + +/** Throws Cancelled for -5 and `E` for every other code. */ +template +[[noreturn]] void raise_runtime(const error& err) { + if (err.code == -5) throw Cancelled(message(err)); + throw E(err.code, message(err)); +} - /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ - void cancel() const noexcept { - if (handle_ != nullptr) {{PREFIX}}_cancel_token_cancel(handle_); - } +/** A call that declares no errors: any failure is an InternalError. A callback method reports -4. */ +template <> +struct Errors { + [[noreturn]] static void raise(const error& err) { raise_runtime(err); } + static void report(error* out_err) noexcept { report_current(out_err, -4); } +}; - /** Whether cancel() has been called. */ - bool is_cancelled() const noexcept { - return handle_ != nullptr && {{PREFIX}}_cancel_token_is_cancelled(handle_); - } +/** A call declared `throws: any`: any failure is an Error. A callback method reports -1. */ +template <> +struct Errors { + [[noreturn]] static void raise(const error& err) { raise_runtime(err); } + static void report(error* out_err) noexcept { report_current(out_err, -1); } +}; - /** The native token, borrowed (null for an empty token). */ - {{PREFIX}}_cancel_token* handle() const noexcept { return handle_; } +/** Releases an error slot's message and payload on scope exit. */ +struct ErrorClear { + error* err; + ~ErrorClear() { {{PREFIX}}_error_clear(err); } }; -namespace detail { +/** Throws the `E` exception for a failed call's error slot, which it then clears. */ +template +void check(error& err) { + if (err.code == 0) return; + ErrorClear clear{&err}; + Errors::raise(err); +} -/** The exception for a failed call that declares no errors: Cancelled for -5, else InternalError. */ -inline std::exception_ptr make_internal_error(int32_t code, const std::string& message) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - return std::make_exception_ptr(InternalError(code, message)); +/** Releases an async completion's heap-boxed error. */ +struct ErrorFree { + void operator()(error* err) const noexcept { {{PREFIX}}_error_free(err); } +}; + +/** + * Settles the `std::promise` an async call passed as `context`, exactly + * once: with the `E` exception for a failure, else with `value()` (which + * takes ownership of the result slots). Releases the boxed error. Runs on + * a producer thread inside a C frame, so nothing unwinds out of it. + */ +template +void settle(void* context, error* err, F&& value) noexcept { + std::unique_ptr> promise(static_cast*>(context)); + std::unique_ptr owned(err); + try { + if (err != nullptr && err->code != 0) Errors::raise(*err); + if constexpr (std::is_void_v) { + value(); + promise->set_value(); + } else { + promise->set_value(value()); + } + } catch (...) { + promise->set_exception(std::current_exception()); + } } -/** The message of a nonzero error slot. */ -inline std::string error_message(const {{PREFIX}}_error& err) { - return std::string(err.message != nullptr ? err.message : "unknown error"); +/** + * Runs a callback method's `body` for a trampoline, reporting any exception + * through `out_err` as a method that throws `E` does. Returns the body's + * value, or a zero value after a failure. + */ +template +auto callback(error* out_err, F&& body) noexcept -> decltype(body()) { + try { + return body(); + } catch (...) { + Errors::report(out_err); + } + if constexpr (!std::is_void_v) return decltype(body()){}; } -/** Throws InternalError if `err` carries a nonzero code (a call that declares no errors). */ -inline void check({{PREFIX}}_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_internal_error(err.code, error_message(err)); - {{PREFIX}}_error_clear(&err); - std::rethrow_exception(ex); +/** Releases a producer run (a string, bytes, value buffer, or typed array) on scope exit. */ +struct Run { + const void* ptr; + size_t len; + ~Run() { + if (ptr != nullptr) {{PREFIX}}_free_bytes(static_cast(const_cast(ptr)), len); + } +}; + +/** The byte size of `count` elements of `T`, failing with -3 when it overflows. */ +template +size_t byte_size(size_t count) { + if (count > SIZE_MAX / sizeof(T)) throw Error(-3, "array is too large"); + return count * sizeof(T); } /** Copies a producer-returned UTF-8 run into a string, then releases it. */ inline std::string take_string(const uint8_t* ptr, size_t len) { + Run run{ptr, len}; if (ptr == nullptr) return std::string(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { {{PREFIX}}_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; return std::string(reinterpret_cast(ptr), len); } /** Copies a producer-returned byte run into a vector, then releases it. */ inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + Run run{ptr, len}; if (ptr == nullptr) return std::vector(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { {{PREFIX}}_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; return std::vector(ptr, ptr + len); } -/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +/** Copies a producer-returned typed array of `len` elements into a vector, then releases it. */ +template +std::vector take_slice(const T* ptr, size_t len) { + Run run{ptr, len * sizeof(T)}; + if (ptr == nullptr) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run as a string_view (a zero-length run may be dangling). */ inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { - if (ptr == nullptr || len == 0) return std::string_view(); + if (len == 0) return std::string_view(); return std::string_view(reinterpret_cast(ptr), len); } -/** Copies a borrowed byte run (null when empty) into a vector. */ +/** Copies a borrowed byte run into a vector. */ inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr || len == 0) return std::vector(); + if (len == 0) return std::vector(); return std::vector(ptr, ptr + len); } +/** Copies a borrowed typed array of `len` elements into a vector. */ +template +std::vector borrow_slice(const T* ptr, size_t len) { + if (len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + /** The data pointer of a string argument, as the C ABI expects it. */ inline const uint8_t* utf8(std::string_view s) noexcept { return reinterpret_cast(s.data()); } +/** The C value of a scalar or C-style enum: an enum crosses as its `int32_t` discriminant. */ +template +constexpr auto to_c(T value) noexcept { + if constexpr (std::is_enum_v) { + return static_cast>(value); + } else { + return value; + } +} + +/** The C value slot of an optional scalar or enum argument: its value, or zero when absent. */ +template +constexpr auto value_or_zero(const std::optional& value) noexcept { + return to_c(value.has_value() ? *value : T{}); +} + +/** An optional scalar or enum from its C presence flag and value. */ +template +std::optional lift_optional(bool present, C value) noexcept { + if (!present) return std::nullopt; + return std::optional(static_cast(value)); +} + +/** A wrapper adopting a returned object reference, or none for null. */ +template +std::optional adopt_optional(typename T::raw_type* raw) noexcept { + if (raw == nullptr) return std::nullopt; + return std::optional(std::in_place, adopt, raw); +} + +/** The borrowed C pointer of an optional object argument (null for none). */ +template +const typename T::raw_type* handle_of(const std::optional& object) noexcept { + return object.has_value() ? object->handle() : nullptr; +} + +/** A new strong reference to an optional object a callback returns (null for none). */ +template +typename T::raw_type* clone_of(const std::optional& object) noexcept { + return object.has_value() ? object->clone_handle() : nullptr; +} + +/** Hands a callback's optional scalar or enum return to the producer: writes the value, returns presence. */ +template +bool give_optional(const std::optional& value, C* out_value) noexcept { + if (!value.has_value()) return false; + *out_value = static_cast(*value); + return true; +} + /** * Hands `len` bytes from `data` to the producer through a callback method's * out slots: copies them into a run from {{PREFIX}}_alloc, which the * producer adopts and frees. */ -inline void hand_over(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { +template +void give_run(const void* data, size_t len, T** out_ptr, size_t* out_len) { uint8_t* run = nullptr; if (len > 0) { run = {{PREFIX}}_alloc(len); if (run == nullptr) throw std::bad_alloc(); std::memcpy(run, data, len); } - *out_ptr = run; + *out_ptr = reinterpret_cast(run); *out_len = len; } +/** Hands a callback's string return to the producer. */ +inline void give(std::string_view value, uint8_t** out_ptr, size_t* out_len) { + give_run(value.data(), value.size(), out_ptr, out_len); +} + +/** Hands a callback's bytes return to the producer. */ +inline void give(const std::vector& value, uint8_t** out_ptr, size_t* out_len) { + give_run(value.data(), value.size(), out_ptr, out_len); +} + +/** Hands a callback's typed-array return to the producer; `out_len` receives the element count. */ +template +void give_slice(const std::vector& value, T** out_ptr, size_t* out_len) { + give_run(value.data(), byte_size(value.size()), out_ptr, out_len); + *out_len = value.size(); +} + +/** The implementation a callback interface's `ctx` boxes. */ +template +I& implementation(void* ctx) noexcept { + return **static_cast*>(ctx); +} + +/** Deletes a callback interface's `ctx` box once the producer releases it. */ +template +void release(void* ctx) noexcept { + delete static_cast*>(ctx); +} + +/** + * Boxes an implementation as a callback interface's `ctx` (null for an + * empty pointer). The caller releases the box into the call, after which + * the producer owns it and deletes it through the vtable's `free`. + */ +template +std::unique_ptr> lend(std::shared_ptr impl) { + if (!impl) return nullptr; + return std::make_unique>(std::move(impl)); +} + +/** Boxes a required implementation, failing with -3 (naming `param`) when it's empty. */ +template +std::unique_ptr> lend_required(std::shared_ptr impl, const char* param) { + if (!impl) throw Error(-3, std::string(param) + ": null callback interface"); + return lend(std::move(impl)); +} + +/** + * The trampolines and static vtable that adapt implementations of the + * callback interface `I` to its C vtable; specialized per callback + * interface. + */ +template +struct Callbacks; + +/** + * Owns one strong reference to a reference-counted producer object, the + * Rule-of-Zero core of every interface wrapper: copying takes a new + * reference through `Traits::clone`, moving transfers it (leaving null), and + * destruction releases it through `Traits::destroy`. + */ +template +class Handle { +public: + /** The C type of the object. */ + using raw_type = typename Traits::raw_type; + + /** An empty handle. */ + Handle() noexcept = default; + + /** Adopts one strong reference (or null). */ + explicit Handle(raw_type* raw) noexcept : raw_(raw) {} + + /** Takes a new strong reference to `other`'s object. */ + Handle(const Handle& other) noexcept : raw_(other.clone()) {} + + /** Transfers `other`'s reference; `other` becomes empty. */ + Handle(Handle&& other) noexcept : raw_(std::exchange(other.raw_, nullptr)) {} + + /** Copy-and-swap assignment from a copy or a moved-from handle. */ + Handle& operator=(Handle other) noexcept { + std::swap(raw_, other.raw_); + return *this; + } + + /** Releases the reference. */ + ~Handle() { + if (raw_ != nullptr) Traits::destroy(raw_); + } + + /** The object, borrowed (null when empty). */ + raw_type* get() const noexcept { return raw_; } + + /** A new strong reference to the object, which the caller owns (null when empty). */ + raw_type* clone() const noexcept { return raw_ != nullptr ? Traits::clone(raw_) : nullptr; } + + /** Releases the current reference and adopts `raw`. */ + void reset(raw_type* raw) noexcept { *this = Handle(raw); } + +private: + raw_type* raw_ = nullptr; +}; + +} // namespace detail + +/** + * A cancellation token for cancellable async functions. + * + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time. Move-only. + */ +class CancelToken { +public: + /** Creates a new, uncancelled token. */ + CancelToken() : handle_({{PREFIX}}_cancel_token_create()) { + if (!handle_) throw std::bad_alloc(); + } + + /** The empty token a cancellable call uses when none is passed; it never cancels. */ + static const CancelToken& none() noexcept { + static const CancelToken token{nullptr}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_) {{PREFIX}}_cancel_token_cancel(handle_.get()); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { return handle_ && {{PREFIX}}_cancel_token_is_cancelled(handle_.get()); } + + /** The native token, borrowed (null for an empty token). */ + {{PREFIX}}_cancel_token* handle() const noexcept { return handle_.get(); } + +private: + struct Destroy { + void operator()({{PREFIX}}_cancel_token* token) const noexcept { {{PREFIX}}_cancel_token_destroy(token); } + }; + + explicit CancelToken(std::nullptr_t) noexcept {} + + std::unique_ptr<{{PREFIX}}_cancel_token, Destroy> handle_; +}; + +/** + * A lazy, single-pass, move-only range over the elements a producer + * iterator yields: what every `iter` callable returns. + * + * Each step makes one producer call, so results stream in constant memory: + * + * for (const std::string& key : store.keys(std::nullopt)) { ... } + * + * The range releases the producer iterator exactly once: when it's + * exhausted, when a step fails (rethrowing the call's exception), on + * close(), or from the destructor when iteration stops early. Moving a range + * invalidates its iterators. + */ +template +class Range { +public: + /** The element type. */ + using value_type = T; + + /** Pulls one element into `item`; returns false once exhausted, throws on a producer error. */ + using Next = bool (*)(void* iter, std::optional& item); + + /** Releases the producer iterator. */ + using Destroy = void (*)(void* iter); + + /** Adopts a producer iterator, driven by `next` and released by `destroy`. */ + Range(adopt_t, void* iter, Next next, Destroy destroy) noexcept : iter_(iter, destroy), next_(next) {} + + /** Pulls the next element, or `std::nullopt` once the range is exhausted. */ + std::optional next() { + std::optional item; + if (!iter_) return item; + bool more = false; + try { + more = next_(iter_.get(), item); + } catch (...) { + iter_.reset(); + throw; + } + if (!more) iter_.reset(); + return item; + } + + /** Releases the producer iterator now; the range is then exhausted. */ + void close() noexcept { iter_.reset(); } + + /** Marks the end of the range. */ + struct sentinel {}; + + /** A single-pass input iterator; each increment pulls one element. */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; + + iterator() noexcept = default; + + reference operator*() const { return *range_->current_; } + pointer operator->() const { return &*range_->current_; } + + iterator& operator++() { + range_->current_ = range_->next(); + return *this; + } + + void operator++(int) { ++*this; } + + friend bool operator==(const iterator& it, sentinel) noexcept { return it.done(); } + friend bool operator==(sentinel end, const iterator& it) noexcept { return it == end; } + friend bool operator!=(const iterator& it, sentinel end) noexcept { return !(it == end); } + friend bool operator!=(sentinel end, const iterator& it) noexcept { return !(it == end); } + + private: + friend class Range; + explicit iterator(Range* range) noexcept : range_(range) {} + bool done() const noexcept { return !range_->current_.has_value(); } + Range* range_ = nullptr; + }; + + /** Begins iteration by pulling the first element. */ + iterator begin() { + current_ = next(); + return iterator(this); + } + + /** The end sentinel. */ + sentinel end() const noexcept { return sentinel{}; } + +private: + std::unique_ptr iter_; + Next next_; + std::optional current_; +}; + +namespace detail { + /** One declaration this header was generated with: its contract id and hash, and its dotted path. */ struct ContractEntry { /** FNV-1a 64 of the declaration's dotted path. */ @@ -272,4 +619,3 @@ inline void check_library() { }(); if (!failure.empty()) throw LoadError(failure); } - diff --git a/crates/weaveffi-cli/src/targets/cpp/tests.rs b/crates/weaveffi-cli/src/targets/cpp/tests.rs index ed779231..4441d3b5 100644 --- a/crates/weaveffi-cli/src/targets/cpp/tests.rs +++ b/crates/weaveffi-cli/src/targets/cpp/tests.rs @@ -1,452 +1,217 @@ -//! Unit tests for the behavior the snapshots don't make obvious: the -//! contract table the library check embeds, the vtable layout, callback -//! returns and errors, optional callbacks, the trap policy, one codec per -//! composite type, and the ordering that keeps adopted objects from leaking. +//! Target-specific unit tests: file naming and configuration, C++ keyword +//! and wrapper-member escaping, local names that dodge parameters, and doc +//! and deprecation text in C++ spelling. The `kitchen_sink` snapshot pins the +//! rest of the generated surface. -use camino::Utf8Path; -use weaveffi_model::contract; -use weaveffi_model::ir::{ - Api, CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, - InterfaceDef, Module, Param, StructDef, StructField, TypeRef, -}; use weaveffi_model::model::Model; +use weaveffi_model::parse::parse_api_str; use weaveffi_model::pkg::Identity; -use weaveffi_model::ty::Prim; use weaveffi_model::validate::validate; -use crate::backend::LanguageBackend; +use crate::codegen::test_model; use crate::targets::cpp::{render_cpp_header, CppConfig, CppGenerator}; +use crate::targets::Target; -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } -} - -fn throwing(f: Function) -> Function { - Function { throws: true, ..f } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) -} - -fn optional(ty: TypeRef) -> TypeRef { - TypeRef::Optional(Box::new(ty)) -} - -fn list(ty: TypeRef) -> TypeRef { - TypeRef::List(Box::new(ty)) -} - -fn string() -> TypeRef { - TypeRef::Prim(Prim::String) -} - -/// One module with an interface, a record holding objects, a C-style enum, -/// an error domain with a payload-carrying code, and a callback interface -/// whose methods take objects and return every family (one of them -/// `throws`), passed both required and optional. -fn fixture() -> Api { - let store = InterfaceDef { - name: "Store".into(), - doc: None, - deprecated: None, - constructors: vec![func("new", vec![param("path", string())], None)], - methods: vec![ - throwing(func("save", vec![param("contact", named("Contact"))], None)), - func("count", vec![], Some(TypeRef::Prim(Prim::U32))), - ], - statics: vec![], - }; - let listener = CallbackInterfaceDef { - name: "Listener".into(), - doc: None, - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", string()), - param("contact", named("Contact")), - param("store", named("Store")), - ], - Some(TypeRef::Prim(Prim::Bool)), - ), - func( - "on_reset", - vec![param("alt", optional(named("Store")))], - None, - ), - func( - "pick", - vec![param("color", named("Color"))], - Some(named("Color")), - ), - throwing(func("label", vec![], Some(string()))), - func("blob", vec![], Some(TypeRef::Prim(Prim::Bytes))), - func("latest", vec![], Some(optional(named("Contact")))), - func("home", vec![], Some(named("Store"))), - func("spare", vec![], Some(optional(named("Store")))), - ], - }; - let variant = |name: &str, value: i32| EnumVariant { - name: name.into(), - value, - doc: None, - fields: vec![], - }; - Api { - version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: "kv".into(), - doc: None, - functions: vec![ - func("count", vec![], Some(TypeRef::Prim(Prim::U32))), - throwing(func("fetch", vec![], Some(named("Contact")))), - func( - "subscribe", - vec![param("listener", named("Listener"))], - None, - ), - func( - "maybe_subscribe", - vec![param("listener", optional(named("Listener")))], - None, - ), - func( - "first", - vec![param("stores", list(named("Store")))], - Some(named("Store")), - ), - ], - interfaces: vec![store], - callback_interfaces: vec![listener], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - deprecated: None, - fields: vec![ - field("name", string()), - field("store", named("Store")), - field("mirrors", list(named("Store"))), - field("primary", optional(named("Store"))), - ], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - deprecated: None, - variants: vec![variant("Red", 0), variant("Blue", 1)], - }], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "NOT_FOUND".into(), - code: 1, - message: "not found".into(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "TOO_BIG".into(), - code: 2, - message: "too big".into(), - doc: None, - fields: vec![field("limit", TypeRef::Prim(Prim::I64))], - }, - ], - }), - modules: vec![], - }], - } -} - -fn render() -> (Model, String) { - let model = validate(&fixture(), &Identity::named("api"), None).expect("fixture validates"); - let header = render_cpp_header(&model, "api", "api.h", "api.hpp"); - (model, header) +fn header(model: &Model) -> String { + render_cpp_header(model, "kv", "kv.h", "kv.hpp") } +#[track_caller] fn assert_contains(header: &str, needle: &str) { assert!( header.contains(needle), - "expected the header to contain {needle:?}\n---\n{header}" + "expected the header to contain:\n{needle}\n---\n{header}" ); } -/// The text of the function or block that starts with `start`, up to the -/// first line that closes it at the same indentation. -fn block<'a>(header: &'a str, start: &str) -> &'a str { - let at = header - .find(start) - .unwrap_or_else(|| panic!("missing {start:?}")); - let line_start = header[..at].rfind('\n').map_or(0, |i| i + 1); - let indent = &header[line_start..at]; - let close = format!("\n{indent}}}"); - let end = header[at..] - .find(&close) - .unwrap_or_else(|| panic!("unterminated {start:?}")); - &header[at..at + end + close.len()] -} - #[test] -fn library_check_embeds_every_contract_entry() { - let (model, h) = render(); - let root = model.roots().next().expect("one root"); - let entries = contract::entries(&model, root); - assert!(!entries.is_empty()); - for e in &entries { - assert_contains( - &h, - &format!( - "{{{:#018x}ull, {:#018x}ull, \"{}\"}},", - e.id, e.hash, e.path - ), - ); - } - assert_contains( - &h, - "if (std::string why = detail::contract_mismatch(api_kv_contract, detail::kv_contract); !why.empty()) {", +fn identity_names_every_file_and_config_overrides_win() { + let api = parse_api_str( + r#" +version: "0.12.0" +modules: + - name: m + functions: + - { name: ping, params: [], return: string } +"#, + "yaml", + ) + .expect("valid YAML"); + let model = validate(&api, &Identity::named("kv-store"), None).expect("valid API"); + let files = CppGenerator::from(CppConfig::default()).render(&model); + let paths: Vec = files.iter().map(|f| f.path.to_string()).collect(); + assert_eq!( + paths, + ["kv_store.h", "kv_store.hpp", "CMakeLists.txt", "README.md"] + ); + let hpp = &files[1].contents; + assert_contains(hpp, "#include \"kv_store.h\""); + assert_contains(hpp, "namespace kv_store {"); + assert_contains(hpp, "return \"kv_store: \" + why;"); + assert!( + !hpp.contains("class BufferWriter"), + "no value buffers, no codec runtime" ); - assert_contains(&h, "\" is missing from the library\""); - assert_contains(&h, "\" changed since these bindings were generated\""); - assert!(!h.contains("checksum"), "the revision 3 checksum is gone"); -} -#[test] -fn vtable_starts_with_the_header_then_methods_in_order() { - let (_, h) = render(); - let vtable = block(&h, "static const api_kv_Listener_vtable vtable = {"); - let order: Vec = [ - "static_cast(sizeof(api_kv_Listener_vtable)),", - "0,", - "&Listener_trampolines::free_ctx,", - "&Listener_trampolines::on_message,", - "&Listener_trampolines::on_reset,", - "&Listener_trampolines::pick,", - "&Listener_trampolines::label,", - "&Listener_trampolines::blob,", - "&Listener_trampolines::latest,", - "&Listener_trampolines::home,", - "&Listener_trampolines::spare,", - ] - .iter() - .map(|entry| { - vtable - .find(entry) - .unwrap_or_else(|| panic!("missing {entry}")) - }) - .collect(); - assert!(order.windows(2).all(|w| w[0] < w[1]), "{vtable}"); + let config = CppConfig { + name: Some("acme::kv".into()), + header_name: Some("acme_kv.hpp".into()), + standard: Some("20".into()), + }; + let files = CppGenerator::from(config).render(&model); + assert_eq!(files[1].path, "acme_kv.hpp"); + assert_contains(&files[1].contents, "namespace acme::kv {"); + assert_contains(&files[1].contents, "#include \"kv_store.h\""); + assert_contains(&files[2].contents, "cxx_std_20"); } #[test] -fn callback_returns_cross_in_every_family() { - let (_, h) = render(); - // Strings, bytes, and buffers go back through the out slots as an - // `alloc` run the producer adopts. - let label = block(&h, "static void label("); - assert_contains( - label, - "uint8_t** out_ptr, size_t* out_len, api_error* out_err) {", - ); - assert_contains( - label, - "detail::hand_over(ret.data(), ret.size(), out_ptr, out_len);", - ); - let blob = block(&h, "static void blob("); - assert_contains(blob, "std::vector ret = impl.blob();"); - let latest = block(&h, "static void latest("); - assert_contains(latest, "detail::write_opt_Contact(ret_buf, ret);"); - assert_contains( - latest, - "detail::hand_over(ret_buf.data(), ret_buf.size(), out_ptr, out_len);", - ); - assert_contains(&h, "inline void hand_over("); - assert_contains(&h, "run = api_alloc(len);"); - // An object return hands over a fresh reference; `I?` may be null. - let home = block(&h, "static api_kv_Store* home("); - assert_contains(home, "return impl.home().clone_handle();"); - assert_contains(home, "return nullptr;"); - let spare = block(&h, "static api_kv_Store* spare("); - assert_contains( - spare, - "return ret.has_value() ? ret->clone_handle() : nullptr;", - ); - // A C-style enum converts both ways. - assert_contains( - &h, - "return static_cast(static_cast(impl.pick(static_cast(static_cast(color)))));", - ); - assert_contains(&h, "virtual std::optional latest() = 0;"); - assert_contains(&h, "virtual Store home() = 0;"); +fn keywords_and_wrapper_members_are_escaped() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: class + structs: + - name: Slot + fields: [{ name: default, type: i32 }, { name: char8_t, type: "i32?" }] + interfaces: + - name: Box + methods: + - { name: handle, params: [], return: i32 } + - { name: traits, params: [], return: i32 } + - { name: delete, params: [{ name: register, type: i32 }], return: bool } + functions: + - { name: new, params: [{ name: namespace, type: string }], return: Slot } +"#, + ); + let h = header(&model); + assert_contains(&h, "namespace class_ {"); + assert_contains(&h, "int32_t default_{};"); + assert_contains(&h, "std::optional char8_t_{};"); + assert_contains(&h, "int32_t handle_() const;"); + assert_contains(&h, "int32_t traits_() const;"); + assert_contains(&h, "bool delete_(int32_t register_) const;"); + assert_contains(&h, "inline Slot new_(std::string_view namespace_) {"); } #[test] -fn only_throwing_callback_methods_report_the_domain() { - let (_, h) = render(); - let label = block(&h, "static void label("); - assert_contains(label, "} catch (const KvError& e) {"); - assert_contains(label, "detail::report_kv_error(e, out_err);"); - let blob = block(&h, "static void blob("); - assert!(!blob.contains("KvError"), "{blob}"); - assert_contains(blob, "api_error_set(out_err, -4, e.what());"); - - // A declared code goes back with its fields as the payload; anything - // else is a callback failure. - let report = block(&h, "inline void report_kv_error("); - assert_contains( - report, - "case 1: api_error_set(out_err, 1, e.what()); return;", - ); +fn locals_dodge_parameter_names() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + structs: + - name: R + fields: [{ name: x, type: i32 }] + functions: + - name: f + params: + - { name: err, type: i32 } + - { name: result, type: R } + - { name: result_buf, type: i32 } + return: i64 +"#, + ); + let h = header(&model); + assert_contains(&h, "const auto result_buf_ = detail::encode(result);"); + assert_contains(&h, "kv_error err_{};"); assert_contains( - report, - "const auto* typed = dynamic_cast(&e);", - ); - assert_contains(report, "payload.write_i64(typed->limit);"); - assert_contains( - report, - "api_error_set_payload(out_err, payload.data(), payload.size());", + &h, + "auto result_ = kv_m_f(err, result_buf_.data(), result_buf_.size(), result_buf, &err_);", ); - assert!(report - .trim_end() - .ends_with("api_error_set(out_err, -4, e.what());\n}")); + assert_contains(&h, "detail::check(err_);"); } #[test] -fn optional_callbacks_pass_a_null_vtable() { - let (_, h) = render(); - let maybe = block(&h, "inline void maybe_subscribe("); - assert_contains(maybe, "(std::shared_ptr listener) {"); +fn doc_identifiers_and_deprecations_use_cpp_spellings() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + errors: + - name: StoreErrors + codes: [{ name: NOT_FOUND, code: 1, message: missing }] + functions: + - name: legacy + doc: "Fails with `StoreErrors` (`NOT_FOUND`) unlike `newOne`." + params: [] + return: i32 + throws: StoreErrors + deprecated: "Use `newOne` instead" + - { name: newOne, params: [], return: i32 } +"#, + ); + let h = header(&model); assert_contains( - maybe, - "const auto* listener_vtable = listener ? &detail::Listener_vtable() : nullptr;", - ); - assert_contains( - maybe, - "api_kv_maybe_subscribe(static_cast(listener_ctx.release()), listener_vtable, &err);", - ); - // A required one refuses an empty pointer before the call. - let required = block(&h, "inline void subscribe("); - assert_contains( - required, - "if (!listener) throw std::invalid_argument(\"listener: null callback interface\");", + &h, + "Fails with `StoreError` (`NotFoundError`) unlike `new_one`.", ); + assert_contains(&h, "[[deprecated(\"Use `new_one` instead\")]]"); + assert_contains(&h, "inline int32_t new_one() {"); } #[test] -fn calls_without_errors_trap_and_throwing_calls_use_the_domain() { - let (_, h) = render(); - assert_contains(block(&h, "inline uint32_t count("), "detail::check(err);"); - assert_contains(block(&h, "inline Contact fetch("), "detail::check_kv(err);"); - assert_contains( - block(&h, "inline void Store::save("), - "detail::check_kv(err);", - ); - assert_contains(&h, "class InternalError : public std::runtime_error {"); - assert_contains( - block(&h, "inline void check(api_error& err) {"), - "make_internal_error(err.code, error_message(err));", - ); - // A throwing call's runtime codes are the root Error, and -5 cancels. - let make = block(&h, "inline std::exception_ptr make_kv_error("); +fn error_fields_never_hide_the_exception_members() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + errors: + - name: Oops + codes: + - name: Failed + code: 1 + message: failed + fields: + - { name: message, type: string } + - { name: code, type: i32 } + - { name: what, type: string } + functions: + - { name: f, params: [], return: i32, throws: Oops } +"#, + ); + let h = header(&model); + assert_contains(&h, "std::string message;"); + assert_contains(&h, "int32_t code_;"); + assert_contains(&h, "std::string what_;"); assert_contains( - make, - "if (code == -5) return std::make_exception_ptr(Cancelled(message));", + &h, + "FailedError(const std::string& message_, std::string message, int32_t code_, std::string what_) \ + : OopsError(1, message_), message(std::move(message)), code_(code_), what_(std::move(what_)) {}", ); assert_contains( - make, - "if (code < 0) return std::make_exception_ptr(Error(code, message));", - ); -} - -#[test] -fn each_composite_has_one_codec() { - let (_, h) = render(); - for def in [ - "inline std::vector read_list_Store(BufferReader& r) {", - "inline void write_list_Store(BufferWriter& w, const std::vector& v) {", - "inline std::optional read_opt_Store(BufferReader& r) {", - "inline std::optional read_opt_Contact(BufferReader& r) {", - ] { - assert_eq!(h.matches(def).count(), 1, "{def}"); - } - // Call sites and record codecs delegate instead of looping inline. - assert_contains( - block(&h, "inline Store first("), - "detail::write_list_Store(stores_buf, stores);", + &h, + "report_with_fields(out_err, e, e.message, e.code_, e.what_);", ); - let record = block(&h, "inline Contact read_Contact(BufferReader& r) {"); - assert_contains(record, "detail::read_list_Store(r),"); - assert!(!record.contains("for ("), "{record}"); -} - -#[test] -fn trampolines_adopt_objects_before_anything_can_throw() { - let (_, h) = render(); - let on_message = block(&h, "static bool on_message("); - let adopt = on_message - .find("Store store_arg(adopt, store);") - .expect("adopts the object argument"); - let attempt = on_message.find("try {").expect("try"); - assert!(adopt < attempt, "{on_message}"); - let on_reset = block(&h, "static void on_reset("); - assert_contains(on_reset, "if (alt != nullptr) alt_arg.emplace(adopt, alt);"); } #[test] -fn identity_names_every_file_and_config_overrides_win() { - let model = - validate(&fixture(), &Identity::named("kv-store"), None).expect("fixture validates"); - let out = Utf8Path::new("/out"); - let files = CppGenerator.files(&model, out, &CppConfig::default()); - let paths: Vec = files.iter().map(|f| f.path.to_string()).collect(); - assert_eq!( - paths, - [ - "/out/cpp/kv_store.h", - "/out/cpp/kv_store.hpp", - "/out/cpp/CMakeLists.txt", - "/out/cpp/README.md", - ] - ); - let hpp = &files[1].contents; - assert_contains(hpp, "#include \"kv_store.h\""); - assert_contains(hpp, "namespace kv_store {"); - assert_contains(hpp, "return \"kv_store: \" + why;"); - - let config = CppConfig { - name: Some("acme::kv".into()), - header_name: Some("acme_kv.hpp".into()), - ..CppConfig::default() - }; - let files = CppGenerator.files(&model, out, &config); - assert_eq!(files[1].path, "/out/cpp/acme_kv.hpp"); - assert_contains(&files[1].contents, "namespace acme::kv {"); - assert_contains(&files[1].contents, "#include \"kv_store.h\""); +fn only_value_initializable_members_get_default_initializers() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: m + interfaces: + - name: Token + methods: [{ name: id, params: [], return: i64 }] + structs: + - name: Holder + fields: [{ name: token, type: Token }, { name: spare, type: "Token?" }] + - name: Outer + fields: [{ name: holder, type: Holder }, { name: many, type: "[Holder]" }, { name: n, type: u8 }] + functions: + - { name: f, params: [{ name: o, type: Outer }], return: i32 } +"#, + ); + let h = header(&model); + assert_contains(&h, " Token token;\n"); + assert_contains(&h, " std::optional spare{};\n"); + assert_contains(&h, " Holder holder;\n"); + assert_contains(&h, " std::vector many{};\n"); + assert_contains(&h, " uint8_t n{};\n"); } diff --git a/crates/weaveffi-cli/src/targets/cpp/types.rs b/crates/weaveffi-cli/src/targets/cpp/types.rs index e6908148..c8eca696 100644 --- a/crates/weaveffi-cli/src/targets/cpp/types.rs +++ b/crates/weaveffi-cli/src/targets/cpp/types.rs @@ -1,20 +1,93 @@ -//! C++ spellings: identifier escaping, name casing, namespace paths, and the -//! IR-to-C++ type mapping. +//! C++ spellings: identifier escaping, name casing, namespace paths, the +//! IR-to-C++ type mapping, the exception a call throws, and the doc-text +//! spelling of backticked API identifiers. use crate::cabi::c_param_name; +use crate::codegen::docs::{ApiNames, IdentKind}; use crate::lang::{self, CPP_KEYWORDS}; use heck::ToSnakeCase; -use weaveffi_model::abi::AbiParam; +use weaveffi_model::abi::{AbiParam, CType}; use weaveffi_model::errors; -use weaveffi_model::model::ModuleBinding; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::model::{Model, ModuleBinding}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy}; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty}; -/// Idiomatic C++ exception class name for an error code: PascalCase with a -/// single `Error` suffix (`KEY_NOT_FOUND` becomes `KeyNotFoundError`). +/// What every renderer needs: the model, its C prefix, and the identifier +/// index the doc rewriting consults. +pub(crate) struct Ctx<'m> { + /// The validated model. + pub(crate) model: &'m Model, + /// The C symbol prefix (`kvstore`). + pub(crate) prefix: &'m str, + names: ApiNames, +} + +impl<'m> Ctx<'m> { + /// Index `model` once for a render. + pub(crate) fn new(model: &'m Model) -> Self { + Self { + model, + prefix: model.prefix(), + names: ApiNames::new(model), + } + } + + /// The C++ spelling of a backticked identifier in IDL doc or + /// deprecation text, or `None` to keep it as written: callables, + /// parameters, and fields in their C++ casing, and error domains and + /// codes as their exception classes. + pub(crate) fn spell(&self, ident: &str) -> Option { + match self.names.kind(ident)? { + IdentKind::Function | IdentKind::CallbackMethod => Some(cpp_fn_name(ident)), + IdentKind::Member => Some(cpp_member_name(ident)), + IdentKind::Param | IdentKind::Field => Some(cpp_ident(ident)), + IdentKind::ErrorDomain | IdentKind::ErrorCode => Some(cpp_error_class(ident)), + IdentKind::Module | IdentKind::Type | IdentKind::Variant => None, + } + } +} + +/// Whether a member of type `ty` can be value-initialized (`T x{}`): every +/// type but an interface wrapper (which has no empty state), and a record or +/// rich enum whose by-value members (a rich enum's first variant's) all +/// can. Optionals, vectors, and maps are empty by default whatever they +/// hold. +pub(crate) fn defaultable(model: &Model, ty: &Ty) -> bool { + match ty { + Ty::Interface(_) => false, + Ty::Record(name) => model + .record(name) + .fields + .iter() + .all(|f| defaultable(model, &f.ty)), + Ty::RichEnum(name) => model + .enumeration(name) + .variants + .first() + .is_none_or(|v| v.fields.iter().all(|f| defaultable(model, &f.ty))), + Ty::Prim(_) | Ty::Enum(_) | Ty::Optional(_) | Ty::List(_) | Ty::Map(..) => true, + } +} + +/// The C++ exception class of an error domain or code: PascalCase with a +/// single `Error` suffix (`KitchenErrors` is `KitchenError`, `NOT_FOUND` is +/// `NotFoundError`), through the shared [`errors::type_name`]. pub(crate) fn cpp_error_class(name: &str) -> String { errors::type_name(name, "Error") } +/// The exception class a call with `error` throws, which keys its +/// `detail::Errors` policy: `InternalError` for a call that declares no +/// errors (the trap policy), `Error` for `throws: any`, and the domain's +/// class otherwise. +pub(crate) fn error_class(error: &ErrorStrategy) -> String { + match error { + ErrorStrategy::Trap => "InternalError".to_string(), + ErrorStrategy::Untyped => "Error".to_string(), + ErrorStrategy::Domain(name) => cpp_error_class(name), + } +} + /// C++ reserved words the shared [`CPP_KEYWORDS`] table doesn't carry: the /// alternative operator tokens spelled with `_eq`, the extended character /// types, the two remaining cast keywords, and `thread_local`. Sorted for @@ -49,16 +122,16 @@ pub(crate) fn cpp_fn_name(name: &str) -> String { cpp_ident(&name.to_snake_case()) } -/// Members every interface wrapper class declares besides its special -/// members: the `raw_type` alias and the `handle()` and `clone_handle()` -/// readers. (The private `raw_` field can't collide: a snake-cased name -/// only ends in `_` once escaped, and `raw` is reserved nowhere.) -const OBJECT_MEMBERS: &[&str] = &["clone_handle", "handle", "raw_type"]; +/// Names every interface wrapper class declares besides its members: the +/// `raw_type` alias, the `handle()` and `clone_handle()` readers, and the +/// private `traits` struct. (The private `raw_` field can't collide: a +/// snake-cased name only ends in `_` once escaped, and `raw` is reserved +/// nowhere.) +const OBJECT_MEMBERS: &[&str] = &["clone_handle", "handle", "raw_type", "traits"]; /// The C++ spelling of an interface member (a factory constructor, method, /// or static): [`cpp_fn_name`], with a trailing underscore when it would -/// collide with a member the wrapper class declares (`handle` is -/// `handle_`). +/// collide with a name the wrapper class declares (`handle` is `handle_`). pub(crate) fn cpp_member_name(name: &str) -> String { lang::escape_member(&cpp_fn_name(name), OBJECT_MEMBERS) } @@ -82,92 +155,100 @@ pub(crate) fn slot_name(p: &AbiParam) -> String { c_param_name(&p.name) } -/// Renders ABI parameter slots to C declarations (` `), the form -/// used inside async completion lambdas and callback-interface trampolines. -pub(crate) fn render_param_decls(params: &[AbiParam], prefix: &str) -> Vec { - params - .iter() - .map(|p| format!("{} {}", p.ty.render_c(prefix), slot_name(p))) - .collect() +/// One ABI slot as a C declaration (` `), the form trampoline +/// and completion parameter lists use. +pub(crate) fn slot_decl(p: &AbiParam, prefix: &str) -> String { + format!("{} {}", p.ty.render_c(prefix), slot_name(p)) } -/// The `detail` accessor returning the process-wide static vtable for a -/// callback interface. -pub(crate) fn vtable_accessor(name: &str) -> String { - format!("{name}_vtable") +/// The C type an out slot (`T* out_value`, `T** out_item`) points to: the +/// type of the local the wrapper passes the address of. +pub(crate) fn pointee(p: &AbiParam, prefix: &str) -> String { + match &p.ty { + CType::Ptr { pointee, .. } => pointee.render_c(prefix), + other => other.render_c(prefix), + } } -/// The `detail` struct holding a callback interface's trampolines. -pub(crate) fn trampoline_struct(name: &str) -> String { - format!("{name}_trampolines") +/// The C++ spelling of a primitive. +fn cpp_prim(p: Prim) -> &'static str { + match p { + Prim::I8 => "int8_t", + Prim::I16 => "int16_t", + Prim::I32 => "int32_t", + Prim::I64 => "int64_t", + Prim::U8 => "uint8_t", + Prim::U16 => "uint16_t", + Prim::U32 => "uint32_t", + Prim::U64 => "uint64_t", + Prim::F32 => "float", + Prim::F64 => "double", + Prim::Bool => "bool", + Prim::String => "std::string", + Prim::Bytes => "std::vector", + } } -/// The idiomatic C++ spelling of an IR type. Interfaces map to their RAII -/// wrapper class and `Interface?` to `std::optional` of it; a callback -/// interface (bare or optional) is a `std::shared_ptr` of the abstract class -/// the consumer implements, empty for none. +/// The idiomatic C++ spelling of a value type. Interfaces map to their RAII +/// wrapper class and `Interface?` to `std::optional` of it. pub(crate) fn cpp_type(ty: &Ty) -> String { match ty { - Ty::Prim(p) => match p { - Prim::I8 => "int8_t", - Prim::I16 => "int16_t", - Prim::I32 => "int32_t", - Prim::I64 => "int64_t", - Prim::U8 => "uint8_t", - Prim::U16 => "uint16_t", - Prim::U32 => "uint32_t", - Prim::U64 => "uint64_t", - Prim::F32 => "float", - Prim::F64 => "double", - Prim::Bool => "bool", - Prim::String => "std::string", - Prim::Bytes => "std::vector", - } - .to_string(), + Ty::Prim(p) => cpp_prim(*p).to_string(), Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => n.clone(), - Ty::CallbackInterface(n) => format!("std::shared_ptr<{n}>"), - Ty::Optional(inner) if matches!(inner.as_ref(), Ty::CallbackInterface(_)) => { - cpp_type(inner) - } Ty::Optional(inner) => format!("std::optional<{}>", cpp_type(inner)), Ty::List(inner) => format!("std::vector<{}>", cpp_type(inner)), Ty::Map(k, v) => format!("std::unordered_map<{}, {}>", cpp_type(k), cpp_type(v)), - Ty::Iterator(_) => unreachable!("iterator returns render as range classes"), } } -/// One C++ parameter declaration (` `) for a wrapper signature. -/// Strings borrow as `std::string_view` (the ABI passes a pointer and a -/// length, so interior NULs survive); heavier types borrow by const -/// reference; scalars and enums pass by value. A callback interface is a -/// `std::shared_ptr` by value, which the wrapper moves into the box it hands -/// the producer as `ctx`. -pub(crate) fn cpp_param_decl(ty: &Ty, name: &str) -> String { - match ty { - Ty::Prim(Prim::String) => format!("std::string_view {name}"), - Ty::Prim(Prim::Bytes) - | Ty::Record(_) - | Ty::RichEnum(_) - | Ty::Interface(_) - | Ty::List(_) - | Ty::Map(_, _) => format!("const {}& {name}", cpp_type(ty)), - Ty::Optional(inner) if !matches!(inner.as_ref(), Ty::CallbackInterface(_)) => { - format!("const {}& {name}", cpp_type(ty)) - } - _ => format!("{} {name}", cpp_type(ty)), +/// The C++ type a callable returns synchronously: the mapped value type, or +/// the lazy `Range` of an iterator. +pub(crate) fn cpp_ret_type(ret: &RetTy) -> String { + match ret { + RetTy::Value(ty) => cpp_type(ty), + RetTy::Iterator(elem) => format!("Range<{}>", cpp_type(elem)), + } +} + +/// One C++ parameter declaration (` `) for a wrapper signature, +/// per how the argument crosses: scalars, enums, and optional scalars by +/// value; strings as `std::string_view` (the ABI passes a pointer and a +/// length, so interior NULs survive); typed arrays, bytes, buffered values, +/// and objects by const reference (the array's storage is passed as is); a +/// callback interface as a `std::shared_ptr` by value, which the wrapper +/// moves into the box it hands the producer. +pub(crate) fn cpp_param_decl(ty: &ParamTy, pass: &ArgPass, name: &str) -> String { + match (ty, pass) { + (ParamTy::Callback { name: iface, .. }, _) => format!("std::shared_ptr<{iface}> {name}"), + (ParamTy::Value(ty), pass) => cpp_value_param_decl(ty, pass, name), + } +} + +/// [`cpp_param_decl`] for a value-typed argument, shared with callback +/// methods' parameters except for objects (see [`cpp_cb_param_decl`]). +fn cpp_value_param_decl(ty: &Ty, pass: &ArgPass, name: &str) -> String { + let cpp = cpp_type(ty); + match pass { + ArgPass::Direct { .. } | ArgPass::OptDirect { .. } => format!("{cpp} {name}"), + ArgPass::String { .. } => format!("std::string_view {name}"), + ArgPass::Slice { .. } + | ArgPass::Bytes { .. } + | ArgPass::Buffer { .. } + | ArgPass::Object { .. } + | ArgPass::Callback { .. } => format!("const {cpp}& {name}"), } } -/// One C++ parameter declaration for a callback-interface method the consumer -/// implements. Strings arrive as a `std::string_view` valid for the call; -/// bytes and buffered values by const reference to the trampoline's decoded -/// copy; objects transfer one strong reference, so they arrive by value as -/// the wrapper (or `std::optional` of it) the implementation now owns. -pub(crate) fn cpp_cb_param_decl(ty: &Ty, name: &str) -> String { - if ty.interface_name().is_some() { - format!("{} {name}", cpp_type(ty)) - } else { - cpp_param_decl(ty, name) +/// One C++ parameter declaration for a callback-interface method the +/// consumer implements. Strings arrive as a `std::string_view` valid for the +/// call; typed arrays, bytes, and buffered values by const reference to the +/// trampoline's copy; objects transfer one strong reference, so they arrive +/// by value as the wrapper (or `std::optional` of it) the implementation +/// now owns. +pub(crate) fn cpp_cb_param_decl(ty: &Ty, pass: &ArgPass, name: &str) -> String { + match pass { + ArgPass::Object { .. } => format!("{} {name}", cpp_type(ty)), + _ => cpp_value_param_decl(ty, pass, name), } } @@ -180,7 +261,21 @@ mod tests { assert_eq!(cpp_member_name("handle"), "handle_"); assert_eq!(cpp_member_name("clone_handle"), "clone_handle_"); assert_eq!(cpp_member_name("raw_type"), "raw_type_"); + assert_eq!(cpp_member_name("traits"), "traits_"); assert_eq!(cpp_member_name("delete"), "delete_"); assert_eq!(cpp_member_name("close"), "close"); } + + #[test] + fn error_classes_never_double_the_suffix() { + assert_eq!(cpp_error_class("KitchenErrors"), "KitchenError"); + assert_eq!(cpp_error_class("KvError"), "KvError"); + assert_eq!(cpp_error_class("NOT_FOUND"), "NotFoundError"); + assert_eq!(error_class(&ErrorStrategy::Trap), "InternalError"); + assert_eq!(error_class(&ErrorStrategy::Untyped), "Error"); + assert_eq!( + error_class(&ErrorStrategy::Domain("PantryError".into())), + "PantryError" + ); + } } diff --git a/crates/weaveffi-cli/src/targets/dart/callbacks.rs b/crates/weaveffi-cli/src/targets/dart/callbacks.rs index b86a6caf..f202c073 100644 --- a/crates/weaveffi-cli/src/targets/dart/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/dart/callbacks.rs @@ -3,29 +3,36 @@ //! then one entry per method), the vtable entries, the dispatcher that //! delivers forwarded void methods, and the vtable itself. //! -//! A method that returns a value is a `NativeCallable.isolateLocal` -//! trampoline: it runs synchronously on the isolate's thread, receives its -//! borrowed arguments in place, adopts object arguments, and hands its return -//! to the producer (by value, as a fresh object reference, or as a -//! `{prefix}_alloc` run in the out slots). A thrown exception never unwinds: -//! a `throws` method reports its domain error's code and payload, and -//! anything else is reported as `-4`. A void method is a -//! `NativeCallable.isolateGroupBound` forwarder: it may run on any thread, -//! copies its arguments into a message for the isolate, and returns at once; -//! the dispatcher then runs the implementation on the event loop. +//! A method that returns a value (its [`CallbackRetPass`] isn't `Void`) is a +//! `NativeCallable.isolateLocal` trampoline: it runs synchronously on the +//! isolate's thread, receives its borrowed arguments in place, adopts object +//! arguments, and hands its return to the producer (by value, as a presence +//! flag and an out value, as a fresh object reference, or as a +//! `{prefix}_alloc` run in the out slots). The vtable is flagged +//! thread-affine, so the producer refuses to call such a method from any +//! other thread (failing it with -4) instead of letting the VM abort. A +//! thrown exception never unwinds: a method that throws a domain reports +//! its code and payload, and anything else is reported with a runtime code. +//! A void method is a `NativeCallable.isolateGroupBound` forwarder: it may +//! run on any thread, copies its arguments into a message for the isolate, +//! and returns at once; the dispatcher then runs the implementation on the +//! event loop. use crate::codegen::CodeWriter; use heck::ToUpperCamelCase; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding}; -use weaveffi_model::plan::{ArgPass, RetPass}; +use weaveffi_model::model::{ + CallbackInterfaceBinding, CallbackMethodBinding, CallbackParamBinding, Model, +}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; use weaveffi_model::ty::{Prim, Ty}; -use crate::targets::dart::calls::adopt_expr; +use crate::targets::dart::calls::{receive, Arrival, Throws}; use crate::targets::dart::codec::{pack_fn, unpack_fn}; -use crate::targets::dart::docs::{write_deprecated, write_doc}; +use crate::targets::dart::docs::Docs; use crate::targets::dart::entities::payload_fn; use crate::targets::dart::types::{ - dart_class, dart_ident, dart_type, ffi_type, ffi_typedef, object_class, zero_literal, + dart_class, dart_ident, dart_in_type, dart_type, ffi_type, ffi_typedef, object_class, slice_fn, + typed_data_kind, zero_literal, }; /// The private global holding a callback interface's vtable. @@ -55,19 +62,26 @@ fn lower_first(s: &str) -> String { }) } -/// Render one callback interface; `exception` is the domain exception -/// class in scope for its module, which its `throws` methods report. +/// Whether a method returns a value to the producer (a non-void C return or +/// a return out slot), which makes it a synchronous, thread-affine +/// trampoline. +pub(crate) fn returns_value(m: &CallbackMethodBinding) -> bool { + !matches!(m.ret_pass, CallbackRetPass::Void) +} + +/// Render one callback interface. pub(crate) fn render_callback_interface( w: &mut CodeWriter, + model: &Model, + docs: &Docs, cb: &CallbackInterfaceBinding, - exception: Option<&str>, ) { let class = dart_class(&cb.name); - render_abstract_class(w, cb, &class, exception); + render_abstract_class(w, model, docs, cb, &class); render_vtable_struct(w, cb, &class); for m in &cb.methods { - if m.ret.is_some() { - render_trampoline(w, cb, m, &class, exception.filter(|_| m.throws)); + if returns_value(m) { + render_trampoline(w, model, cb, m, &class); } else { render_forwarder(w, cb, m, &class); } @@ -79,52 +93,70 @@ pub(crate) fn render_callback_interface( /// The class a consumer extends or implements. fn render_abstract_class( w: &mut CodeWriter, + model: &Model, + docs: &Docs, cb: &CallbackInterfaceBinding, class: &str, - exception: Option<&str>, ) { w.blank(); - write_doc(w, &cb.doc); + docs.write(w, &cb.doc); if cb.doc.is_some() { w.line("///"); } - w.line("/// A callback interface: implement it and pass an instance to any"); - w.line(format!( - "/// function taking a [{class}]. The library keeps the instance alive" - )); - w.line("/// until the producer releases it."); - w.line("///"); - w.line("/// Methods that return a value run synchronously and must be called by"); - w.line("/// the producer on this isolate's thread during a call from Dart; a"); - w.line("/// thrown exception fails that call with [NativeException.foreignCode]"); - w.line("/// (or, from a method that throws, the domain exception's code and"); - w.line("/// fields). Void methods may be called from any thread: they're"); - w.line("/// delivered asynchronously on this isolate's event loop, in the zone"); - w.line("/// that passed the instance, where a thrown exception is an uncaught"); - w.line("/// error of that zone. Object arguments are owned by the"); - w.line("/// implementation."); - write_deprecated(w, &cb.deprecated); + for line in [ + "A callback interface: implement it and pass an instance to any", + &format!("function taking a [{class}]. The library keeps the instance alive"), + "until the producer releases it.", + "", + "Methods that return a value run synchronously on this isolate's", + "thread, during the call from Dart that led the producer to call them.", + "The producer refuses to call one from any other thread: the method", + "doesn't run, and the producer sees it fail with", + "[NativeException.foreignCode]. An exception a method throws fails the", + "producer's call (see each method). Void methods may be called from any", + "thread: they're delivered asynchronously on this isolate's event loop,", + "in the zone that passed the instance, where a thrown exception is an", + "uncaught error of that zone. Object arguments are owned by the", + "implementation.", + ] { + w.line(if line.is_empty() { + "///".to_string() + } else { + format!("/// {line}") + }); + } + docs.write_deprecated(w, &cb.deprecated); w.block(format!("abstract class {class} {{"), "}", |w| { for (i, m) in cb.methods.iter().enumerate() { if i > 0 { w.blank(); } - write_doc(w, &m.doc); - if let (true, Some(exc), Some(_)) = (m.throws, exception, &m.ret) { + docs.write(w, &m.doc); + if returns_value(m) { + let note = match Throws::of(model, &m.error) { + Throws::Domain(exc) => format!( + "Throw a [{exc}] to fail the producer's call with its code and fields." + ), + Throws::Untyped => { + "Throw any exception to fail the producer's call with its message.".into() + } + Throws::Trap => { + "An exception fails the producer's call with [NativeException.foreignCode]." + .into() + } + }; if m.doc.is_some() { w.line("///"); } - w.line(format!( - "/// Throw a [{exc}] to fail the producer's call with it." - )); + w.line(format!("/// {note}")); } - write_deprecated(w, &m.deprecated); + docs.write_deprecated(w, &m.deprecated); let params: Vec = m .params .iter() .map(|p| format!("{} {}", dart_type(&p.ty), dart_ident(&p.name))) .collect(); - let ret = m.ret.as_ref().map_or("void".to_string(), dart_type); + let ret = m.ret.as_ref().map_or("void".to_string(), dart_in_type); w.line(format!( "{ret} {}({});", dart_ident(&m.name), @@ -144,11 +176,11 @@ fn entry_typedef(cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding) -> St fn render_vtable_struct(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) { w.blank(); for m in &cb.methods { - let natives: Vec = m.abi_params.iter().map(|p| ffi_type(&p.ty).0).collect(); + let natives: Vec = m.abi.params.iter().map(|p| ffi_type(&p.ty).0).collect(); w.line(format!( "typedef {} = {} Function({});", entry_typedef(cb, m), - ffi_type(&m.abi_ret).0, + ffi_type(&m.abi.ret).0, natives.join(", ") )); } @@ -174,41 +206,84 @@ fn render_vtable_struct(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class ); } -/// The Dart parameter list of one vtable entry. +/// The Dart parameter list of one vtable entry, named after its C slots. fn entry_params(m: &CallbackMethodBinding) -> String { - m.abi_params + m.abi + .params .iter() .map(|p| format!("{} {}", ffi_type(&p.ty).1, dart_ident(&p.name))) .collect::>() .join(", ") } -/// The Dart name of one of an entry's trailing slots (`out_ptr`, `out_len`, -/// `out_err`), counted from the end. -fn trailing_slot(m: &CallbackMethodBinding, from_end: usize) -> String { - dart_ident(&m.abi_params[m.abi_params.len() - from_end].name) +/// The Dart name of the entry's `out_err` slot (always last). +fn out_err(m: &CallbackMethodBinding) -> String { + dart_ident(&m.abi.params.last().expect("out_err").name) +} + +/// How a borrowed callback argument arrives, per its [`ArgPass`]. +fn arrival(p: &CallbackParamBinding) -> Arrival { + let n = |s: &weaveffi_model::abi::AbiParam| dart_ident(&s.name); + match &p.pass { + ArgPass::Direct { slot } => Arrival::Direct(n(slot)), + ArgPass::OptDirect { has, value, .. } => Arrival::OptDirect { + has: n(has), + value: n(value), + }, + ArgPass::Slice { ptr, len, elem } => Arrival::Slice { + ptr: n(ptr), + len: n(len), + elem: *elem, + }, + ArgPass::String { ptr, len } => Arrival::String { + ptr: n(ptr), + len: n(len), + }, + ArgPass::Bytes { ptr, len } => Arrival::Bytes { + ptr: n(ptr), + len: n(len), + }, + ArgPass::Buffer { ptr, len } => Arrival::Buffer { + ptr: n(ptr), + len: n(len), + }, + ArgPass::Object { slot, nullable, .. } => Arrival::Object { + ptr: n(slot), + nullable: *nullable, + }, + ArgPass::Callback { .. } => { + unreachable!("callback methods never take callback interfaces") + } + } +} + +/// Whether an argument is received into a local first: objects (adopted +/// references) and buffers (which may carry object tokens), so each +/// reference is owned by a finalizable wrapper even if a later step throws. +fn owns_references(p: &CallbackParamBinding) -> bool { + matches!(p.pass, ArgPass::Object { .. } | ArgPass::Buffer { .. }) } /// A value-returning method's `isolateLocal` trampoline: adopt object /// arguments first (so a later failure still leaves each reference owned by /// a finalizable wrapper), receive the borrowed ones in place, call the /// implementation, and hand its value to the producer. A thrown exception is -/// reported through `out_err` and the zero value is returned; `exception` is -/// the domain the method reports when it's declared `throws`. +/// reported through `out_err` and the zero value is returned. fn render_trampoline( w: &mut CodeWriter, + model: &Model, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding, class: &str, - exception: Option<&str>, ) { - let ret = m.ret.as_ref().expect("value-returning method"); - let pass = RetPass::of(Some(ret)); - let (_, dart_ret) = ffi_type(&m.abi_ret); - let out_err = trailing_slot(m, 1); - let zero = match &pass { - RetPass::Direct => Some(zero_literal(ret)), - RetPass::Object { .. } => Some("nullptr"), + let throws = &Throws::of(model, &m.error); + let (_, dart_ret) = ffi_type(&m.abi.ret); + let out_err = out_err(m); + let n = |s: &weaveffi_model::abi::AbiParam| dart_ident(&s.name); + let zero = match (&m.ret_pass, &m.ret) { + (CallbackRetPass::Direct, Some(ret)) => Some(zero_literal(ret)), + (CallbackRetPass::OptDirect { .. }, _) => Some("false"), + (CallbackRetPass::Object { .. }, _) => Some("nullptr"), _ => None, }; w.blank(); @@ -223,61 +298,140 @@ fn render_trampoline( |w| { w.line("try {"); w.scope(|w| { - // Objects first: nothing before them can throw. - let is_object = |p: &ParamBinding| matches!(p.arg_pass(), ArgPass::Object { .. }); let mut args = vec![String::new(); m.params.len()]; - for (i, p) in m.params.iter().enumerate().filter(|(_, p)| is_object(p)) { - args[i] = receive_arg(w, p); + let first = m + .params + .iter() + .enumerate() + .filter(|(_, p)| owns_references(p)); + for (i, p) in first { + let local = format!("_{}", dart_ident(&p.name)); + w.line(format!( + "final {local} = {};", + receive(&p.ty, arrival(p), false) + )); + args[i] = local; } - for (i, p) in m.params.iter().enumerate().filter(|(_, p)| !is_object(p)) { - args[i] = receive_arg(w, p); + for (i, p) in m.params.iter().enumerate() { + if !owns_references(p) { + args[i] = receive(&p.ty, arrival(p), false); + } } let call = format!( "(_callbackTarget(ctx) as {class}).{}({})", dart_ident(&m.name), args.join(", ") ); - if matches!(pass, RetPass::Direct) { - let value = if matches!(ret, Ty::Enum(_)) { - ".value" - } else { - "" - }; - w.line(format!("return {call}{value};")); - return; - } - w.line(format!("final result = {call};")); - // The out slots of a string, bytes, or buffer return. - let out = || format!("{}, {}", trailing_slot(m, 3), trailing_slot(m, 2)); - w.line(match &pass { - // A fresh strong reference the producer adopts. - RetPass::Object { nullable: false } => "return result._cloneRef();".into(), - RetPass::Object { nullable: true } => { - "return result?._cloneRef() ?? nullptr;".into() + let enum_value = |ty: Option<&Ty>| match ty { + Some(Ty::Enum(_)) => ".value", + _ => "", + }; + match &m.ret_pass { + CallbackRetPass::Void => unreachable!("void methods are forwarded"), + CallbackRetPass::Direct => { + w.line(format!("return {call}{};", enum_value(m.ret.as_ref()))); + } + CallbackRetPass::OptDirect { out_value } => { + let inner = match &m.ret { + Some(Ty::Optional(inner)) => Some(inner.as_ref()), + _ => None, + }; + w.line(format!("final _result = {call};")); + w.line("if (_result == null) return false;"); + w.line(format!( + "{}.value = _result{};", + n(out_value), + enum_value(inner) + )); + w.line("return true;"); + } + CallbackRetPass::Slice { + out_ptr, + out_len, + elem, + } => { + w.line(format!("final _result = {call};")); + w.line(format!( + "{}(_result, {}, {});", + slice_fn("handOver", *elem), + n(out_ptr), + n(out_len) + )); + } + CallbackRetPass::String { out_ptr, out_len } => { + w.line(format!("final _result = {call};")); + w.line(format!( + "_handOver(utf8.encode(_result), {}, {});", + n(out_ptr), + n(out_len) + )); } - RetPass::String => { - format!("_handOver(utf8.encode(result), {});", out()) + CallbackRetPass::Bytes { out_ptr, out_len } => { + w.line(format!("final _result = {call};")); + w.line(format!( + "_handOver(_result, {}, {});", + n(out_ptr), + n(out_len) + )); } - RetPass::Bytes => format!("_handOver(result, {});", out()), - RetPass::Buffer => { - format!("_handOver(_encode(result, {}), {});", pack_fn(ret), out()) + CallbackRetPass::Buffer { out_ptr, out_len } => { + let ret = m.ret.as_ref().expect("a buffer return"); + w.line(format!("final _result = {call};")); + w.line(format!( + "_handOver(_encode(_result, {}), {}, {});", + pack_fn(ret), + n(out_ptr), + n(out_len) + )); } - RetPass::Void | RetPass::Direct => unreachable!("handled above"), - }); + // A fresh strong reference the producer adopts. + CallbackRetPass::Object { nullable, .. } => { + w.line(format!("final _result = {call};")); + w.line(if *nullable { + "return _result?._cloneRef() ?? nullptr;" + } else { + "return _result._cloneRef();" + }); + } + } }); - if let Some(exc) = exception { - w.line(format!("}} on {exc} catch (e) {{")); - w.scope(|w| { - w.line(format!( - "_failCallback({out_err}, e.code, e.message, {}(e));", - payload_fn(exc) - )); - }); + match throws { + Throws::Domain(exc) => { + let payload = if domain_has_fields(model, &m.error) { + format!("{}(e)", payload_fn(exc)) + } else { + "null".into() + }; + w.line(format!("}} on {exc} catch (e) {{")); + w.scope(|w| { + w.line(format!( + "_failCallback({out_err}, e.code, e.message, {payload});" + )); + }); + w.line("} catch (e) {"); + w.scope(|w| { + w.line(format!( + "_reportCallbackError({out_err}, NativeException.genericCode, e);" + )); + }); + } + Throws::Untyped => { + w.line("} catch (e) {"); + w.scope(|w| { + w.line(format!( + "_reportCallbackError({out_err}, NativeException.genericCode, e);" + )); + }); + } + Throws::Trap => { + w.line("} catch (e) {"); + w.scope(|w| { + w.line(format!( + "_reportCallbackError({out_err}, NativeException.foreignCode, e);" + )); + }); + } } - w.line("} catch (e) {"); - w.scope(|w| { - w.line(format!("_foreignError({out_err}, e);")); - }); w.line("}"); if let Some(zero) = zero { w.line(format!("return {zero};")); @@ -286,38 +440,36 @@ fn render_trampoline( ); } -/// The expression receiving one parameter inside a trampoline: borrowed runs -/// are copied, buffers decoded, and objects adopted. Objects and buffers -/// (which may carry object tokens) are received into locals declared first, -/// so each reference is owned even if a later step throws. -fn receive_arg(w: &mut CodeWriter, p: &ParamBinding) -> String { - let slot = dart_ident(&p.abi[0].name); - match p.arg_pass() { - ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(n) => format!("{}.fromValue({slot})", dart_class(n)), - _ => slot, +/// The `_CallbackMessage` cascade step posting one argument of a forwarded +/// void method. +fn post_arg(p: &CallbackParamBinding) -> String { + let n = |s: &weaveffi_model::abi::AbiParam| dart_ident(&s.name); + let scalar = |ty: &Ty| match ty { + Ty::Prim(Prim::Bool) => "Bool", + Ty::Prim(Prim::F32 | Prim::F64) => "Float64", + _ => "Int64", + }; + match &p.pass { + ArgPass::Direct { slot } => match scalar(&p.ty) { + "Bool" => format!("..boolean({})", n(slot)), + "Float64" => format!("..float64({})", n(slot)), + _ => format!("..int64({})", n(slot)), }, - ArgPass::String { len, .. } => format!("_readString({slot}, {})", dart_ident(&len.name)), - ArgPass::Bytes { len, .. } => format!("_copyBytes({slot}, {})", dart_ident(&len.name)), - ArgPass::Buffer { len, .. } => { - let decode = format!( - "_decode(_copyBytes({slot}, {}), {})", - dart_ident(&len.name), - unpack_fn(&p.ty) - ); - // Any object tokens inside are adopted references. - let local = format!("_{}", dart_ident(&p.name)); - w.line(format!("final {local} = {decode};")); - local + ArgPass::OptDirect { has, value, inner } => { + format!("..maybe{}({}, {})", scalar(inner), n(has), n(value)) } - ArgPass::Object { nullable, .. } => { - let local = format!("_{}", dart_ident(&p.name)); - w.line(format!( - "final {local} = {};", - adopt_expr(&slot, &p.ty, nullable) - )); - local + ArgPass::Slice { ptr, len, elem } => format!( + "..typedData({}, {}, _CallbackMessage.{})", + n(ptr), + n(len), + typed_data_kind(*elem) + ), + ArgPass::String { ptr, len } + | ArgPass::Bytes { ptr, len } + | ArgPass::Buffer { ptr, len } => { + format!("..bytes({}, {})", n(ptr), n(len)) } + ArgPass::Object { slot, .. } => format!("..pointer({})", n(slot)), ArgPass::Callback { .. } => { unreachable!("callback methods never take callback interfaces") } @@ -325,8 +477,8 @@ fn receive_arg(w: &mut CodeWriter, p: &ParamBinding) -> String { } /// A void method's `isolateGroupBound` forwarder: it runs outside the -/// isolate, so it only copies its slots into a message (borrowed bytes are -/// copied by the post) and touches no Dart globals. +/// isolate, so it only copies its slots into a message (borrowed runs and +/// arrays are copied by the post) and touches no Dart globals. fn render_forwarder( w: &mut CodeWriter, cb: &CallbackInterfaceBinding, @@ -354,25 +506,7 @@ fn render_forwarder( w.scope(|w| { w.scope(|w| { for p in &m.params { - let slot = dart_ident(&p.abi[0].name); - w.line(match p.arg_pass() { - ArgPass::Direct { .. } => match &p.ty { - Ty::Prim(Prim::Bool) => format!("..boolean({slot})"), - Ty::Prim(Prim::F32 | Prim::F64) => { - format!("..float64({slot})") - } - _ => format!("..int64({slot})"), - }, - ArgPass::String { len, .. } - | ArgPass::Bytes { len, .. } - | ArgPass::Buffer { len, .. } => { - format!("..bytes({slot}, {})", dart_ident(&len.name)) - } - ArgPass::Object { .. } => format!("..pointer({slot})"), - ArgPass::Callback { .. } => { - unreachable!("callback methods never take callback interfaces") - } - }); + w.line(post_arg(p)); } w.line("..send();"); }); @@ -392,7 +526,7 @@ fn render_dispatch(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &st ), "}", |w| { - if cb.methods.iter().all(|m| m.ret.is_some()) { + if cb.methods.iter().all(returns_value) { w.line(format!( "// Every {class} method returns a value: nothing is forwarded." )); @@ -401,21 +535,19 @@ fn render_dispatch(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &st w.line(format!("final target = impl as {class};")); w.block("switch (method) {", "}", |w| { for (index, m) in cb.methods.iter().enumerate() { - if m.ret.is_some() { + if returns_value(m) { continue; } // Object arguments are adopted first, so a failure while // decoding the rest still leaves each transferred // reference owned by a finalizable wrapper. w.block(format!("case {index}: {{"), "}", |w| { - let params: Vec<(usize, &ParamBinding)> = - m.params.iter().enumerate().collect(); let is_object = - |p: &ParamBinding| matches!(p.arg_pass(), ArgPass::Object { .. }); - for (i, p) in params.iter().filter(|(_, p)| is_object(p)) { + |p: &CallbackParamBinding| matches!(p.pass, ArgPass::Object { .. }); + for (i, p) in m.params.iter().enumerate().filter(|(_, p)| is_object(p)) { message_arg(w, p, i + 2); } - for (i, p) in params.iter().filter(|(_, p)| !is_object(p)) { + for (i, p) in m.params.iter().enumerate().filter(|(_, p)| !is_object(p)) { message_arg(w, p, i + 2); } let args: Vec = @@ -433,27 +565,40 @@ fn render_dispatch(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &st } /// Decode forwarded argument `i` of `message` into the local `a{i}`. -fn message_arg(w: &mut CodeWriter, p: &ParamBinding, i: usize) { - let field = format!("message[{i}]!"); +fn message_arg(w: &mut CodeWriter, p: &CallbackParamBinding, i: usize) { + let field = format!("message[{i}]"); let local = format!("a{i}"); - let value = match p.arg_pass() { + let value = match &p.pass { ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(n) => format!("{}.fromValue({field} as int)", dart_class(n)), - ty => format!("{field} as {}", dart_type(ty)), + Ty::Enum(n) => format!("{}.fromValue({field}! as int)", dart_class(n)), + ty => format!("{field}! as {}", dart_type(ty)), + }, + ArgPass::OptDirect { inner, .. } => match inner { + Ty::Enum(n) => { + let raw = format!("{local}Value"); + w.line(format!("final {raw} = {field} as int?;")); + format!("{raw} == null ? null : {}.fromValue({raw})", dart_class(n)) + } + other => format!("{field} as {}?", dart_type(other)), }, - ArgPass::String { .. } => format!("utf8.decode({field} as Uint8List)"), - ArgPass::Bytes { .. } => format!("{field} as Uint8List"), + ArgPass::Slice { .. } => format!("{field}! as {}", dart_type(&p.ty)), + ArgPass::String { .. } => format!("utf8.decode({field}! as Uint8List)"), + ArgPass::Bytes { .. } => format!("{field}! as Uint8List"), ArgPass::Buffer { .. } => { - format!("_decode({field} as Uint8List, {})", unpack_fn(&p.ty)) + format!("_decode({field}! as Uint8List, {})", unpack_fn(&p.ty)) } - ArgPass::Object { nullable, .. } => { + ArgPass::Object { + nullable, + interface, + .. + } => { let address = format!("{local}Address"); - w.line(format!("final {address} = {field} as int;")); + w.line(format!("final {address} = {field}! as int;")); let adopt = format!( "{}._(Pointer.fromAddress({address}))", - object_class(&p.ty) + object_class(interface) ); - if nullable { + if *nullable { format!("{address} == 0 ? null : {adopt}") } else { adopt @@ -467,7 +612,9 @@ fn message_arg(w: &mut CodeWriter, p: &ParamBinding, i: usize) { } /// The vtable every instance passed to the producer from this isolate -/// shares; the per-instance state travels in `ctx`. +/// shares; the per-instance state travels in `ctx`. It's flagged +/// thread-affine (DESIGN 1.7): only this isolate's thread may call a +/// value-returning method. fn render_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) { let strukt = vtable_struct(class); w.blank(); @@ -480,18 +627,25 @@ fn render_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) w.scope(|w| { w.scope(|w| { w.line(format!("..size = sizeOf<{strukt}>()")); - w.line("..flags = 0"); - w.line("..free = _callbackFree"); + w.line("..flags = _vtableThreadAffine"); + w.line(if cb.methods.is_empty() { + "..free = _callbackFree;" + } else { + "..free = _callbackFree" + }); for (i, m) in cb.methods.iter().enumerate() { let td = entry_typedef(cb, m); let entry = entry_fn(class, &m.name); - let callable = match RetPass::of(m.ret.as_ref()) { - RetPass::Void => { + let callable = match (&m.ret_pass, &m.ret) { + (CallbackRetPass::Void, _) => { format!("NativeCallable<{td}>.isolateGroupBound({entry})") } - RetPass::Direct => format!( + (CallbackRetPass::Direct, Some(ret)) => format!( "NativeCallable<{td}>.isolateLocal({entry}, exceptionalReturn: {})", - zero_literal(m.ret.as_ref().expect("direct return")) + zero_literal(ret) + ), + (CallbackRetPass::OptDirect { .. }, _) => format!( + "NativeCallable<{td}>.isolateLocal({entry}, exceptionalReturn: false)" ), _ => format!("NativeCallable<{td}>.isolateLocal({entry})"), }; @@ -504,3 +658,37 @@ fn render_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) }, ); } + +/// Whether `error` names a domain with a code that carries fields. +fn domain_has_fields(model: &Model, error: &ErrorStrategy) -> bool { + error.domain().is_some_and(|name| { + model + .error_domain(name) + .codes + .iter() + .any(|c| !c.fields.is_empty()) + }) +} + +/// The error domains a value-returning callback method throws whose codes +/// carry fields, by domain name: each needs an encoder for those fields. +pub(crate) fn reported_domains(model: &Model) -> std::collections::BTreeSet { + model + .callback_interfaces() + .flat_map(|(_, cb)| &cb.methods) + .filter(|m| returns_value(m) && domain_has_fields(model, &m.error)) + .filter_map(|m| m.error.domain().map(str::to_string)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn names_follow_the_class() { + assert_eq!(vtable_var("ready_listener"), "_readyListenerVtable"); + assert_eq!(dispatch_fn("Sampler"), "_samplerDispatch"); + assert_eq!(entry_fn("Sampler", "on_item"), "_samplerOnItem"); + } +} diff --git a/crates/weaveffi-cli/src/targets/dart/calls.rs b/crates/weaveffi-cli/src/targets/dart/calls.rs index c898f980..ff585809 100644 --- a/crates/weaveffi-cli/src/targets/dart/calls.rs +++ b/crates/weaveffi-cli/src/targets/dart/calls.rs @@ -3,60 +3,68 @@ //! //! FFI signatures come straight from the model's lowered [`AbiFn`]s, so they //! match the C header slot for slot. Parameter marshalling dispatches on the -//! shared [`ArgPass`] contract, result handling on [`RetPass`], and error -//! handling on [`ErrorStrategy`]. +//! stored [`ArgPass`], results on [`RetPass`], [`ResultPass`], and +//! [`ItemPass`], and error handling on [`ErrorStrategy`]; nothing here +//! re-derives a transport from a type. +//! +//! Every call runs in its own `_Frame` (the runtime's pooled per-call +//! scratch): its error slot, its return out slots, and the arena its +//! arguments are staged in belong to that call alone, so a call made from a +//! callback during another call never disturbs the outer one. use crate::codegen::CodeWriter; use weaveffi_model::abi::{AbiParam, CType}; -use weaveffi_model::model::{AbiFn, AsyncBinding, CallShape, FnBinding, IteratorBinding}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; -use weaveffi_model::ty::Ty; +use weaveffi_model::model::{AbiFn, AsyncBinding, FnBinding, IteratorBinding, Model}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{Prim, Ty}; use crate::targets::dart::callbacks::{dispatch_fn, vtable_var}; use crate::targets::dart::codec::{pack_fn, unpack_fn}; -use crate::targets::dart::docs::emit_wrapper_doc; +use crate::targets::dart::docs::Docs; use crate::targets::dart::types::{ - dart_class, dart_ident, dart_type, ffi_type, ffi_typedef, ffi_var, object_class, + dart_class, dart_ident, dart_type, exception_class, ffi_type, ffi_typedef, ffi_var, + object_class, param_type, return_type, slice_fn, zero_literal, }; -/// Error-reporting context for one wrapper: the mapper its error checks -/// route through. -/// -/// The split follows [`ErrorStrategy`]: a throwing callable maps codes onto -/// the module's typed domain exceptions, while a non-throwing callable traps -/// (a failure there is a producer bug, raised as a `NativeError`). -#[derive(Clone, Copy)] -pub(crate) struct ErrCtx<'a> { - /// The domain exception class a throwing callable maps codes onto, or - /// `None` for a callable that traps. - exception: Option<&'a str>, +/// How a callable's failures surface, resolved from its [`ErrorStrategy`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Throws { + /// Not declared to throw: a failure is a producer bug, raised as a + /// `NativeError`. + Trap, + /// `throws: Domain`: the domain's exception class (open: an unknown + /// positive code is the base class itself). + Domain(String), + /// `throws: any`: a `NativeException` carrying -1 and the message. + Untyped, } -impl<'a> ErrCtx<'a> { - /// The context of `f`, whose module has the domain exception class - /// `exception` in scope (its own or an ancestor's). - pub(crate) fn of(f: &FnBinding, exception: Option<&'a str>) -> Self { - Self { - exception: exception.filter(|_| f.error_strategy() == ErrorStrategy::Throws), +impl Throws { + /// The surface of `error`, naming a domain's exception class. + pub(crate) fn of(model: &Model, error: &ErrorStrategy) -> Self { + match error { + ErrorStrategy::Trap => Self::Trap, + ErrorStrategy::Domain(name) => { + Self::Domain(exception_class(&model.error_domain(name).name)) + } + ErrorStrategy::Untyped => Self::Untyped, } } - /// The domain exception this wrapper throws, if it throws one. - pub(crate) fn thrown_exception(&self) -> Option<&'a str> { - self.exception - } - - /// The `_ErrorMapper` the wrapper's checks use. - fn mapper(&self) -> String { - self.exception - .map_or_else(|| "_trap".to_string(), mapper_fn) + /// The `_ErrorMapper` a failure goes through. + pub(crate) fn mapper(&self) -> String { + match self { + Self::Trap => "_trap".into(), + Self::Domain(exc) => mapper_fn(exc), + Self::Untyped => "_runtimeException".into(), + } } - /// The statement checking the shared error slot after a call. - fn check_stmt(&self) -> String { - match self.exception { - Some(exc) => format!("_check(_err, {});", mapper_fn(exc)), - None => "_check(_err);".to_string(), + /// The statement checking frame `f`'s error slot after a call. + fn check(&self, f: &str) -> String { + match self { + Self::Trap => format!("{f}.check();"), + other => format!("{f}.check({});", other.mapper()), } } } @@ -112,40 +120,42 @@ impl DartDecl<'_> { /// calls as leaf calls (only sound when the API declares no callback /// interfaces, so no call can re-enter Dart). pub(crate) fn emit_bindings(w: &mut CodeWriter, f: &FnBinding, leaf: bool) { - match &f.shape { - CallShape::Sync(abi) => emit_lookup(w, abi, leaf, None), - CallShape::Async(a) => { - let cb = ffi_typedef(&a.callback_type); - let (natives, _) = slot_types(&a.callback_params, None); - w.blank(); - w.line(format!( - "typedef {cb} = Void Function({});", - natives.join(", ") - )); - emit_lookup(w, &a.launch, false, Some(&cb)); - } - CallShape::Iterator(ib) => { - emit_lookup(w, &ib.launch, false, None); - emit_lookup(w, &ib.next, false, None); - emit_destroy(w, &ib.destroy_symbol, false); - } + if let Some(a) = f.async_binding() { + let cb = ffi_typedef(&a.callback_type); + let (natives, _) = slot_types(&a.callback_params, None); + w.blank(); + w.line(format!( + "typedef {cb} = Void Function({});", + natives.join(", ") + )); + emit_lookup(w, &f.abi, false, Some(&cb)); + } else if let Some(it) = f.iterator() { + emit_lookup(w, &f.abi, false, None); + emit_lookup(w, &it.next, false, None); + emit_destroy(w, &it.destroy_symbol, false); + } else { + emit_lookup(w, &f.abi, leaf, None); } } /// Render `f`'s wrapper, declared as `kind` and named `name`. pub(crate) fn emit_wrapper( w: &mut CodeWriter, + model: &Model, + docs: &Docs, f: &FnBinding, kind: &DartDecl, name: &str, - err: ErrCtx, ) { + let throws = Throws::of(model, &f.error); w.blank(); - emit_wrapper_doc(w, f, err); - match &f.shape { - CallShape::Sync(abi) => render_sync(w, f, abi, kind, name, err), - CallShape::Async(a) => render_async(w, f, a, kind, name, err), - CallShape::Iterator(ib) => render_iterator(w, f, ib, kind, name, err), + docs.write_wrapper(w, f, &throws); + if let Some(a) = f.async_binding() { + render_async(w, f, a, kind, name, &throws); + } else if let Some(it) = f.iterator() { + render_iterator(w, f, it, kind, name, &throws); + } else { + render_sync(w, f, kind, name, &throws); } } @@ -212,9 +222,9 @@ fn wrapper_params(f: &FnBinding) -> String { let mut params: Vec = f .params .iter() - .map(|p| format!("{} {}", dart_type(&p.ty), dart_ident(&p.name))) + .map(|p| format!("{} {}", param_type(&p.ty), dart_ident(&p.name))) .collect(); - if f.cancellable { + if f.cancellable() { params.push(format!("{{CancelToken? {}}}", cancel_param(f))); } params.join(", ") @@ -233,15 +243,24 @@ fn cancel_param(f: &FnBinding) -> &'static str { } } -/// The marshalling of a callable's inputs: staging statements, call -/// arguments, and whether an `Arena` scopes them. +/// The marshalling of a callable's inputs into frame `_f`: staging +/// statements and call arguments. struct Inputs { /// Statements run before the call (staging, borrowing, registering). stage: Vec, /// The call's argument expressions, in ABI order. args: Vec, - /// Whether the statements allocate from (or borrow through) `_arena`. - arena: bool, +} + +impl Inputs { + /// Whether staging allocates from (or borrows through) the frame's + /// arena. + fn uses_arena(&self) -> bool { + self.stage + .iter() + .chain(&self.args) + .any(|s| s.contains("_f.arena")) + } } /// Marshal `f`'s inputs. An instance method's call passes `_self` first. @@ -251,58 +270,67 @@ fn marshal_inputs(f: &FnBinding) -> Inputs { let mut stage = Vec::new(); let mut registrations = Vec::new(); let mut args = Vec::new(); - let mut arena = false; - if f.has_self { + if f.has_self() { args.push("_self".into()); } for p in &f.params { let name = dart_ident(&p.name); - match p.arg_pass() { - ArgPass::Direct { .. } => args.push(match &p.ty { - Ty::Enum(_) => format!("{name}.value"), + let value = p.ty.value(); + match &p.pass { + ArgPass::Direct { .. } => args.push(match value { + Some(Ty::Enum(_)) => format!("{name}.value"), _ => name, }), + ArgPass::OptDirect { inner, .. } => { + args.push(format!("{name} != null")); + args.push(match inner { + Ty::Enum(_) => format!("{name}?.value ?? 0"), + other => format!("{name} ?? {}", zero_literal(other)), + }); + } + ArgPass::Slice { elem, .. } => { + args.push(format!("{}(_f.arena, {name})", slice_fn("stage", *elem))); + args.push(format!("{name}.length")); + } ArgPass::String { .. } => { - arena = true; stage.push(format!("final _{name}Bytes = utf8.encode({name});")); - args.push(format!("_stage(_arena, _{name}Bytes)")); + args.push(format!("_stage(_f.arena, _{name}Bytes)")); args.push(format!("_{name}Bytes.length")); } ArgPass::Bytes { .. } => { - arena = true; - args.push(format!("_stage(_arena, {name})")); + args.push(format!("_stage(_f.arena, {name})")); args.push(format!("{name}.length")); } ArgPass::Buffer { .. } => { - arena = true; + let ty = value.expect("a buffer parameter is a value"); stage.push(format!( "final _{name}Bytes = _encode({name}, {});", - pack_fn(&p.ty) + pack_fn(ty) )); - args.push(format!("_stage(_arena, _{name}Bytes)")); + args.push(format!("_stage(_f.arena, _{name}Bytes)")); args.push(format!("_{name}Bytes.length")); } // The wrapper keeps its reference and is borrowed for the call. ArgPass::Object { nullable, .. } => { - arena = true; let ptr = format!("_{name}Ptr"); - stage.push(if nullable { - format!("final {ptr} = {name} == null ? nullptr : _borrow(_arena, {name});") + stage.push(if *nullable { + format!("final {ptr} = {name} == null ? nullptr : _borrow(_f.arena, {name});") } else { - format!("final {ptr} = _borrow(_arena, {name});") + format!("final {ptr} = _borrow(_f.arena, {name});") }); args.push(ptr); } // The producer owns the registration and releases it through the // vtable's `free`; a null vtable means no implementation. - ArgPass::Callback { nullable, .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback family names a callback interface"); + ArgPass::Callback { + nullable, + interface, + .. + } => { let ctx = format!("_{name}Ctx"); - let register = format!("_registerCallback({name}, {})", dispatch_fn(cb)); - let vtable = format!("{}.cast()", vtable_var(cb)); - if nullable { + let register = format!("_registerCallback({name}, {})", dispatch_fn(interface)); + let vtable = format!("{}.cast()", vtable_var(interface)); + if *nullable { registrations.push(format!( "final {ctx} = {name} == null ? nullptr : {register};" )); @@ -317,13 +345,107 @@ fn marshal_inputs(f: &FnBinding) -> Inputs { } } stage.extend(registrations); - Inputs { stage, args, arena } + Inputs { stage, args } +} + +/// How one value arrives from the producer: the expressions holding its +/// slots, per its transport. +pub(crate) enum Arrival { + /// A scalar (or C-style enum discriminant). + Direct(String), + /// A presence flag and a scalar (ignored when absent). + OptDirect { + /// The presence flag. + has: String, + /// The scalar. + value: String, + }, + /// A typed array and its element count. + Slice { + /// The array pointer. + ptr: String, + /// The element count. + len: String, + /// The element type. + elem: Prim, + }, + /// A UTF-8 run. + String { + /// The run pointer. + ptr: String, + /// The byte length. + len: String, + }, + /// A byte run. + Bytes { + /// The run pointer. + ptr: String, + /// The byte length. + len: String, + }, + /// A value buffer. + Buffer { + /// The run pointer. + ptr: String, + /// The byte length. + len: String, + }, + /// An object reference (always owned by the receiver). + Object { + /// The object pointer. + ptr: String, + /// Whether a null pointer means no object. + nullable: bool, + }, +} + +/// The Dart expression turning an arriving value of type `ty` into its +/// surface value. `owned` runs and arrays are copied and released; borrowed +/// ones (a callback's arguments) are copied. Objects are always adopted. +pub(crate) fn receive(ty: &Ty, arrival: Arrival, owned: bool) -> String { + let enum_of = |t: &Ty, v: &str| match t { + Ty::Enum(n) => format!("{}.fromValue({v})", dart_class(n)), + _ => v.to_string(), + }; + match arrival { + Arrival::Direct(v) => enum_of(ty, &v), + Arrival::OptDirect { has, value } => { + let inner = match ty { + Ty::Optional(inner) => inner.as_ref(), + other => other, + }; + format!("{has} ? {} : null", enum_of(inner, &value)) + } + Arrival::Slice { ptr, len, elem } => { + let op = if owned { "take" } else { "copy" }; + format!("{}({ptr}, {len})", slice_fn(op, elem)) + } + Arrival::String { ptr, len } => { + let op = if owned { "_takeString" } else { "_readString" }; + format!("{op}({ptr}, {len})") + } + Arrival::Bytes { ptr, len } => format!("{}({ptr}, {len})", bytes_fn(owned)), + Arrival::Buffer { ptr, len } => format!( + "_decode({}({ptr}, {len}), {})", + bytes_fn(owned), + unpack_fn(ty) + ), + Arrival::Object { ptr, nullable } => adopt_expr(&ptr, ty, nullable), + } +} + +fn bytes_fn(owned: bool) -> &'static str { + if owned { + "_takeBytes" + } else { + "_copyBytes" + } } /// The expression adopting the owned object pointer `expr` into its wrapper; /// a null pointer becomes `null` when `nullable`. pub(crate) fn adopt_expr(expr: &str, ty: &Ty, nullable: bool) -> String { - let class = object_class(ty); + let class = object_class(ty.interface_name().expect("objects are interfaces")); if nullable { format!("{expr} == nullptr ? null : {class}._({expr})") } else { @@ -331,44 +453,31 @@ pub(crate) fn adopt_expr(expr: &str, ty: &Ty, nullable: bool) -> String { } } -/// The expression turning a produced value into its Dart value per its -/// [`RetPass`]: `value` is the pointer or scalar and `len` its length slot. -/// Owned strings, bytes, and buffers are copied and released; objects are -/// adopted. -pub(crate) fn receive_expr(ty: &Ty, pass: &RetPass, value: &str, len: &str) -> String { - match pass { - RetPass::Void => String::new(), - RetPass::Direct => match ty { - Ty::Enum(n) => format!("{}.fromValue({value})", dart_class(n)), - _ => value.to_string(), - }, - RetPass::String => format!("_takeString({value}, {len})"), - RetPass::Bytes => format!("_takeBytes({value}, {len})"), - RetPass::Buffer => format!("_decode(_takeBytes({value}, {len}), {})", unpack_fn(ty)), - RetPass::Object { nullable, .. } => adopt_expr(value, ty, *nullable), +/// The Dart expression reading out slot `param` (a `T*`) through the frame +/// slot `slot`. +fn read_slot(slot: &str, param: &AbiParam) -> String { + match ¶m.ty { + CType::Ptr { pointee, .. } => format!("{slot}.cast<{}>().value", ffi_type(pointee).0), + other => unreachable!("out slot {} is a {other:?}, not a pointer", param.name), } } -/// Write `body` inside the try/finally that scopes `_self` and `_arena`, or -/// bare when the call needs neither. -fn scoped(w: &mut CodeWriter, has_self: bool, arena: bool, body: impl FnOnce(&mut CodeWriter)) { +/// The frame slot holding out slot number `i` of a call. +fn frame_slot(f: &str, i: usize) -> String { + format!("{f}.slot{i}") +} + +/// Write `body` inside the try/finally that scopes `_self` and frame `_f`. +fn framed(w: &mut CodeWriter, has_self: bool, body: impl FnOnce(&mut CodeWriter)) { if has_self { w.line("final _self = _enter();"); } - if arena { - w.line("final _arena = Arena();"); - } - if !has_self && !arena { - body(w); - return; - } + w.line("final _f = _Frame.take();"); w.line("try {"); w.scope(body); w.line("} finally {"); w.scope(|w| { - if arena { - w.line("_arena.releaseAll();"); - } + w.line("_f.release();"); if has_self { w.line("_leave();"); } @@ -376,45 +485,73 @@ fn scoped(w: &mut CodeWriter, has_self: bool, arena: bool, body: impl FnOnce(&mu w.line("}"); } +/// The value type of `f`'s return. +fn ret_value(f: &FnBinding) -> &Ty { + f.ret + .as_ref() + .and_then(|r| r.value()) + .expect("a value return") +} + /// A synchronous wrapper: stage, call, check, receive. -fn render_sync( - w: &mut CodeWriter, - f: &FnBinding, - abi: &AbiFn, - kind: &DartDecl, - name: &str, - err: ErrCtx, -) { - let ret = f.ret.as_ref().map_or("void".to_string(), dart_type); - let pass = RetPass::of(f.ret.as_ref()); +fn render_sync(w: &mut CodeWriter, f: &FnBinding, kind: &DartDecl, name: &str, throws: &Throws) { let mut inputs = marshal_inputs(f); - if matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer) { - inputs.args.push("_outLen".into()); + let outs = f.ret_pass.out_slots(); + for i in 0..outs.len() { + inputs.args.push(format!("{}.cast()", frame_slot("_f", i))); } - inputs.args.push("_err".into()); - let call = format!("{}({})", ffi_var(&abi.symbol), inputs.args.join(", ")); + inputs.args.push("_f.err".into()); + let call = format!("{}({})", ffi_var(&f.abi.symbol), inputs.args.join(", ")); + let slot = |i: usize| read_slot(&frame_slot("_f", i), outs[i]); w.block( - kind.open_line(&ret, name, &wrapper_params(f), ""), + kind.open_line(&return_type(f), name, &wrapper_params(f), ""), "}", |w| { - scoped(w, f.has_self, inputs.arena, |w| { + framed(w, f.has_self(), |w| { for s in &inputs.stage { w.line(s); } - match (&pass, f.ret.as_ref()) { - (RetPass::Void, _) | (_, None) => { + let result = match &f.ret_pass { + RetPass::Void => { w.line(format!("{call};")); - w.line(err.check_stmt()); - } - (_, Some(ty)) => { - w.line(format!("final _result = {call};")); - w.line(err.check_stmt()); - w.line(format!( - "return {};", - receive_expr(ty, &pass, "_result", "_outLen.value") - )); + w.line(throws.check("_f")); + return; } - } + RetPass::Direct => Arrival::Direct("_result".into()), + RetPass::OptDirect { .. } => Arrival::OptDirect { + has: "_present".into(), + value: slot(0), + }, + RetPass::Slice { elem, .. } => Arrival::Slice { + ptr: "_result".into(), + len: slot(0), + elem: *elem, + }, + RetPass::String { .. } => Arrival::String { + ptr: "_result".into(), + len: slot(0), + }, + RetPass::Bytes { .. } => Arrival::Bytes { + ptr: "_result".into(), + len: slot(0), + }, + RetPass::Buffer { .. } => Arrival::Buffer { + ptr: "_result".into(), + len: slot(0), + }, + RetPass::Object { nullable, .. } => Arrival::Object { + ptr: "_result".into(), + nullable: *nullable, + }, + RetPass::Iterator(_) => unreachable!("iterators render separately"), + }; + let local = match &result { + Arrival::OptDirect { .. } => "_present", + _ => "_result", + }; + w.line(format!("final {local} = {call};")); + w.line(throws.check("_f")); + w.line(format!("return {};", receive(ret_value(f), result, true))); }); }, ); @@ -431,43 +568,80 @@ fn render_async( a: &AsyncBinding, kind: &DartDecl, name: &str, - err: ErrCtx, + throws: &Throws, ) { let cb = ffi_typedef(&a.callback_type); - let ret = f.ret.as_ref().map_or("void".to_string(), dart_type); - let pass = RetPass::of(f.ret.as_ref()); + let ret = return_type(f); let mut inputs = marshal_inputs(f); - if f.cancellable { + if f.cancellable() { inputs.args.push("_cancel?.pointer ?? nullptr".into()); } inputs.args.push("_callback.nativeFunction".into()); inputs.args.push("nullptr".into()); - let call = format!("{}({});", ffi_var(&a.launch.symbol), inputs.args.join(", ")); + let call = format!("{}({});", ffi_var(&f.abi.symbol), inputs.args.join(", ")); - // The listener's parameters, named after the callback's result slots. + // The listener's parameters, named after the completion's slots: + // `context`, `err`, then the result's. let (_, darts) = slot_types(&a.callback_params, None); - let names = ["_", "error", "value", "valueLen"]; + let names: Vec = a + .callback_params + .iter() + .map(|p| dart_ident(&p.name)) + .collect(); let listener_params: Vec = darts .iter() - .zip(names) + .zip(&names) .map(|(d, n)| format!("{d} {n}")) .collect(); - let result = match (&pass, f.ret.as_ref()) { - (RetPass::Void, _) | (_, None) => String::new(), - (_, Some(ty)) => receive_expr(ty, &pass, "value", "valueLen"), + let err = &names[1]; + let n = |p: &AbiParam| dart_ident(&p.name); + let result = match &a.result { + ResultPass::Void => None, + ResultPass::Direct { result } => Some(Arrival::Direct(n(result))), + ResultPass::OptDirect { has, value } => Some(Arrival::OptDirect { + has: n(has), + value: n(value), + }), + ResultPass::Slice { ptr, len, elem } => Some(Arrival::Slice { + ptr: n(ptr), + len: n(len), + elem: *elem, + }), + ResultPass::String { ptr, len } => Some(Arrival::String { + ptr: n(ptr), + len: n(len), + }), + ResultPass::Bytes { ptr, len } => Some(Arrival::Bytes { + ptr: n(ptr), + len: n(len), + }), + ResultPass::Buffer { ptr, len } => Some(Arrival::Buffer { + ptr: n(ptr), + len: n(len), + }), + ResultPass::Object { + result, nullable, .. + } => Some(Arrival::Object { + ptr: n(result), + nullable: *nullable, + }), + }; + let complete = match result { + None => "_completer.complete();".to_string(), + Some(arrival) => format!( + "_completer.complete({});", + receive(ret_value(f), arrival, true) + ), }; let open = kind.open_line(&format!("Future<{ret}>"), name, &wrapper_params(f), ""); - let has_self = f.has_self; + let has_self = f.has_self(); w.block(open, "}", |w| { if has_self { w.line("final _self = _enter();"); } - if inputs.arena { - w.line("final _arena = Arena();"); - } w.line(format!("final _completer = Completer<{ret}>();")); - if f.cancellable { + if f.cancellable() { w.line(format!( "final _cancel = _NativeCancel.bind({});", cancel_param(f) @@ -478,16 +652,16 @@ fn render_async( w.line(format!(" {}) {{", listener_params.join(", "))); w.scope(|w| { w.line("_callback.close();"); - if f.cancellable { + if f.cancellable() { w.line("_cancel?.release();"); } w.line("try {"); w.scope(|w| { w.line(format!( - "if (error != nullptr) throw _takeAsyncError(error, {});", - err.mapper() + "if ({err} != nullptr) throw _takeAsyncError({err}, {});", + throws.mapper() )); - w.line(format!("_completer.complete({result});")); + w.line(&complete); }); w.line("} catch (e, s) {"); w.scope(|w| { @@ -496,6 +670,11 @@ fn render_async( w.line("}"); }); w.line("});"); + // Staged arguments live in a frame's arena until the launcher returns. + let arena = inputs.uses_arena(); + if arena { + w.line("final _f = _Frame.take();"); + } w.line("try {"); w.scope(|w| { for s in &inputs.stage { @@ -506,16 +685,16 @@ fn render_async( w.line("} catch (_) {"); w.scope(|w| { w.line("_callback.close();"); - if f.cancellable { + if f.cancellable() { w.line("_cancel?.release();"); } w.line("rethrow;"); }); - if has_self || inputs.arena { + if arena || has_self { w.line("} finally {"); w.scope(|w| { - if inputs.arena { - w.line("_arena.releaseAll();"); + if arena { + w.line("_f.release();"); } if has_self { w.line("_leave();"); @@ -528,50 +707,68 @@ fn render_async( } /// An iterator wrapper: a lazy `sync*` body that launches the native -/// iterator on the first pull, issues one `next` per element, and destroys -/// the iterator exactly once (eagerly on completion or failure, or through -/// the finalizer when the iteration is abandoned). +/// iterator on the first pull, issues one `next` per element (each in its +/// own frame, released before the element is yielded), and destroys the +/// iterator exactly once (eagerly on completion or failure, or through the +/// finalizer when the iteration is abandoned). fn render_iterator( w: &mut CodeWriter, f: &FnBinding, - ib: &IteratorBinding, + it: &IteratorBinding, kind: &DartDecl, name: &str, - err: ErrCtx, + throws: &Throws, ) { - let ret = f.ret.as_ref().map_or("void".to_string(), dart_type); - let elem = RetPass::of(Some(&ib.elem)); let mut inputs = marshal_inputs(f); - inputs.args.push("_err".into()); - let launch = format!("{}({})", ffi_var(&ib.launch.symbol), inputs.args.join(", ")); - // `next(iter, out_item, [out_len,] out_err)`: `_outItem` is read as the - // item slot's pointee. - let item_type = ffi_type(ib.item_ctype()).0; - let has_len = ib.next.params.len() == 4; - let next_args = if has_len { - "_iter, _outItem.cast(), _outLen, _err" - } else { - "_iter, _outItem.cast(), _err" + inputs.args.push("_f.err".into()); + let launch = format!("{}({})", ffi_var(&f.abi.symbol), inputs.args.join(", ")); + let slots = it.item.slots(); + let mut next_args = vec!["_iter".to_string()]; + next_args.extend((0..slots.len()).map(|i| format!("{}.cast()", frame_slot("_step", i)))); + next_args.push("_step.err".into()); + let slot = |i: usize| read_slot(&frame_slot("_step", i), slots[i]); + let item = match &it.item { + ItemPass::Direct { .. } => Arrival::Direct(slot(0)), + ItemPass::OptDirect { .. } => Arrival::OptDirect { + has: slot(0), + value: slot(1), + }, + ItemPass::Slice { elem, .. } => Arrival::Slice { + ptr: slot(0), + len: slot(1), + elem: *elem, + }, + ItemPass::String { .. } => Arrival::String { + ptr: slot(0), + len: slot(1), + }, + ItemPass::Bytes { .. } => Arrival::Bytes { + ptr: slot(0), + len: slot(1), + }, + ItemPass::Buffer { .. } => Arrival::Buffer { + ptr: slot(0), + len: slot(1), + }, + ItemPass::Object { nullable, .. } => Arrival::Object { + ptr: slot(0), + nullable: *nullable, + }, }; - let destroy = ffi_var(&ib.destroy_symbol); - let item = receive_expr( - &ib.elem, - &elem, - &format!("_outItem.cast<{item_type}>().value"), - "_outLen.value", - ); + let item = receive(&it.elem, item, true); + let destroy = ffi_var(&it.destroy_symbol); w.block( - kind.open_line(&ret, name, &wrapper_params(f), "sync*"), + kind.open_line(&return_type(f), name, &wrapper_params(f), "sync*"), "}", |w| { w.line("final Pointer _iter;"); - scoped(w, f.has_self, inputs.arena, |w| { + framed(w, f.has_self(), |w| { for s in &inputs.stage { w.line(s); } w.line(format!("_iter = {launch};")); - w.line(err.check_stmt()); + w.line(throws.check("_f")); }); w.line("final _anchor = _IteratorAnchor();"); w.line(format!( @@ -580,13 +777,25 @@ fn render_iterator( w.line("try {"); w.scope(|w| { w.block("while (true) {", "}", |w| { - w.line(format!( - "final _more = {}({next_args});", - ffi_var(&ib.next.symbol) - )); - w.line(err.check_stmt()); - w.line("if (_more == 0) break;"); - w.line(format!("yield {item};")); + w.line("final _step = _Frame.take();"); + w.line(format!("final {} _item;", dart_type(&it.elem))); + w.line("try {"); + w.scope(|w| { + w.line(format!( + "final _more = {}({});", + ffi_var(&it.next.symbol), + next_args.join(", ") + )); + w.line(throws.check("_step")); + w.line("if (_more == 0) break;"); + w.line(format!("_item = {item};")); + }); + w.line("} finally {"); + w.scope(|w| { + w.line("_step.release();"); + }); + w.line("}"); + w.line("yield _item;"); }); }); w.line("} finally {"); diff --git a/crates/weaveffi-cli/src/targets/dart/codec.rs b/crates/weaveffi-cli/src/targets/dart/codec.rs index b224c9b3..22f6761d 100644 --- a/crates/weaveffi-cli/src/targets/dart/codec.rs +++ b/crates/weaveffi-cli/src/targets/dart/codec.rs @@ -1,8 +1,10 @@ //! Value-buffer codec emitters: the expression writing and the expression //! reading every wire shape, and the composite codecs, one -//! `_pack{Name}`/`_unpack{Name}` pair per distinct optional, list, or map -//! type (`[Entry]`, `{string:i64}`, `Shape?`), so no call site inlines a -//! loop. +//! `_pack_{stem}`/`_unpack_{stem}` pair per distinct optional, list, or map +//! type that crosses inside a buffer (`_pack_list_Entry`, +//! `_unpack_map_string_i64`), named by the shared +//! [`codecs::stem`](crate::codegen::codecs::stem), so no call site inlines a +//! loop. A record or rich enum's own pair is `_pack_{Name}`. //! //! Every dispatch goes through [`Ty::wire`], and primitives map onto the //! runtime's `read{Prim}`/`write{Prim}` methods by @@ -13,36 +15,21 @@ //! wrapper's `_cloneRef()` so the encoding owns a fresh reference and the //! wrapper keeps its own; reading one adopts the pointer into a new wrapper. -use std::collections::BTreeMap; - +use crate::codegen::codecs::{composites, stem}; use crate::codegen::CodeWriter; -use weaveffi_model::model::{FieldBinding, Model, ParamBinding}; +use weaveffi_model::model::Model; use weaveffi_model::ty::{Ty, WireType}; -use crate::targets::dart::types::{dart_class, dart_type}; - -/// The name of a buffered type's codec functions after `_pack`/`_unpack`: -/// a record or rich enum's class, or a composite spelled in prefix order so -/// it's unambiguous (`[Entry]` is `ListOfEntry`, `{string:[i32]}` is -/// `MapOfStringToListOfI32`, `Shape?` is `OptionalOfShape`). -fn codec_name(t: &Ty) -> String { - match t { - Ty::Prim(p) => p.pascal().to_string(), - Ty::Optional(inner) => format!("OptionalOf{}", codec_name(inner)), - Ty::List(inner) => format!("ListOf{}", codec_name(inner)), - Ty::Map(k, v) => format!("MapOf{}To{}", codec_name(k), codec_name(v)), - other => dart_class(other.user_name().expect("only user types remain")), - } -} +use crate::targets::dart::types::{dart_class, dart_in_type, dart_type}; -/// The `_pack{Name}` function writing a value of the buffered type `t`. +/// The `_pack_{stem}` function writing a value of the buffered type `t`. pub(crate) fn pack_fn(t: &Ty) -> String { - format!("_pack{}", codec_name(t)) + format!("_pack_{}", stem(t)) } -/// The `_unpack{Name}` function reading a value of the buffered type `t`. +/// The `_unpack_{stem}` function reading a value of the buffered type `t`. pub(crate) fn unpack_fn(t: &Ty) -> String { - format!("_unpack{}", codec_name(t)) + format!("_unpack_{}", stem(t)) } /// The Dart expression writing `expr` (a value of `t`) into the writer `w`. @@ -74,80 +61,14 @@ pub(crate) fn read_expr(t: &Ty, r: &str) -> String { } } -/// Every distinct composite type (optional, list, or map) that crosses -/// inside a value buffer, by codec name. -fn composites(model: &Model) -> BTreeMap { - /// A type in a top-level position: a parameter, return, or iterator - /// element. `Interface?` and `Cb?` aren't buffers there. - fn top(t: &Ty, out: &mut BTreeMap) { - match t { - Ty::Iterator(elem) => top(elem, out), - _ if t.is_buffered() => nested(t, out), - _ => {} - } - } - /// A type inside a value buffer, where every optional is encoded. - fn nested(t: &Ty, out: &mut BTreeMap) { - match t { - Ty::Optional(inner) | Ty::List(inner) => { - out.insert(codec_name(t), t.clone()); - nested(inner, out); - } - Ty::Map(k, v) => { - out.insert(codec_name(t), t.clone()); - nested(k, out); - nested(v, out); - } - _ => {} - } - } - fn fields(fs: &[FieldBinding], out: &mut BTreeMap) { - for f in fs { - nested(&f.ty, out); - } - } - fn signature(ps: &[ParamBinding], ret: &Option, out: &mut BTreeMap) { - for p in ps { - top(&p.ty, out); - } - if let Some(t) = ret { - top(t, out); - } - } - let mut out = BTreeMap::new(); - for m in &model.modules { - for s in &m.structs { - fields(&s.fields, &mut out); - } - for e in &m.enums { - for v in &e.variants { - fields(&v.fields, &mut out); - } - } - if let Some(eb) = &m.errors { - for c in &eb.codes { - fields(&c.fields, &mut out); - } - } - for f in m.callables() { - signature(&f.params, &f.ret, &mut out); - } - for cb in &m.callback_interfaces { - for f in &cb.methods { - signature(&f.params, &f.ret, &mut out); - } - } - } - out -} - /// Render the composite codecs `model` uses, one `_pack`/`_unpack` pair per /// distinct type, built on the runtime's generic `writeOptional`, -/// `writeList`, and `writeMap` (and their readers). +/// `writeList`, and `writeMap` (and their readers). A writer accepts the +/// input flavor of its type (any `List` for `bytes`), so a value of +/// either flavor encodes. pub(crate) fn render_codecs(w: &mut CodeWriter, model: &Model) { - for t in composites(model).values() { - let ty = dart_type(t); - let (write, read) = match t { + for t in composites(model) { + let (write, read) = match &t { Ty::Optional(inner) => ( format!("w.writeOptional(v, (e) => {})", write_expr(inner, "w", "e")), format!("r.readOptional(() => {})", read_expr(inner, "r")), @@ -171,10 +92,18 @@ pub(crate) fn render_codecs(w: &mut CodeWriter, model: &Model) { _ => unreachable!("only composites are collected"), }; w.blank(); - w.line(format!("void {}(_BufferWriter w, {ty} v) =>", pack_fn(t))); + w.line(format!( + "void {}(_BufferWriter w, {} v) =>", + pack_fn(&t), + dart_in_type(&t) + )); w.line(format!(" {write};")); w.blank(); - w.line(format!("{ty} {}(_BufferReader r) =>", unpack_fn(t))); + w.line(format!( + "{} {}(_BufferReader r) =>", + dart_type(&t), + unpack_fn(&t) + )); w.line(format!(" {read};")); } } diff --git a/crates/weaveffi-cli/src/targets/dart/docs.rs b/crates/weaveffi-cli/src/targets/dart/docs.rs index 81173cbb..f7ecf69e 100644 --- a/crates/weaveffi-cli/src/targets/dart/docs.rs +++ b/crates/weaveffi-cli/src/targets/dart/docs.rs @@ -1,89 +1,175 @@ -//! Doc-comment emission: `///` comments carried from the IDL, plus the -//! generated notes (streaming contract, ownership, thrown exception, -//! cancellation, deprecation) a wrapper's declaration carries. +//! Doc-comment emission: `///` comments carried from the IDL, with every +//! backticked API identifier respelled for Dart (through the shared +//! [`codegen::docs`](crate::codegen::docs) rewriting), plus the generated +//! notes (streaming contract, ownership, thrown exception, cancellation) a +//! wrapper's declaration carries and its `@Deprecated` annotation. +//! +//! In doc comments, functions, types, and exceptions become dartdoc +//! references (`` `new_op` `` is `[newOp]`); members, parameters, and +//! fields keep their backticks in Dart spelling. Deprecation messages are +//! plain strings, so every identifier keeps its backticks there. use crate::codegen::common::DocCommentStyle; +use crate::codegen::docs::{map_code_spans, rewrite, ApiNames, Doc, IdentKind}; use crate::codegen::CodeWriter; -use weaveffi_model::model::{CallShape, FnBinding}; -use weaveffi_model::ty::Ty; +use weaveffi_model::model::{FnBinding, Model}; +use weaveffi_model::ty::RetTy; -use crate::targets::dart::calls::ErrCtx; -use crate::targets::dart::types::dart_str_literal; +use crate::targets::dart::calls::Throws; +use crate::targets::dart::types::{ + dart_class, dart_ident, dart_member, dart_str_literal, exception_class, +}; -/// Emit `doc` at the writer's current depth. -pub(crate) fn write_doc(w: &mut CodeWriter, doc: &Option) { - w.doc(doc, DocCommentStyle::TripleSlash); +/// Spells the API identifiers IDL docs and deprecation messages mention. +pub(crate) struct Docs { + names: ApiNames, } -/// Emit `@Deprecated(...)` when `deprecated` carries a message. -pub(crate) fn write_deprecated(w: &mut CodeWriter, deprecated: &Option) { - if let Some(msg) = deprecated { - w.line(format!("@Deprecated('{}')", dart_str_literal(msg))); +impl Docs { + /// Index every identifier `model` declares. + pub(crate) fn new(model: &Model) -> Self { + Self { + names: ApiNames::new(model), + } } -} -/// Emit a wrapper's doc comment followed by the generated notes that apply -/// to it, then its `@Deprecated` annotation. -pub(crate) fn emit_wrapper_doc(w: &mut CodeWriter, f: &FnBinding, err: ErrCtx) { - write_doc(w, &f.doc); - let mut notes: Vec<&str> = Vec::new(); - let mut owned: Vec = Vec::new(); - if let CallShape::Iterator(ib) = &f.shape { - notes.extend([ - "Returns a lazy [Iterable]: each step pulls one element from a native", - "iterator, and iterating again starts a new one. The native iterator", - "is released when the iteration ends, fails, or is abandoned and", - "collected.", - ]); - if ib.elem.interface_name().is_some() { - notes.extend([ - "", - "Each element is owned by the caller: call its `dispose()` when", - "you're done with it.", - ]); - } - } else if let Some(ret) = f.ret.as_ref().filter(|r| r.interface_name().is_some()) { - if matches!(ret, Ty::Optional(_)) { - notes.extend([ - "Returns null when the producer reports no object. A returned object", - "is owned by the caller: call its `dispose()` when you're done with it.", - ]); - } else { - notes.extend([ - "The returned object is owned by the caller: call its `dispose()`", - "when you're done with it.", - ]); + /// The Dart spelling of `ident` and whether dartdoc can link to it from + /// anywhere in the library, or `None` to keep it as written. + fn spell(&self, ident: &str) -> Option<(String, bool)> { + if ident.contains('.') { + return None; } + Some(match self.names.kind(ident)? { + IdentKind::Function => (dart_ident(ident), true), + IdentKind::Type => (dart_class(ident), true), + IdentKind::ErrorDomain | IdentKind::ErrorCode => (exception_class(ident), true), + IdentKind::Member => (dart_member(ident), false), + IdentKind::CallbackMethod | IdentKind::Field | IdentKind::Param => { + (dart_ident(ident), false) + } + IdentKind::Module | IdentKind::Variant => return None, + }) } - if f.cancellable { - if !notes.is_empty() { - notes.push(""); - } - notes.extend([ - "Cancelling [cancelToken] completes the returned future with a", - "[CancelledException].", - ]); + + /// `text` respelled for a doc comment: linkable names become `[name]`, + /// the rest keep their backticks. + fn doc_text(&self, text: &str) -> String { + map_code_spans(text, |ident| { + self.spell(ident).map(|(name, link)| { + if link { + format!("[{name}]") + } else { + format!("`{name}`") + } + }) + }) } - if let Some(exc) = err.thrown_exception() { - if !notes.is_empty() { - notes.push(""); + + /// Emit `doc` (respelled) at the writer's current depth. + pub(crate) fn write(&self, w: &mut CodeWriter, doc: &Option) { + let none = None; + let text = Doc::new(doc, &none).text(|_| None); + w.doc( + &text.map(|t| self.doc_text(&t)), + DocCommentStyle::TripleSlash, + ); + } + + /// Emit `@Deprecated(...)` when `deprecated` carries a message. + pub(crate) fn write_deprecated(&self, w: &mut CodeWriter, deprecated: &Option) { + let none = None; + if let Some(msg) = Doc::new(&none, deprecated).deprecation(|_| None) { + let msg = rewrite(&msg, |ident| self.spell(ident).map(|(name, _)| name)); + w.line(format!("@Deprecated('{}')", dart_str_literal(&msg))); } - owned.push(format!("Throws [{exc}] on domain errors.")); } - if !notes.is_empty() || !owned.is_empty() { - if f.doc.is_some() { - w.line("///"); + + /// Emit a wrapper's doc comment followed by the generated notes that + /// apply to it, then its `@Deprecated` annotation. + pub(crate) fn write_wrapper(&self, w: &mut CodeWriter, f: &FnBinding, throws: &Throws) { + self.write(w, &f.doc); + let mut notes: Vec = Vec::new(); + let para = |notes: &mut Vec, lines: &[&str]| { + if !notes.is_empty() { + notes.push(String::new()); + } + notes.extend(lines.iter().map(|l| (*l).to_string())); + }; + match &f.ret { + Some(RetTy::Iterator(elem)) => { + para( + &mut notes, + &[ + "Returns a lazy [Iterable]: each step pulls one element from a native", + "iterator, and iterating again starts a new one. The native iterator", + "is released when the iteration ends, fails, or is abandoned and", + "collected.", + ], + ); + if elem.interface_name().is_some() { + para( + &mut notes, + &[ + "Each element is owned by the caller: call its `dispose()` when", + "you're done with it.", + ], + ); + } + } + Some(RetTy::Value(ret)) if ret.interface_name().is_some() => { + if matches!(ret, weaveffi_model::ty::Ty::Optional(_)) { + para( + &mut notes, + &[ + "Returns null when the producer reports no object. A returned object", + "is owned by the caller: call its `dispose()` when you're done with it.", + ], + ); + } else { + para( + &mut notes, + &[ + "The returned object is owned by the caller: call its `dispose()`", + "when you're done with it.", + ], + ); + } + } + _ => {} + } + if f.cancellable() { + para( + &mut notes, + &[ + "Cancelling [cancelToken] completes the returned future with a", + "[CancelledException].", + ], + ); } - for n in notes { - w.line(if n.is_empty() { - "///".to_string() - } else { - format!("/// {n}") - }); + match throws { + Throws::Domain(exc) => { + let line = + format!("Throws [{exc}] (or a subclass for one of its codes) on failure."); + para(&mut notes, &[line.as_str()]); + } + Throws::Untyped => para( + &mut notes, + &["Throws a [NativeException] with [NativeException.genericCode] on failure."], + ), + Throws::Trap => {} } - for n in owned { - w.line(format!("/// {n}")); + if !notes.is_empty() { + if f.doc.as_deref().is_some_and(|d| !d.trim().is_empty()) { + w.line("///"); + } + for n in notes { + w.line(if n.is_empty() { + "///".to_string() + } else { + format!("/// {n}") + }); + } } + self.write_deprecated(w, &f.deprecated); } - write_deprecated(w, &f.deprecated); } diff --git a/crates/weaveffi-cli/src/targets/dart/entities.rs b/crates/weaveffi-cli/src/targets/dart/entities.rs index d489bdbf..5351e1e3 100644 --- a/crates/weaveffi-cli/src/targets/dart/entities.rs +++ b/crates/weaveffi-cli/src/targets/dart/entities.rs @@ -1,41 +1,24 @@ //! Entity rendering: error domains, C-style enums, rich enums, records, and //! interface wrapper classes, plus their value-buffer codec helpers. +use crate::codegen::errors::ErrorTable; use crate::codegen::CodeWriter; -use heck::ToUpperCamelCase; use weaveffi_model::model::{ - EnumBinding, EnumVariantBinding, ErrorBinding, FieldBinding, InterfaceBinding, ModuleBinding, - StructBinding, + EnumBinding, EnumVariantBinding, FieldBinding, InterfaceBinding, Model, StructBinding, }; -use weaveffi_model::ty::Ty; +use weaveffi_model::ty::{Prim, Ty}; use crate::targets::dart::calls::{ - emit_bindings, emit_destroy, emit_wrapper, lookup, mapper_fn, DartDecl, ErrCtx, + emit_bindings, emit_destroy, emit_wrapper, lookup, mapper_fn, DartDecl, }; -use crate::targets::dart::codec::{read_expr, write_expr}; -use crate::targets::dart::docs::{write_deprecated, write_doc}; +use crate::targets::dart::codec::{pack_fn, read_expr, unpack_fn, write_expr}; +use crate::targets::dart::docs::Docs; use crate::targets::dart::types::{ - dart_class, dart_ident, dart_member, dart_str_literal, dart_type, ffi_var, + dart_class, dart_field, dart_ident, dart_member, dart_str_literal, dart_type, error_field, + ffi_var, }; -/// The Dart exception class of an error domain or code: the PascalCase name -/// with a trailing `Error` swapped for `Exception` (`KvError` becomes -/// `KvException`, a code `IoError` becomes `IoException`, and a bare -/// `Error` becomes `ErrorException`). -pub(crate) fn dart_exception_name(raw: &str) -> String { - let pascal = raw.to_upper_camel_case(); - let stem = pascal - .strip_suffix("Error") - .filter(|s| !s.is_empty()) - .unwrap_or(&pascal); - if stem.ends_with("Exception") { - dart_class(stem) - } else { - dart_class(&format!("{stem}Exception")) - } -} - -/// The `_{exception}Payload` function encoding a domain exception's fields +/// The `_payloadOf{Exception}` function encoding a domain exception's fields /// for a callback that reports it. pub(crate) fn payload_fn(exception: &str) -> String { format!("_payloadOf{exception}") @@ -50,15 +33,17 @@ fn article(class: &str) -> &'static str { } } -/// Render the final fields of a value class, with their docs. -fn render_fields(w: &mut CodeWriter, fields: &[FieldBinding]) { +/// Render the final fields of a value class, with their docs; `name` +/// spells a field. +fn render_fields( + w: &mut CodeWriter, + docs: &Docs, + fields: &[FieldBinding], + name: fn(&str) -> String, +) { for f in fields { - write_doc(w, &f.doc); - w.line(format!( - "final {} {};", - dart_type(&f.ty), - dart_ident(&f.name) - )); + docs.write(w, &f.doc); + w.line(format!("final {} {};", dart_type(&f.ty), name(&f.name))); } } @@ -67,7 +52,7 @@ fn render_fields(w: &mut CodeWriter, fields: &[FieldBinding]) { fn is_collection(ty: &Ty) -> bool { match ty { Ty::Optional(inner) => is_collection(inner), - Ty::List(_) | Ty::Map(..) | Ty::Prim(weaveffi_model::ty::Prim::Bytes) => true, + Ty::List(_) | Ty::Map(..) | Ty::Prim(Prim::Bytes) => true, _ => false, } } @@ -95,7 +80,7 @@ fn render_value_members(w: &mut CodeWriter, class: &str, fields: &[FieldBinding] w.line("identical(this, other) ||"); w.line(format!("other is {class} &&")); for (i, f) in fields.iter().enumerate() { - let n = dart_ident(&f.name); + let n = dart_field(&f.name); let end = if i + 1 == fields.len() { ";" } else { " &&" }; if is_collection(&f.ty) { w.line(format!("_deepEquals({n}, other.{n}){end}")); @@ -110,7 +95,7 @@ fn render_value_members(w: &mut CodeWriter, class: &str, fields: &[FieldBinding] let hashes: Vec = fields .iter() .map(|f| { - let n = dart_ident(&f.name); + let n = dart_field(&f.name); if is_collection(&f.ty) { format!("_deepHash({n})") } else { @@ -127,7 +112,7 @@ fn render_value_members(w: &mut CodeWriter, class: &str, fields: &[FieldBinding] let shown: Vec = fields .iter() .map(|f| { - let n = dart_ident(&f.name); + let n = dart_field(&f.name); format!("{n}: ${n}") }) .collect(); @@ -137,29 +122,26 @@ fn render_value_members(w: &mut CodeWriter, class: &str, fields: &[FieldBinding] )); } -/// Render one module's declared error domain: a sealed domain exception (a -/// `NativeException` subclass) with one final subclass per code carrying -/// its code, default message, and decoded payload fields; the -/// `_map{Domain}` mapper that throwing wrappers route codes through (only -/// declared codes gain a `case`; every other code, including the negative -/// runtime range, maps onto the runtime exceptions); and, when `reported` -/// (a callback method of the API reports this domain), the encoder of each -/// code's fields. -pub(crate) fn render_error( - w: &mut CodeWriter, - module: &ModuleBinding, - eb: &ErrorBinding, - reported: bool, -) { - let exc = dart_exception_name(&eb.type_name); +/// Render one error domain: an open domain exception (a `NativeException` +/// subclass) with one final subclass per code carrying its code, default +/// message, and decoded payload fields; the `_map{Domain}` mapper the +/// domain's callables route codes through (a declared code is its subclass, +/// any other positive code the domain class itself with its code and +/// message, and a negative code a runtime exception); and, when `reported` +/// (a callback method reports this domain), the encoder of each code's +/// fields. +pub(crate) fn render_error(w: &mut CodeWriter, docs: &Docs, table: &ErrorTable, reported: bool) { + let exc = dart_class(&table.type_name); w.blank(); w.line(format!( "/// The `{}` error domain of module `{}`: catch it to handle any of", - eb.name, module.dot_path + table.domain.name, table.module.dot_path )); - w.line("/// its codes, or a subclass for one code."); + w.line("/// its codes, or a subclass for one code. The domain is open: a code"); + w.line("/// these bindings don't declare (from a newer library) arrives as this"); + w.line("/// class itself, with its [code] and [message]."); w.block( - format!("sealed class {exc} extends NativeException {{"), + format!("class {exc} extends NativeException {{"), "}", |w| { w.line("/// Creates a domain error carrying [code] and [message]."); @@ -167,11 +149,12 @@ pub(crate) fn render_error( }, ); - for c in &eb.codes { - let class = dart_exception_name(&c.name); + for row in &table.codes { + let c = row.code; + let class = dart_class(&row.type_name); let message = dart_str_literal(&c.message); w.blank(); - write_doc(w, &c.doc.clone().or_else(|| Some(c.message.clone()))); + docs.write(w, &c.doc.clone().or_else(|| Some(c.message.clone()))); w.block(format!("final class {class} extends {exc} {{"), "}", |w| { w.line(format!( "/// Creates {} [{class}] (code {}).", @@ -187,7 +170,7 @@ pub(crate) fn render_error( let params: Vec = c .fields .iter() - .map(|f| format!("this.{}", dart_ident(&f.name))) + .map(|f| format!("this.{}", error_field(&f.name))) .collect(); w.line(format!( "{class}({}, [String message = '{message}'])", @@ -195,43 +178,41 @@ pub(crate) fn render_error( )); w.line(format!(" : super({}, message);", c.value)); w.blank(); - render_fields(w, &c.fields); + render_fields(w, docs, &c.fields, error_field); } }); } w.blank(); - w.block( - format!( - "NativeException {}(int code, String message, Uint8List payload) {{", - mapper_fn(&exc) - ), - "}", - |w| { - w.block("switch (code) {", "}", |w| { - for c in &eb.codes { - let class = dart_exception_name(&c.name); - w.line(format!("case {}:", c.value)); - w.scope(|w| { - if c.fields.is_empty() { - w.line(format!("return {class}(message);")); - } else { - let args: Vec = - c.fields.iter().map(|f| read_expr(&f.ty, "r")).collect(); - w.line(format!( - "return _decode(payload, (r) => {class}({}, message));", - args.join(", ") - )); - } - }); + w.line(format!( + "NativeException {}(int code, String message, Uint8List payload) =>", + mapper_fn(&exc) + )); + w.scope(|w| { + w.scope(|w| { + w.line("switch (code) {"); + w.scope(|w| { + for row in &table.codes { + let c = row.code; + let class = dart_class(&row.type_name); + if c.fields.is_empty() { + w.line(format!("{} => {class}(message),", c.value)); + } else { + let args: Vec = + c.fields.iter().map(|f| read_expr(&f.ty, "r")).collect(); + w.line(format!( + "{} => _decode(payload, (r) => {class}({}, message)),", + c.value, + args.join(", ") + )); + } } - w.line("default:"); - w.scope(|w| { - w.line("return _runtimeException(code, message, payload);"); - }); + w.line(format!("> 0 => {exc}(code, message),")); + w.line("_ => _runtimeException(code, message, payload),"); }); - }, - ); + w.line("};"); + }); + }); if !reported { return; @@ -242,24 +223,23 @@ pub(crate) fn render_error( format!("Uint8List? {}({exc} e) {{", payload_fn(&exc)), "}", |w| { + w.line("final w = _BufferWriter();"); w.block("switch (e) {", "}", |w| { - for c in &eb.codes { - let class = dart_exception_name(&c.name); - w.line(format!("case {class}():")); + for row in table.codes.iter().filter(|r| !r.code.fields.is_empty()) { + w.line(format!("case {}():", dart_class(&row.type_name))); w.scope(|w| { - if c.fields.is_empty() { - w.line("return null;"); - } else { - w.line("final w = _BufferWriter();"); - for f in &c.fields { - let expr = format!("e.{}", dart_ident(&f.name)); - w.line(format!("{};", write_expr(&f.ty, "w", &expr))); - } - w.line("return w.takeBytes();"); + for f in &row.code.fields { + let expr = format!("e.{}", error_field(&f.name)); + w.line(format!("{};", write_expr(&f.ty, "w", &expr))); } }); } + w.line("default:"); + w.scope(|w| { + w.line("return null;"); + }); }); + w.line("return w.takeBytes();"); }, ); } @@ -268,11 +248,11 @@ pub(crate) fn render_error( /// then a wrapper class over the runtime's `_NativeObject` where the /// canonical `new` constructor is an unnamed factory, other constructors are /// named factories, methods borrow the wrapper's pointer, and statics are -/// `static`. `exception` is the domain exception class in scope for the -/// interface's module. +/// `static`. pub(crate) fn render_interface( w: &mut CodeWriter, - exception: Option<&str>, + model: &Model, + docs: &Docs, i: &InterfaceBinding, leaf: bool, ) { @@ -304,14 +284,14 @@ pub(crate) fn render_interface( } w.blank(); - write_doc(w, &i.doc); + docs.write(w, &i.doc); if i.doc.is_some() { w.line("///"); } w.line("/// A reference-counted native object. Each instance holds one strong"); w.line("/// reference, released by [dispose] or, when an undisposed instance is"); w.line("/// collected, by a finalizer."); - write_deprecated(w, &i.deprecated); + docs.write_deprecated(w, &i.deprecated); w.block( format!("final class {class} extends _NativeObject {{"), "}", @@ -333,7 +313,7 @@ pub(crate) fn render_interface( "Pointer _clone(Pointer ptr) => {clone}(ptr);" )); for (f, kind) in members() { - emit_wrapper(w, f, &kind, &dart_member(&f.name), ErrCtx::of(f, exception)); + emit_wrapper(w, model, docs, f, &kind, &dart_member(&f.name)); } }, ); @@ -345,22 +325,22 @@ const ENUM_MEMBERS: &[&str] = &["fromValue", "index", "name", "value", "values"] /// Render one enum: a C-style enum becomes an enhanced Dart `enum` carrying /// its discriminant; a rich enum becomes a sealed class hierarchy. -pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { +pub(crate) fn render_enum(w: &mut CodeWriter, docs: &Docs, e: &EnumBinding) { if e.is_rich() { - render_rich_enum(w, e); + render_rich_enum(w, docs, e); return; } let name = dart_class(&e.name); w.blank(); - write_doc(w, &e.doc); - write_deprecated(w, &e.deprecated); + docs.write(w, &e.doc); + docs.write_deprecated(w, &e.deprecated); w.block(format!("enum {name} {{"), "}", |w| { for (i, v) in e.variants.iter().enumerate() { let mut variant = dart_ident(&v.name); if ENUM_MEMBERS.contains(&variant.as_str()) { variant.push('_'); } - write_doc(w, &v.doc); + docs.write(w, &v.doc); let end = if i + 1 == e.variants.len() { ';' } else { ',' }; w.line(format!("{variant}({}){end}", v.value)); } @@ -382,12 +362,13 @@ pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { /// Render one record as a Dart value class (final fields, one named /// constructor argument per field, optional fields not required, value -/// equality) plus its `_pack{Name}`/`_unpack{Name}` helpers. -pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { +/// equality) plus its `_pack_{Name}`/`_unpack_{Name}` helpers. +pub(crate) fn render_struct(w: &mut CodeWriter, docs: &Docs, s: &StructBinding) { let class = dart_class(&s.name); + let ty = Ty::Record(s.name.clone()); w.blank(); - write_doc(w, &s.doc); - write_deprecated(w, &s.deprecated); + docs.write(w, &s.doc); + docs.write_deprecated(w, &s.deprecated); w.block(format!("final class {class} {{"), "}", |w| { w.line(format!("/// Creates {} [{class}].", article(&class))); if s.fields.is_empty() { @@ -397,7 +378,7 @@ pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { .fields .iter() .map(|f| { - let n = dart_ident(&f.name); + let n = dart_field(&f.name); if matches!(f.ty, Ty::Optional(_)) { format!("this.{n}") } else { @@ -407,7 +388,7 @@ pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { .collect(); w.line(format!("{class}({{{}}});", params.join(", "))); w.blank(); - render_fields(w, &s.fields); + render_fields(w, docs, &s.fields, dart_field); } render_value_members(w, &class, &s.fields); }); @@ -415,15 +396,16 @@ pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { w.blank(); if s.fields.is_empty() { w.line(format!( - "void _pack{class}(_BufferWriter w, {class} v) {{}}" + "void {}(_BufferWriter w, {class} v) {{}}", + pack_fn(&ty) )); } else { w.block( - format!("void _pack{class}(_BufferWriter w, {class} v) {{"), + format!("void {}(_BufferWriter w, {class} v) {{", pack_fn(&ty)), "}", |w| { for f in &s.fields { - let expr = format!("v.{}", dart_ident(&f.name)); + let expr = format!("v.{}", dart_field(&f.name)); w.line(format!("{};", write_expr(&f.ty, "w", &expr))); } }, @@ -434,17 +416,19 @@ pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { w.blank(); if s.fields.is_empty() { w.line(format!( - "{class} _unpack{class}(_BufferReader r) => const {class}();" + "{class} {}(_BufferReader r) => const {class}();", + unpack_fn(&ty) )); } else { w.line(format!( - "{class} _unpack{class}(_BufferReader r) => {class}(" + "{class} {}(_BufferReader r) => {class}(", + unpack_fn(&ty) )); w.indent().indent().indent(); for f in &s.fields { w.line(format!( "{}: {},", - dart_ident(&f.name), + dart_field(&f.name), read_expr(&f.ty, "r") )); } @@ -460,13 +444,14 @@ fn variant_class(e: &EnumBinding, v: &EnumVariantBinding) -> String { /// Render one rich enum as a sealed class hierarchy: a sealed base plus one /// final subclass per variant carrying its fields (positional constructor, -/// value equality), and `_pack{Name}`/`_unpack{Name}` helpers encoding the +/// value equality), and `_pack_{Name}`/`_unpack_{Name}` helpers encoding the /// `i32` tag followed by the active variant's fields. -fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { +fn render_rich_enum(w: &mut CodeWriter, docs: &Docs, e: &EnumBinding) { let base = dart_class(&e.name); + let ty = Ty::RichEnum(e.name.clone()); w.blank(); - write_doc(w, &e.doc); - write_deprecated(w, &e.deprecated); + docs.write(w, &e.doc); + docs.write_deprecated(w, &e.deprecated); w.block(format!("sealed class {base} {{"), "}", |w| { w.line(format!("const {base}();")); }); @@ -474,7 +459,7 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { for v in &e.variants { let cls = variant_class(e, v); w.blank(); - write_doc(w, &v.doc); + docs.write(w, &v.doc); w.block(format!("final class {cls} extends {base} {{"), "}", |w| { w.line(format!("/// Creates {} [{cls}].", article(&cls))); if v.fields.is_empty() { @@ -483,11 +468,11 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { let params: Vec = v .fields .iter() - .map(|f| format!("this.{}", dart_ident(&f.name))) + .map(|f| format!("this.{}", dart_field(&f.name))) .collect(); w.line(format!("{cls}({});", params.join(", "))); w.blank(); - render_fields(w, &v.fields); + render_fields(w, docs, &v.fields, dart_field); } render_value_members(w, &cls, &v.fields); }); @@ -497,7 +482,7 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { // the switch exhaustive. w.blank(); w.block( - format!("void _pack{base}(_BufferWriter w, {base} v) {{"), + format!("void {}(_BufferWriter w, {base} v) {{", pack_fn(&ty)), "}", |w| { w.block("switch (v) {", "}", |w| { @@ -507,7 +492,7 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { w.scope(|w| { w.line(format!("w.writeI32({});", var.value)); for f in &var.fields { - let expr = format!("v.{}", dart_ident(&f.name)); + let expr = format!("v.{}", dart_field(&f.name)); w.line(format!("{};", write_expr(&f.ty, "w", &expr))); } }); @@ -519,7 +504,7 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { // Unpack: positional arguments evaluate left to right, in wire order. w.blank(); w.block( - format!("{base} _unpack{base}(_BufferReader r) {{"), + format!("{base} {}(_BufferReader r) {{", unpack_fn(&ty)), "}", |w| { w.line("final tag = r.readI32();"); diff --git a/crates/weaveffi-cli/src/targets/dart/mod.rs b/crates/weaveffi-cli/src/targets/dart/mod.rs index 604a9019..e2f6be3d 100644 --- a/crates/weaveffi-cli/src/targets/dart/mod.rs +++ b/crates/weaveffi-cli/src/targets/dart/mod.rs @@ -1,23 +1,29 @@ //! Dart (`dart:ffi`) binding generator. //! -//! Emits a standalone Dart package (`pubspec.yaml`, `README.md`, and -//! `lib/{package}.dart`) whose bindings call the C ABI through `dart:ffi`. -//! The package is named after the identity's C prefix and loads the -//! identity's library, honoring the `{PREFIX}_LIBRARY` override. On first use -//! the library's ABI revision and every top-level module's contract table -//! are verified. +//! Emits a standalone Dart package: `pubspec.yaml`, `README.md`, and one +//! library, `lib/{package}.dart`, whose parts live under `lib/src/`: the +//! runtime sections under `lib/src/runtime/` and one file per module +//! (`lib/src/kitchen.dart`, `lib/src/kitchen/nested.dart`). The package is +//! named after the identity's C prefix and loads the identity's library, +//! honoring the `{PREFIX}_LIBRARY` override. On first use the library's ABI +//! revision and every top-level module's contract table are verified, and a +//! mismatch throws a catchable `NativeLibraryException`. //! //! Records and rich enums are value types: plain Dart classes (a sealed //! hierarchy for a rich enum) with value equality that cross the ABI -//! serialized in the value buffer format. Interfaces are wrapper classes -//! owning one strong reference, released by `dispose()` or a -//! `NativeFinalizer`, and guarded so a `dispose()` during an in-flight call -//! defers the release until the call returns. Async functions return a -//! `Future` completed by a `NativeCallable.listener`; cancellable ones take a -//! `CancelToken`. Callback interfaces are abstract classes: value-returning -//! methods are isolate-local trampolines (callable only on the isolate's -//! thread), and void methods are isolate-group-bound forwarders that may run -//! on any thread and deliver the call on the isolate's event loop. +//! serialized in the value buffer format. Optional scalars cross as a +//! presence flag and a value, and numeric lists as C arrays. Interfaces are +//! wrapper classes owning one strong reference, released by `dispose()` or +//! a `NativeFinalizer`, and guarded so a `dispose()` during an in-flight +//! call defers the release until the call returns. Every call runs in its +//! own pooled frame of native out slots, so calls nested in callbacks never +//! share state. Async functions return a `Future` completed by a +//! `NativeCallable.listener`; cancellable ones take a `CancelToken`. +//! Callback interfaces are abstract classes: value-returning methods are +//! isolate-local trampolines on a thread-affine vtable (the producer refuses +//! to call them off the isolate's thread), and void methods are +//! isolate-group-bound forwarders that may run on any thread and deliver the +//! call on the isolate's event loop. mod callbacks; mod calls; @@ -33,29 +39,37 @@ mod tests; use std::collections::BTreeSet; -use crate::backend::{LanguageBackend, OutputFile}; +use crate::codegen::errors::{tables as error_tables, ErrorTable}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{per_platform_libraries, Artifact, PackageContext, PackagedFile}; +use crate::targets::Target; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use camino::Utf8Path; +use camino::Utf8PathBuf; +use miette::Result; use serde::{Deserialize, Serialize}; -use weaveffi_model::model::Model; +use weaveffi_model::model::{Model, ModuleBinding}; -use crate::targets::dart::callbacks::render_callback_interface; -use crate::targets::dart::calls::{emit_bindings, emit_wrapper, DartDecl, ErrCtx}; +use crate::targets::dart::callbacks::{render_callback_interface, reported_domains}; +use crate::targets::dart::calls::{emit_bindings, emit_wrapper, DartDecl}; use crate::targets::dart::codec::render_codecs; -use crate::targets::dart::entities::{ - dart_exception_name, render_enum, render_error, render_interface, render_struct, -}; +use crate::targets::dart::docs::Docs; +use crate::targets::dart::entities::{render_enum, render_error, render_interface, render_struct}; use crate::targets::dart::package::{render_packaged_readme, render_pubspec, render_readme}; -use crate::targets::dart::runtime::{bundles_platform, render_runtime, Bundle}; -use crate::targets::dart::types::dart_ident; +use crate::targets::dart::runtime::{bundles_platform, runtime_parts, Bundle}; +use crate::targets::dart::types::{dart_ident, dart_str_literal}; /// The lowest Dart SDK the generated bindings support: /// `NativeCallable.isolateGroupBound`, which delivers void callback methods /// from any thread, first shipped in Dart 3.10. pub const MIN_DART_SDK: &str = "3.10.0"; +/// The `ignore_for_file` line every part carries: private codec names use +/// the shared composite stems (`_pack_list_i32`), and a runtime section an +/// API doesn't fully use leaves private helpers unreferenced. +const IGNORES: &str = "// ignore_for_file: camel_case_types, non_constant_identifier_names, \ + unused_element, unused_field"; + /// Per-target configuration for [`DartGenerator`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] @@ -88,14 +102,194 @@ impl DartConfig { } /// The Dart backend. -pub struct DartGenerator; +pub struct DartGenerator { + config: DartConfig, +} + +impl From for DartGenerator { + fn from(config: DartConfig) -> Self { + Self { config } + } +} + +/// The part file of `module`, relative to `lib/`: one file per module, +/// nested by module path (`src/kitchen.dart`, `src/kitchen/nested.dart`). +/// A top-level module named `runtime` is `src/runtime_.dart`, so its +/// submodules can't land among the runtime sections. +fn module_part(module: &ModuleBinding) -> Utf8PathBuf { + // Dart `part` URIs always use `/`, so the path is built as a string + // rather than with platform-dependent joins. + let segments: Vec<&str> = module + .segments + .iter() + .enumerate() + .map(|(i, segment)| { + if i == 0 && segment == "runtime" { + "runtime_" + } else { + segment.as_str() + } + }) + .collect(); + Utf8PathBuf::from(format!("src/{}.dart", segments.join("/"))) +} + +/// Wrap a part's body: the prelude, the analyzer ignores, the `part of` +/// directive (relative to the part), and the trailer. +fn part_file(package: &str, rel: &Utf8PathBuf, body: &str) -> String { + let depth = rel.components().count() - 1; + let file = rel.file_name().expect("a part file name"); + let mut out = render_prelude(CommentStyle::DoubleSlash); + out.push_str(IGNORES); + out.push('\n'); + out.push_str(&format!( + "part of '{}{package}.dart';\n\n", + "../".repeat(depth) + )); + out.push_str(body.trim_end()); + out.push_str("\n\n"); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, file)); + out +} + +/// One module's declarations: its error domains, enums, records, callback +/// interfaces, interfaces, and functions. +fn render_module( + model: &Model, + docs: &Docs, + module: &ModuleBinding, + errors: &[&ErrorTable], + reported: &BTreeSet, + leaf: bool, +) -> String { + let mut w = CodeWriter::two_space(); + w.line(format!("// ── Module `{}` ──", module.dot_path)); + if let Some(doc) = module + .doc + .as_deref() + .map(str::trim) + .filter(|d| !d.is_empty()) + { + w.line("//"); + for line in doc.lines() { + w.line(format!("// {line}").trim_end()); + } + } + for table in errors { + render_error(&mut w, docs, table, reported.contains(&table.domain.name)); + } + for e in &module.enums { + render_enum(&mut w, docs, e); + } + for s in &module.structs { + render_struct(&mut w, docs, s); + } + for cb in &module.callback_interfaces { + render_callback_interface(&mut w, model, docs, cb); + } + for i in &module.interfaces { + render_interface(&mut w, model, docs, i, leaf); + } + for f in &module.functions { + emit_bindings(&mut w, f, leaf); + emit_wrapper( + &mut w, + model, + docs, + f, + &DartDecl::TopLevel, + &dart_ident(&f.name), + ); + } + w.finish() +} + +/// Whether a module declares anything (a module with only submodules has +/// no part of its own). +fn declares_anything(m: &ModuleBinding) -> bool { + !(m.errors.is_empty() + && m.enums.is_empty() + && m.structs.is_empty() + && m.callback_interfaces.is_empty() + && m.interfaces.is_empty() + && m.functions.is_empty()) +} + +/// Render the library: `lib/{package}.dart` and its parts, as paths +/// relative to the package root. +fn render_library(model: &Model, package: &str, bundle: &Bundle) -> Vec<(Utf8PathBuf, String)> { + let mut parts: Vec<(Utf8PathBuf, String)> = Vec::new(); + for (file, body) in runtime_parts(model, package, bundle) { + parts.push((Utf8PathBuf::from(format!("src/runtime/{file}")), body)); + } + let mut codecs = CodeWriter::two_space(); + render_codecs(&mut codecs, model); + if !codecs.is_empty() { + let body = format!( + "// ── Composite codecs ──\n// One writer and reader per optional, list, or map type that crosses\n// inside a value buffer.\n{}", + codecs.finish() + ); + parts.push(("src/runtime/composites.dart".into(), body)); + } + + // Synchronous calls are leaf calls only when no callback interface + // exists, so no call can re-enter Dart. + let leaf = !model.has_callback_interfaces(); + let docs = Docs::new(model); + let reported = reported_domains(model); + let errors = error_tables(model, "Exception"); + for module in model.modules.iter().filter(|m| declares_anything(m)) { + let mine: Vec<&ErrorTable> = errors + .iter() + .filter(|t| t.module.index == module.index) + .collect(); + let body = render_module(model, &docs, module, &mine, &reported, leaf); + parts.push((module_part(module), body)); + } + + let mut files: Vec<(Utf8PathBuf, String)> = parts + .iter() + .map(|(rel, body)| { + ( + Utf8PathBuf::from(format!("lib/{rel}")), + part_file(package, rel, body), + ) + }) + .collect(); + let main = render_main(model, package, bundle, parts.iter().map(|(rel, _)| rel)); + files.insert(0, (Utf8PathBuf::from(format!("lib/{package}.dart")), main)); + files +} -/// Render `lib/{package}.dart`: imports, the runtime sections the API uses, -/// the composite codecs, then every module's declarations. -fn render_library(model: &Model, package: &str, bundle: &Bundle, file_name: &str) -> String { +/// `lib/{package}.dart`: the library's doc comment, its imports, and its +/// parts. +fn render_main<'a>( + model: &Model, + package: &str, + bundle: &Bundle, + parts: impl Iterator, +) -> String { let mut w = CodeWriter::two_space(); w.raw(render_prelude(CommentStyle::DoubleSlash)); - w.line("// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element"); + let identity = &model.identity; + if let Some(description) = identity.description.as_deref().map(str::trim) { + for line in description.lines() { + w.line(format!("/// {line}").trim_end()); + } + w.line("///"); + } + w.line(format!( + "/// `dart:ffi` bindings for the `{}` native library.", + identity.name + )); + w.line("///"); + w.line(format!( + "/// The library loads on first use (set `{}` to its path to pick a", + identity.library_env_var() + )); + w.line("/// build); a library that can't be loaded or doesn't match these"); + w.line("/// bindings throws a [NativeLibraryException]."); + w.line("library;"); w.blank(); let callbacks = model.has_callback_interfaces(); let bundled = matches!(bundle, Bundle::Packaged { .. }); @@ -115,110 +309,68 @@ fn render_library(model: &Model, package: &str, bundle: &Bundle, file_name: &str w.line("import 'dart:typed_data';"); w.blank(); w.line("import 'package:ffi/ffi.dart';"); - - render_runtime(&mut w, model, package, bundle); - render_codecs(&mut w, model); - - // Synchronous calls are leaf calls only when no callback interface - // exists, so no call can re-enter Dart. - let leaf = !callbacks; - let reported = reported_domains(model); - for module in &model.modules { - let exception = model - .error_domain(module) - .map(|e| dart_exception_name(&e.type_name)); - let exception = exception.as_deref(); - if let Some(e) = module.errors.as_ref() { - render_error(&mut w, module, e, reported.contains(&e.type_name)); - } - for e in &module.enums { - render_enum(&mut w, e); - } - for s in &module.structs { - render_struct(&mut w, s); - } - for cb in &module.callback_interfaces { - render_callback_interface(&mut w, cb, exception); - } - for i in &module.interfaces { - render_interface(&mut w, exception, i, leaf); - } - for f in &module.functions { - emit_bindings(&mut w, f, leaf); - emit_wrapper( - &mut w, - f, - &DartDecl::TopLevel, - &dart_ident(&f.name), - ErrCtx::of(f, exception), - ); - } + w.blank(); + for rel in parts { + w.line(format!("part '{}';", dart_str_literal(rel.as_str()))); } w.blank(); - w.raw(render_trailer(CommentStyle::DoubleSlash, file_name)); + w.raw(render_trailer( + CommentStyle::DoubleSlash, + &format!("{package}.dart"), + )); w.finish() } -/// The error domains (by type name) that a value-returning callback method -/// declared `throws` reports, which need an encoder for their fields. -fn reported_domains(model: &Model) -> BTreeSet { - model - .modules - .iter() - .filter(|m| { - m.callback_interfaces - .iter() - .flat_map(|cb| &cb.methods) - .any(|f| f.throws && f.ret.is_some()) - }) - .filter_map(|m| model.error_domain(m)) - .map(|e| e.type_name.clone()) - .collect() -} - -impl LanguageBackend for DartGenerator { - type Config = DartConfig; - +impl Target for DartGenerator { fn name(&self) -> &'static str { "dart" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { - let package = config.package_name(model); - let dir = out_dir.join("dart"); - let file = format!("{package}.dart"); - vec![ - OutputFile::new( - dir.join("lib").join(&file), - render_library(model, &package, &Bundle::None, &file), - ), - OutputFile::new( - dir.join("pubspec.yaml"), - render_pubspec(&model.identity, &package, &config.sdk), - ), - OutputFile::new( - dir.join("README.md"), - render_readme(&model.identity, &package, &config.sdk), - ), + fn fixed_files(&self) -> &'static [&'static str] { + &[ + "README.md", + "pubspec.yaml", + "loader.dart", + "core.dart", + "object.dart", + "codec.dart", + "arrays.dart", + "async.dart", + "cancel.dart", + "callbacks.dart", + "iterator.dart", ] } + fn render(&self, model: &Model) -> Vec { + let config = &self.config; + let package = config.package_name(model); + let mut files: Vec = render_library(model, &package, &Bundle::None) + .into_iter() + .map(|(path, contents)| OutputFile::new(path, contents)) + .collect(); + files.push(OutputFile::new( + "pubspec.yaml", + render_pubspec(&model.identity, &package, &config.sdk), + )); + files.push(OutputFile::new( + "README.md", + render_readme(&model.identity, &package, &config.sdk), + )); + files + } + /// A pub package directory at `dart/{package}/` with the desktop /// libraries under `native//`, which the loader tries (relative /// to the package, then to the working directory) before the system /// search path. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let natives = per_platform_libraries(ctx.binaries, "native", bundles_platform); if natives.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } let package = config.package_name(model); - let file = format!("{package}.dart"); let bundle = Bundle::Packaged { lib: &ctx.binaries.lib_name, platforms: ctx @@ -227,18 +379,19 @@ impl LanguageBackend for DartGenerator { .filter(|p| bundles_platform(*p)) .collect(), }; - let mut files = vec![ - PackagedFile::text( - format!("lib/{file}"), - render_library(model, &package, &bundle, &file), - ), - PackagedFile::text( - "pubspec.yaml", - render_pubspec(&model.identity, &package, &config.sdk), - ), - PackagedFile::text("README.md", render_packaged_readme(&model.identity, ctx)), - ]; + let mut files: Vec = render_library(model, &package, &bundle) + .into_iter() + .map(|(path, contents)| PackagedFile::text(path, contents)) + .collect(); + files.push(PackagedFile::text( + "pubspec.yaml", + render_pubspec(&model.identity, &package, &config.sdk), + )); + files.push(PackagedFile::text( + "README.md", + render_packaged_readme(&model.identity, ctx), + )); files.extend(natives); - Some(vec![Artifact::directory(format!("dart/{package}"), files)]) + Ok(vec![Artifact::directory(format!("dart/{package}"), files)]) } } diff --git a/crates/weaveffi-cli/src/targets/dart/package.rs b/crates/weaveffi-cli/src/targets/dart/package.rs index 335f0756..b39641e6 100644 --- a/crates/weaveffi-cli/src/targets/dart/package.rs +++ b/crates/weaveffi-cli/src/targets/dart/package.rs @@ -56,17 +56,19 @@ import 'package:{package}/{package}.dart'; The library loads `{macos}` (macOS), `{linux}` (Linux and Android), or `{windows}` (Windows) from the platform's search path on first use. Set `{env}` to a full path to load a specific build. On iOS, and as a macOS -fallback, the bindings look the symbols up in the running executable. +fallback, the bindings look the symbols up in the running executable. A +library that can't be loaded, or that doesn't match these bindings, throws a +`NativeLibraryException` from the first call; the next call tries again. ## Objects and callbacks Each interface wrapper holds one native reference: call `dispose()` when you're done, or let the garbage collector's finalizer release it. -Callback-interface methods that return a value run synchronously and must -be called by the native library on the isolate's thread during a call from -Dart; void methods may be called from any thread and are delivered on the -event loop. +Callback-interface methods that return a value run synchronously on the +isolate's thread, during a call from Dart; the native library can't call +one from another thread (that call fails without running the method). Void +methods may be called from any thread and are delivered on the event loop. ## Requirements diff --git a/crates/weaveffi-cli/src/targets/dart/runtime.rs b/crates/weaveffi-cli/src/targets/dart/runtime.rs index 3276a463..da2dda73 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime.rs +++ b/crates/weaveffi-cli/src/targets/dart/runtime.rs @@ -1,15 +1,18 @@ //! The fixed Dart runtime every generated library ships, kept as real Dart //! source under `runtime/` and spliced in with `{{PLACEHOLDER}}` -//! substitution: the loader and load-time checks, the error and memory -//! helpers, the object base class, the value-buffer codec, async and -//! cancellation glue, the callback-interface machinery, and the iterator -//! anchor. Sections an API doesn't use are left out. The contract tables the -//! loader checks are rendered from the model. +//! substitution: the loader and load-time checks, the error, call-frame, +//! and memory helpers, the object base class, the value-buffer codec, the +//! typed-array helpers, async and cancellation glue, the callback-interface +//! machinery, and the iterator anchor. Each section is one part file under +//! `lib/src/runtime/`, and sections an API doesn't use are left out. The +//! contract tables the loader checks are rendered from the model into +//! `contracts.dart`. +use crate::codegen::contract::{hex, tables}; use crate::codegen::CodeWriter; use crate::platform::{Arch, Os, Platform}; -use weaveffi_model::contract::entries; -use weaveffi_model::model::{contract_symbol, Model, ABI_VERSION}; +use weaveffi_model::model::{Model, ABI_VERSION}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; use crate::targets::dart::types::dart_str_literal; @@ -18,6 +21,7 @@ const BUNDLED: &str = include_str!("runtime/bundled.dart"); const CORE: &str = include_str!("runtime/core.dart"); const OBJECT: &str = include_str!("runtime/object.dart"); const CODEC: &str = include_str!("runtime/codec.dart"); +const ARRAYS: &str = include_str!("runtime/arrays.dart"); const ASYNC: &str = include_str!("runtime/async.dart"); const CANCEL: &str = include_str!("runtime/cancel.dart"); const CALLBACKS: &str = include_str!("runtime/callbacks.dart"); @@ -68,9 +72,39 @@ fn fill(template: &str, vars: &[(&str, &str)]) -> String { out } -/// Render the runtime sections `model` needs into `w`; `package` is the -/// Dart package name and `bundle` says where the loader looks first. -pub(crate) fn render_runtime(w: &mut CodeWriter, model: &Model, package: &str, bundle: &Bundle) { +/// Whether any value in `model` crosses as a typed array (the Slice +/// transport), in any position. +fn uses_arrays(model: &Model) -> bool { + let callables = model.callables().any(|(_, f)| { + f.params + .iter() + .any(|p| matches!(p.pass, ArgPass::Slice { .. })) + || matches!(f.ret_pass, RetPass::Slice { .. }) + || f.async_binding() + .is_some_and(|a| matches!(a.result, ResultPass::Slice { .. })) + || f.iterator() + .is_some_and(|it| matches!(it.item, ItemPass::Slice { .. })) + }); + let methods = model + .callback_interfaces() + .flat_map(|(_, cb)| &cb.methods) + .any(|m| { + m.params + .iter() + .any(|p| matches!(p.pass, ArgPass::Slice { .. })) + || matches!(m.ret_pass, CallbackRetPass::Slice { .. }) + }); + callables || methods +} + +/// The runtime sections `model` needs, as `(file name, source)` pairs in +/// library order; `package` is the Dart package name and `bundle` says +/// where the loader looks first. +pub(crate) fn runtime_parts( + model: &Model, + package: &str, + bundle: &Bundle, +) -> Vec<(String, String)> { let identity = &model.identity; let (macos, linux, windows) = identity.library_files(); let prefix = identity.prefix.as_str(); @@ -104,9 +138,10 @@ pub(crate) fn render_runtime(w: &mut CodeWriter, model: &Model, package: &str, b ("BUNDLED_LOOKUP", bundled_lookup), ("ABI_VERSION", abi.as_str()), ]); - w.blank(); - w.raw(fill(LOADER, &vars)); - render_contracts(w, model); + let mut parts = vec![ + ("loader.dart".to_string(), fill(LOADER, &vars)), + ("contracts.dart".to_string(), render_contracts(model)), + ]; if let Bundle::Packaged { lib, platforms } = bundle { let cases: String = platforms .iter() @@ -125,45 +160,47 @@ pub(crate) fn render_runtime(w: &mut CodeWriter, model: &Model, package: &str, b ("BUNDLED_CASES", cases.as_str()), ("PACKAGE", package.as_str()), ]); - w.raw(fill(BUNDLED, &vars)); + parts.push(("bundled.dart".into(), fill(BUNDLED, &vars))); } - let cancellable = model.callables().any(|(_, f)| f.cancellable); + let cancellable = model.callables().any(|(_, f)| f.cancellable()); let sections = [ - (true, CORE), - (model.has_interfaces(), OBJECT), - (model.has_buffers(), CODEC), - (model.has_async(), ASYNC), - (cancellable, CANCEL), - (model.has_callback_interfaces(), CALLBACKS), - (model.has_iterators(), ITERATOR), + (true, "core.dart", CORE), + (model.has_interfaces(), "object.dart", OBJECT), + (model.has_buffers(), "codec.dart", CODEC), + (uses_arrays(model), "arrays.dart", ARRAYS), + (model.has_async(), "async.dart", ASYNC), + (cancellable, "cancel.dart", CANCEL), + (model.has_callback_interfaces(), "callbacks.dart", CALLBACKS), + (model.has_iterators(), "iterator.dart", ITERATOR), ]; - for (_, source) in sections.iter().filter(|(used, _)| *used) { - w.blank(); - w.raw(fill(source, &common)); + for (_, file, source) in sections.iter().filter(|(used, ..)| *used) { + parts.push(((*file).to_string(), fill(source, &common))); } + parts } /// The contract tables the loader checks: for every top-level module, its -/// contract symbol and each declaration these bindings were generated with. -fn render_contracts(w: &mut CodeWriter, model: &Model) { +/// contract symbol and each declaration these bindings were generated with, +/// with its canonical signature as a comment. +fn render_contracts(model: &Model) -> String { + let mut w = CodeWriter::two_space(); + w.line("// ── Contract tables ──"); w.blank(); w.line("/// Each top-level module's contract-table symbol and the declarations"); w.line("/// these bindings were generated with: (id, signature hash, path)."); w.line("const List<(String, List<(int, int, String)>)> _contracts = ["); w.scope(|w| { - for root in model.roots() { - w.line(format!( - "('{}', [", - contract_symbol(model.prefix(), &root.name) - )); + for table in tables(model) { + w.line(format!("('{}', [", table.symbol)); w.scope(|w| { - for e in entries(model, root) { + for row in &table.rows { + w.line(format!("// {}", row.signature)); w.line(format!( - "(0x{:016x}, 0x{:016x}, '{}'),", - e.id, - e.hash, - dart_str_literal(&e.path) + "({}, {}, '{}'),", + hex(row.id), + hex(row.hash), + dart_str_literal(&row.path) )); } }); @@ -171,4 +208,5 @@ fn render_contracts(w: &mut CodeWriter, model: &Model) { } }); w.line("];"); + w.finish() } diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/arrays.dart b/crates/weaveffi-cli/src/targets/dart/runtime/arrays.dart new file mode 100644 index 00000000..4a626cd1 --- /dev/null +++ b/crates/weaveffi-cli/src/targets/dart/runtime/arrays.dart @@ -0,0 +1,171 @@ +// ── Typed arrays ── +// A numeric list (`[i32]`, `[f64]`, ...) crosses a call boundary as a C +// array and its element count, not as a value buffer. An argument is copied +// into arena memory for the call; a returned array is copied into a typed +// list (an `Int32List`, a `Float64List`, ...) and released with +// `{{PREFIX}}_free_bytes`; a callback's array argument is copied before the +// method runs; and a callback's returned array is handed over as a +// `{{PREFIX}}_alloc` run the producer adopts. A `u64` element travels as +// its two's-complement bit pattern, like every other `u64`. + +/// Copies [data]'s bytes into [arena] memory (8-aligned), or returns null +/// when it's empty. +Pointer _stageData(Arena arena, TypedData data) { + final n = data.lengthInBytes; + if (n == 0) return nullptr; + final ptr = arena(n); + ptr.asTypedList(n).setAll(0, Uint8List.sublistView(data)); + return ptr; +} + +/// Hands [count] elements of [data] to the producer through a callback's +/// `out_ptr`/`out_len` slots as a `{{PREFIX}}_alloc` run, which the producer +/// adopts. An empty array is null with count 0. +void _handOverData(TypedData data, int count, Pointer> outPtr, + Pointer outLen) { + final n = data.lengthInBytes; + final run = n == 0 ? nullptr : _alloc(n); + if (run != nullptr) run.asTypedList(n).setAll(0, Uint8List.sublistView(data)); + outPtr.value = run; + outLen.value = run == nullptr ? 0 : count; +} + +/// Releases an owned array of [count] elements of [size] bytes. +void _freeArray(Pointer ptr, int count, int size) { + if (count != 0) _freeBytes(ptr.cast(), count * size); +} + +Pointer _stageI8s(Arena a, List v) => + _stageData(a, v is Int8List ? v : Int8List.fromList(v)).cast(); +List _copyI8s(Pointer p, int n) => + n == 0 ? Int8List(0) : Int8List.fromList(p.asTypedList(n)); +List _takeI8s(Pointer p, int n) { + final v = _copyI8s(p, n); + _freeArray(p, n, 1); + return v; +} + +void _handOverI8s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Int8List ? v : Int8List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageI16s(Arena a, List v) => + _stageData(a, v is Int16List ? v : Int16List.fromList(v)).cast(); +List _copyI16s(Pointer p, int n) => + n == 0 ? Int16List(0) : Int16List.fromList(p.asTypedList(n)); +List _takeI16s(Pointer p, int n) { + final v = _copyI16s(p, n); + _freeArray(p, n, 2); + return v; +} + +void _handOverI16s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Int16List ? v : Int16List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageI32s(Arena a, List v) => + _stageData(a, v is Int32List ? v : Int32List.fromList(v)).cast(); +List _copyI32s(Pointer p, int n) => + n == 0 ? Int32List(0) : Int32List.fromList(p.asTypedList(n)); +List _takeI32s(Pointer p, int n) { + final v = _copyI32s(p, n); + _freeArray(p, n, 4); + return v; +} + +void _handOverI32s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Int32List ? v : Int32List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageI64s(Arena a, List v) => + _stageData(a, v is Int64List ? v : Int64List.fromList(v)).cast(); +List _copyI64s(Pointer p, int n) => + n == 0 ? Int64List(0) : Int64List.fromList(p.asTypedList(n)); +List _takeI64s(Pointer p, int n) { + final v = _copyI64s(p, n); + _freeArray(p, n, 8); + return v; +} + +void _handOverI64s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Int64List ? v : Int64List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageU16s(Arena a, List v) => + _stageData(a, v is Uint16List ? v : Uint16List.fromList(v)).cast(); +List _copyU16s(Pointer p, int n) => + n == 0 ? Uint16List(0) : Uint16List.fromList(p.asTypedList(n)); +List _takeU16s(Pointer p, int n) { + final v = _copyU16s(p, n); + _freeArray(p, n, 2); + return v; +} + +void _handOverU16s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Uint16List ? v : Uint16List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageU32s(Arena a, List v) => + _stageData(a, v is Uint32List ? v : Uint32List.fromList(v)).cast(); +List _copyU32s(Pointer p, int n) => + n == 0 ? Uint32List(0) : Uint32List.fromList(p.asTypedList(n)); +List _takeU32s(Pointer p, int n) { + final v = _copyU32s(p, n); + _freeArray(p, n, 4); + return v; +} + +void _handOverU32s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Uint32List ? v : Uint32List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageU64s(Arena a, List v) => + _stageData(a, v is Uint64List ? v : Uint64List.fromList(v)).cast(); +List _copyU64s(Pointer p, int n) => + n == 0 ? Uint64List(0) : Uint64List.fromList(p.asTypedList(n)); +List _takeU64s(Pointer p, int n) { + final v = _copyU64s(p, n); + _freeArray(p, n, 8); + return v; +} + +void _handOverU64s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Uint64List ? v : Uint64List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageF32s(Arena a, List v) => + _stageData(a, v is Float32List ? v : Float32List.fromList(v)).cast(); +List _copyF32s(Pointer p, int n) => + n == 0 ? Float32List(0) : Float32List.fromList(p.asTypedList(n)); +List _takeF32s(Pointer p, int n) { + final v = _copyF32s(p, n); + _freeArray(p, n, 4); + return v; +} + +void _handOverF32s( + List v, Pointer> outPtr, Pointer outLen) => + _handOverData(v is Float32List ? v : Float32List.fromList(v), v.length, + outPtr.cast(), outLen); + +Pointer _stageF64s(Arena a, List v) => + _stageData(a, v is Float64List ? v : Float64List.fromList(v)).cast(); +List _copyF64s(Pointer p, int n) => + n == 0 ? Float64List(0) : Float64List.fromList(p.asTypedList(n)); +List _takeF64s(Pointer p, int n) { + final v = _copyF64s(p, n); + _freeArray(p, n, 8); + return v; +} + +void _handOverF64s(List v, Pointer> outPtr, + Pointer outLen) => + _handOverData(v is Float64List ? v : Float64List.fromList(v), v.length, + outPtr.cast(), outLen); diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/async.dart b/crates/weaveffi-cli/src/targets/dart/runtime/async.dart index f0c63bf6..7107a922 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime/async.dart +++ b/crates/weaveffi-cli/src/targets/dart/runtime/async.dart @@ -10,7 +10,9 @@ final _errorFree = /// Builds the error a completion reports and frees its boxed error. Object _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { - final error = _readError(err, map); - _errorFree(err); - return error; + try { + return _readError(err, map); + } finally { + _errorFree(err); + } } diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/callbacks.dart b/crates/weaveffi-cli/src/targets/dart/runtime/callbacks.dart index 40b517a4..de570af5 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime/callbacks.dart +++ b/crates/weaveffi-cli/src/targets/dart/runtime/callbacks.dart @@ -5,17 +5,23 @@ // object and the GC never sees a raw pointer. The entry lives until the // producer calls the vtable's `free`. // -// Methods that return a value are `NativeCallable.isolateLocal` trampolines: -// they run synchronously, so the producer must call them on this isolate's -// thread while a call from Dart is in progress (the VM aborts the process -// otherwise, and an entry point that may run on any thread can't re-enter -// the isolate to run the method). Void methods and `free` are +// Methods that return a value are `NativeCallable.isolateLocal` +// trampolines: they run synchronously, so the producer may only call them +// on this isolate's thread while a call from Dart is in progress. Every +// vtable is flagged thread-affine (`{{PREFIX}}_VTABLE_THREAD_AFFINE`), so +// the producer refuses such a call from any other thread, failing the +// method with -4 ("callback called off its thread") instead of making it +// (the VM would abort the process). Void methods and `free` are // `NativeCallable.isolateGroupBound` forwarders, which may run on any // thread: they copy their arguments into a message for this isolate -// (borrowed bytes are copied before the forwarder returns) and the +// (borrowed runs are copied before the forwarder returns) and the // implementation runs later on the event loop, in the zone that passed the // callback. +/// Vtable `flags` bit 0: value-returning methods may only be called on the +/// thread that passed the vtable. +const int _vtableThreadAffine = 1; + /// Where forwarders send messages: the allocator they build a message with /// and the isolate port that receives it. Forwarders run outside the /// isolate and can't read Dart globals, so everything they need is native. @@ -150,12 +156,25 @@ final class _CallbackMessage { } static const int free = -1; + static const int _kNull = 0; static const int _kBool = 1; static const int _kInt64 = 3; static const int _kDouble = 4; static const int _kArray = 6; static const int _kTypedData = 7; - static const int _kUint8 = 2; + + // `Dart_TypedData_Type` values: a posted array arrives as the matching + // typed list (`Int32List`, `Float64List`, ...). + static const int typedInt8 = 1; + static const int typedUint8 = 2; + static const int typedInt16 = 4; + static const int typedUint16 = 5; + static const int typedInt32 = 6; + static const int typedUint32 = 7; + static const int typedInt64 = 8; + static const int typedUint64 = 9; + static const int typedFloat32 = 10; + static const int typedFloat64 = 11; final _CallbackContext _context; final int _capacity; @@ -177,13 +196,26 @@ final class _CallbackMessage { void pointer(Pointer v) => int64(v.address); + /// An absent optional: arrives as `null`. + void none() => _next(_kNull); + + void maybeBool(bool present, bool v) => present ? boolean(v) : none(); + + void maybeInt64(bool present, int v) => present ? int64(v) : none(); + + void maybeFloat64(bool present, double v) => present ? float64(v) : none(); + /// A borrowed byte run; posting copies it. - void bytes(Pointer ptr, int len) { + void bytes(Pointer ptr, int len) => typedData(ptr, len, typedUint8); + + /// A borrowed array of [count] elements of the typed-data [type]; posting + /// copies it. A run of length 0 is never read. + void typedData(Pointer ptr, int count, int type) { final data = _next(_kTypedData).value.asTypedData; data - ..type = _kUint8 - ..length = ptr == nullptr ? 0 : len - ..values = ptr; + ..type = type + ..length = count + ..values = count == 0 ? nullptr : ptr.cast(); } void send() { @@ -219,47 +251,44 @@ Pointer> _pin(NativeCallable callable) } final _errorSet = _lib.lookupFunction< - Void Function(Pointer<_Error>, Int32, Pointer), - void Function(Pointer<_Error>, int, Pointer)>('{{PREFIX}}_error_set'); + Void Function(Pointer<_Error>, Int32, Pointer, Size), + void Function( + Pointer<_Error>, int, Pointer, int)>('{{PREFIX}}_error_set'); final _errorSetPayload = _lib.lookupFunction< Void Function(Pointer<_Error>, Pointer, Size), void Function( Pointer<_Error>, Pointer, int)>('{{PREFIX}}_error_set_payload'); -final _alloc = _lib.lookupFunction Function(Size), - Pointer Function(int)>('{{PREFIX}}_alloc', isLeaf: true); - /// Reports a failed callback method to the producer through [outErr]: /// `{{PREFIX}}_error_set` copies [message], and `{{PREFIX}}_error_set_payload` -/// copies a domain code's encoded fields. Never throws: it runs in the catch -/// path of a trampoline an exception must not unwind through. +/// copies a domain code's encoded fields. Runs in the catch path of a +/// trampoline, which an exception must never unwind through. void _failCallback( Pointer<_Error> outErr, int code, String message, Uint8List? payload) { if (outErr == nullptr) return; - final text = message.toNativeUtf8(allocator: calloc); - try { - _errorSet(outErr, code, text); - } finally { - calloc.free(text); - } - if (payload == null || payload.isEmpty) return; - final bytes = calloc(payload.length); + final text = utf8.encode(message); + final scratch = calloc(text.length + (payload?.length ?? 0) + 1); try { - bytes.asTypedList(payload.length).setAll(0, payload); - _errorSetPayload(outErr, bytes, payload.length); + scratch.asTypedList(text.length).setAll(0, text); + _errorSet(outErr, code, scratch, text.length); + if (payload == null || payload.isEmpty) return; + scratch.asTypedList(payload.length).setAll(0, payload); + _errorSetPayload(outErr, scratch, payload.length); } finally { - calloc.free(bytes); + calloc.free(scratch); } } -/// Reports an implementation's exception that isn't a domain error of the -/// method with the foreign code (-4) and the exception's text. -void _foreignError(Pointer<_Error> outErr, Object error) { +/// Reports [error], an exception that isn't a domain error the method +/// declares, with [code] and the exception's text (a [NativeException]'s +/// message). +void _reportCallbackError(Pointer<_Error> outErr, int code, Object error) { try { - _failCallback(outErr, NativeException.foreignCode, '$error', null); + final message = error is NativeException ? error.message : '$error'; + _failCallback(outErr, code, message, null); } catch (_) { - _errorSet(outErr, NativeException.foreignCode, nullptr); + _errorSet(outErr, code, nullptr, 0); } } @@ -271,5 +300,5 @@ void _handOver( final run = bytes.isEmpty ? nullptr : _alloc(bytes.length); if (run != nullptr) run.asTypedList(bytes.length).setAll(0, bytes); outPtr.value = run; - outLen.value = bytes.length; + outLen.value = run == nullptr ? 0 : bytes.length; } diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/codec.dart b/crates/weaveffi-cli/src/targets/dart/runtime/codec.dart index b3d95da5..a51ac562 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime/codec.dart +++ b/crates/weaveffi-cli/src/targets/dart/runtime/codec.dart @@ -1,8 +1,8 @@ // ── Value buffers ── -// Records, rich enums, optionals, lists, maps, and error payloads cross the -// C ABI serialized in this little-endian, packed format. A malformed buffer -// is a contract violation between producer and bindings, never a domain -// error. +// Records, rich enums, maps, error payloads, and every optional or list that +// isn't a scalar optional or a numeric array at a call boundary cross the C +// ABI serialized in this little-endian, packed format. A malformed buffer is +// a contract violation between producer and bindings, never a domain error. Never _bufferError(String context) => throw StateError('malformed value buffer: $context'); diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/core.dart b/crates/weaveffi-cli/src/targets/dart/runtime/core.dart index faf6d464..4f03d1bd 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime/core.dart +++ b/crates/weaveffi-cli/src/targets/dart/runtime/core.dart @@ -1,12 +1,15 @@ -// ── Errors and memory ── +// ── Errors, call frames, and memory ── /// The C `{{PREFIX}}_error` slot every fallible call writes. A non-zero -/// `code` carries a producer-owned `message` and, for a domain error with -/// fields, a value-buffer `payload`; `{{PREFIX}}_error_clear` releases both. +/// `code` carries a producer-owned UTF-8 message run (not NUL-terminated; +/// null with length 0 is the empty message) and, for a domain error with +/// fields, a value-buffer payload; `{{PREFIX}}_error_clear` releases both. final class _Error extends Struct { @Int32() external int code; - external Pointer message; + external Pointer messagePtr; + @Size() + external int messageLen; external Pointer payloadPtr; @Size() external int payloadLen; @@ -18,11 +21,60 @@ final _errorClear = _lib.lookupFunction), final _freeBytes = _lib.lookupFunction, Size), void Function(Pointer, int)>('{{PREFIX}}_free_bytes', isLeaf: true); -// Synchronous calls on this isolate share one error slot and one length -// slot: each call reads (and clears) them right after it returns, before any -// other call can reuse them, including a nested call from a callback. -final Pointer<_Error> _err = calloc<_Error>(); -final Pointer _outLen = calloc(); +/// `{{PREFIX}}_alloc`: an 8-aligned, zero-filled run the producer adopts +/// (and frees), for what a callback hands back. +final _alloc = _lib.lookupFunction Function(Size), + Pointer Function(int)>('{{PREFIX}}_alloc', isLeaf: true); + +/// The native scratch of one call: its `{{PREFIX}}_error` slot, two 8-byte +/// out slots for what it returns (an `out_len`, an `out_value`, an +/// iterator's `out_item`), and an [Arena] for the arguments it stages. +/// +/// Every call takes its own frame and gives it back when it returns, so a +/// call made from a callback while another call is in progress never +/// touches the outer call's slots. Frames are pooled per isolate, so a call +/// allocates nothing in the steady state; a pooled frame's memory is freed +/// when the isolate goes away. +final class _Frame implements Finalizable { + _Frame._() : _base = calloc(_size) { + _finalizer.attach(this, _base.cast()); + } + + static final _finalizer = NativeFinalizer(calloc.nativeFree); + + /// The error slot, rounded up so the out slots are 8-aligned. + static final int _errorSize = (sizeOf<_Error>() + 7) & ~7; + static final int _size = _errorSize + 16; + static final List<_Frame> _pool = <_Frame>[]; + + final Pointer _base; + Arena? _arena; + + /// A frame for one call: a pooled one, or a new one. + static _Frame take() => _pool.isEmpty ? _Frame._() : _pool.removeLast(); + + /// The call's `out_err` slot. + Pointer<_Error> get err => _base.cast(); + + /// The first out slot; `cast` it to the slot's C type. + Pointer get slot0 => (_base + _errorSize).cast(); + + /// The second out slot; `cast` it to the slot's C type. + Pointer get slot1 => (_base + _errorSize + 8).cast(); + + /// The arena staged arguments live in until the call returns. + Arena get arena => _arena ??= Arena(); + + /// Throws the error the call left in [err], clearing the slot; [map] is + /// the callable's error mapper. + void check([_ErrorMapper map = _trap]) => _check(err, map); + + /// Releases what the call staged and returns the frame to the pool. + void release() { + _arena?.releaseAll(reuse: true); + _pool.add(this); + } +} /// A failure the native library reported to a function declared to throw. /// Domain errors extend this class with a positive [code]; a negative code is @@ -31,7 +83,7 @@ class NativeException implements Exception { /// Creates an exception carrying [code] and [message]. NativeException(this.code, this.message); - /// The producer reported an untyped error. + /// The producer reported an untyped error (a `throws any` function). static const int genericCode = -1; /// The producer panicked; [message] carries the panic text. @@ -92,9 +144,11 @@ Object _trap(int code, String message, Uint8List payload) => ? CancelledException(message) : NativeError(code, message); -/// Maps a runtime code (or one the domain doesn't declare) onto its -/// exception, for a function declared to throw. -NativeException _runtimeException(int code, String message, Uint8List payload) => +/// The mapper of a `throws any` function, and the fallback of a domain's +/// mapper for runtime codes: a [NativeException] (a [CancelledException] +/// for cancellation). +NativeException _runtimeException( + int code, String message, Uint8List payload) => code == NativeException.cancelledCode ? CancelledException(message) : NativeException(code, message); @@ -102,26 +156,31 @@ NativeException _runtimeException(int code, String message, Uint8List payload) = /// Builds the error [err] reports, using [map]. Object _readError(Pointer<_Error> err, _ErrorMapper map) { final e = err.ref; - final message = e.message == nullptr ? '' : e.message.toDartString(); + final message = _readString(e.messagePtr, e.messageLen); return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); } -/// Throws the error a synchronous call left in [err], clearing the slot. -/// [map] is the domain's mapper for a function declared to throw. +/// Throws the error a synchronous call left in [err], clearing the slot +/// whether or not the error decodes. [map] is the callable's error mapper. void _check(Pointer<_Error> err, [_ErrorMapper map = _trap]) { if (err.ref.code == 0) return; - final error = _readError(err, map); - _errorClear(err); + final Object error; + try { + error = _readError(err, map); + } finally { + _errorClear(err); + } throw error; } -/// Copies a borrowed native byte run into Dart memory. +/// Copies a borrowed native byte run into Dart memory. A run of length 0 +/// is never read (its pointer may dangle). Uint8List _copyBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + len == 0 ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); /// Copies an owned native byte run and releases it. Uint8List _takeBytes(Pointer ptr, int len) { - if (ptr == nullptr) return Uint8List(0); + if (len == 0) return Uint8List(0); final bytes = Uint8List.fromList(ptr.asTypedList(len)); _freeBytes(ptr, len); return bytes; @@ -129,15 +188,16 @@ Uint8List _takeBytes(Pointer ptr, int len) { /// Decodes an owned UTF-8 run and releases it. String _takeString(Pointer ptr, int len) { - if (ptr == nullptr) return ''; - final text = utf8.decode(ptr.asTypedList(len)); + if (len == 0) return ''; + final text = utf8.decode(ptr.asTypedList(len), allowMalformed: true); _freeBytes(ptr, len); return text; } /// Decodes a borrowed UTF-8 run. -String _readString(Pointer ptr, int len) => - ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); +String _readString(Pointer ptr, int len) => len == 0 + ? '' + : utf8.decode(ptr.asTypedList(len), allowMalformed: true); /// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. Pointer _stage(Arena arena, List bytes) { diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/iterator.dart b/crates/weaveffi-cli/src/targets/dart/runtime/iterator.dart index 1c77bbfa..a2ed55f8 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime/iterator.dart +++ b/crates/weaveffi-cli/src/targets/dart/runtime/iterator.dart @@ -7,5 +7,3 @@ /// eagerly instead, so the iterator is destroyed exactly once either way. final class _IteratorAnchor implements Finalizable {} -/// The `out_item` slot iterator steps write, wide enough for any element. -final Pointer _outItem = calloc(); diff --git a/crates/weaveffi-cli/src/targets/dart/runtime/loader.dart b/crates/weaveffi-cli/src/targets/dart/runtime/loader.dart index 81e4921b..3dbfc9c9 100644 --- a/crates/weaveffi-cli/src/targets/dart/runtime/loader.dart +++ b/crates/weaveffi-cli/src/targets/dart/runtime/loader.dart @@ -3,21 +3,25 @@ // lookup: it must implement the C ABI revision these bindings were generated // against and carry every declaration they were generated with, unchanged. -/// The native library every binding in this file calls. +/// The native library every binding in this package calls. A failed load +/// throws from whichever call came first and is tried again on the next one. final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); /// The native library couldn't be loaded, or it doesn't match these /// bindings: a different C ABI revision, or a declaration that's missing or /// changed. Regenerate the bindings from the library you ship. -final class NativeLibraryError extends Error { +/// +/// It's thrown by the first call into the library, and the next call tries +/// to load it again, so an app can catch it, report it, and carry on. +final class NativeLibraryException implements Exception { /// Creates a load failure described by [message]. - NativeLibraryError(this.message); + NativeLibraryException(this.message); /// What failed, naming the library or declaration. final String message; @override - String toString() => 'NativeLibraryError: $message'; + String toString() => 'NativeLibraryException: $message'; } /// The platform file name of the library on the system search path. @@ -40,7 +44,7 @@ DynamicLibrary _openLibrary() { } on ArgumentError catch (e) { // A macOS app may link the library into its executable instead. if (Platform.isMacOS) return DynamicLibrary.process(); - throw NativeLibraryError("can't open '$_libraryFile' ($e); set " + throw NativeLibraryException("can't open '$_libraryFile' ($e); set " '{{LIBRARY_ENV}} to its path'); } } @@ -49,7 +53,7 @@ DynamicLibrary _open(String path) { try { return DynamicLibrary.open(path); } on ArgumentError catch (e) { - throw NativeLibraryError("can't open '$path': $e"); + throw NativeLibraryException("can't open '$path': $e"); } } @@ -70,14 +74,15 @@ DynamicLibrary _verifyLibrary(DynamicLibrary lib) { abiVersion = lib.lookupFunction( '{{PREFIX}}_abi_version'); } on ArgumentError { - throw NativeLibraryError( + throw NativeLibraryException( "the native library '{{LIBRARY}}' isn't loaded; set {{LIBRARY_ENV}} " 'to its path'); } final found = abiVersion(); if (found != _abiVersion) { - throw NativeLibraryError('{{NAME}}: these bindings expect C ABI revision ' - '$_abiVersion, but the loaded library implements revision $found'); + throw NativeLibraryException('{{NAME}}: these bindings expect C ABI ' + 'revision $_abiVersion, but the loaded library implements revision ' + '$found'); } for (final (symbol, expected) in _contracts) { _checkContract(lib, symbol, expected); @@ -87,7 +92,8 @@ DynamicLibrary _verifyLibrary(DynamicLibrary lib) { /// Checks that every `(id, hash, path)` entry in [expected] is in the table /// [symbol] returns with an equal hash. Entries only the library has are -/// fine: they're declarations added after these bindings were generated. +/// fine: they're declarations (or error codes, or callback methods) added +/// after these bindings were generated. void _checkContract( DynamicLibrary lib, String symbol, List<(int, int, String)> expected) { final Pointer<_ContractEntry> Function(Pointer) table; @@ -95,7 +101,7 @@ void _checkContract( table = lib.lookupFunction Function(Pointer), Pointer<_ContractEntry> Function(Pointer)>(symbol); } on ArgumentError { - throw NativeLibraryError( + throw NativeLibraryException( "{{NAME}}: the loaded library doesn't export '$symbol'"); } final hashes = {}; @@ -111,10 +117,11 @@ void _checkContract( for (final (id, hash, path) in expected) { final found = hashes[id]; if (found == null) { - throw NativeLibraryError('{{NAME}}: $path is missing from the library'); + throw NativeLibraryException( + '{{NAME}}: $path is missing from the library'); } if (found != hash) { - throw NativeLibraryError( + throw NativeLibraryException( '{{NAME}}: $path changed since these bindings were generated'); } } diff --git a/crates/weaveffi-cli/src/targets/dart/tests.rs b/crates/weaveffi-cli/src/targets/dart/tests.rs index bf607172..7c4880b0 100644 --- a/crates/weaveffi-cli/src/targets/dart/tests.rs +++ b/crates/weaveffi-cli/src/targets/dart/tests.rs @@ -1,112 +1,92 @@ -//! Unit tests: render a small API that exercises every C ABI revision 4 -//! shape the Dart bindings distinguish and assert the key pieces of each -//! contract. +//! Unit tests for what's specific to the Dart target: package naming and +//! layout, identifier escaping, leaf calls, and the packaged loader. The +//! `kitchen_sink` snapshot pins the rendered bindings themselves. -use camino::Utf8Path; -use weaveffi_model::contract::entries; use weaveffi_model::ir::Api; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; use weaveffi_model::validate::validate; -use crate::backend::LanguageBackend; +use crate::codegen::OutputFile; use crate::package::{FileContent, PackageContext}; use crate::platform::{BinarySet, NativeBinary, Platform}; +use crate::targets::Target; use super::{DartConfig, DartGenerator, MIN_DART_SDK}; -/// One module with an error domain carrying a field; a C-style enum; a -/// record carrying objects; an interface with a `new` constructor, a -/// method, and a throwing static; nullable objects; an iterator of objects; -/// a cancellable async function; composites; and a callback interface whose -/// methods take every argument family and return a direct value, an enum, a -/// string, a record, an object, and an optional object, one of them -/// `throws`, plus a void method, passed both required and optional. +/// Two root modules (one named `runtime`, with a child), an error domain +/// and a callback interface, and names that collide with Dart keywords, +/// the runtime's types, and the wrapper's members. const FIXTURE: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: shop errors: - name: ShopError - codes: - - { name: OutOfStock, code: 1, message: "out of stock", fields: [{ name: sku, type: string }] } - - { name: Closed, code: 2, message: "closed" } - enums: - - name: Mood - variants: [{ name: Happy, value: 0 }, { name: Grumpy, value: 1 }] + - name: Native + codes: [{ name: Closed, code: 1, message: "closed" }] structs: - - name: Order - fields: - - { name: cart, type: Cart } - - { name: history, type: "[Cart]" } - - { name: note, type: "string?" } + - name: Int32List + fields: [{ name: class, type: i32 }] interfaces: - name: Cart - constructors: - - { name: new, params: [{ name: owner, type: string }] } methods: - - { name: add, params: [{ name: sku, type: string }], return: bool } - statics: - - { name: restore, params: [{ name: id, type: i64 }], return: Cart, throws: true } + - { name: dispose, return: bool } callback_interfaces: - name: Watcher methods: - - name: on_event - params: - - { name: name, type: string } - - { name: count, type: i32 } - - { name: order, type: Order } - - { name: cart, type: Cart } - return: bool - - { name: mood, return: Mood } - - { name: label, return: string, throws: true } - - { name: latest, return: "Order?" } - - { name: favorite, return: Cart } - - { name: maybe, return: "Cart?" } - - { name: on_done, params: [{ name: summary, type: string }, { name: cart, type: "Cart?" }] } + - { name: seen, params: [{ name: count, type: i32 }], return: bool } functions: - - { name: find_cart, params: [{ name: current, type: "Cart?" }], return: "Cart?" } - - { name: all_carts, return: "iter" } - { name: watch, params: [{ name: watcher, type: Watcher }] } - - { name: maybe_watch, params: [{ name: watcher, type: "Watcher?" }] } - - { name: echo, params: [{ name: text, type: string }], return: string } - - { name: tally, params: [{ name: counts, type: "{string:[i32?]}" }], return: "[Order]" } - - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } - - { name: checkout, params: [{ name: order, type: Order }], return: Order, throws: true } + - name: wait + params: [{ name: cancelToken, type: i64 }] + return: i64 + async: true + cancellable: true + throws: Native + - name: runtime + functions: + - { name: ping, return: i32 } + modules: + - name: loader + functions: + - { name: pong, return: i32 } "#; +fn model_of(yaml: &str, name: &str) -> Model { + let api: Api = serde_saphyr::from_str(yaml).unwrap(); + validate(&api, &Identity::named(name), None).unwrap() +} + fn model() -> Model { - let api: Api = serde_yaml::from_str(FIXTURE).unwrap(); - validate(&api, &Identity::named("shop_kit"), None).unwrap() + model_of(FIXTURE, "shop_kit") } -fn render() -> String { - let files = DartGenerator.files(&model(), Utf8Path::new("out"), &DartConfig::default()); - let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); - assert_eq!( - paths, - [ - "out/dart/lib/shop_kit.dart", - "out/dart/pubspec.yaml", - "out/dart/README.md" - ] - ); - files[0].contents.clone() +fn render_with(config: DartConfig) -> Vec { + DartGenerator::from(config).render(&model()) +} + +fn file<'a>(files: &'a [OutputFile], path: &str) -> &'a str { + &files + .iter() + .find(|f| f.path.as_str() == path) + .unwrap_or_else(|| panic!("no {path}")) + .contents } fn assert_has(out: &str, needle: &str) { assert!(out.contains(needle), "missing `{needle}` in:\n{out}"); } -/// The packaged library of the fixture, bundling every platform. -fn packaged() -> String { +/// The packaged library of the fixture, bundling every platform, as +/// `(path, text)` pairs. +fn packaged() -> Vec<(String, String)> { let mut binaries = BinarySet::new("shop_kit"); for p in Platform::ALL { binaries.insert(NativeBinary::new(p, format!("/prebuilt/{}/lib", p.id()))); } let ctx = PackageContext::new(&binaries); - let artifacts = DartGenerator - .package(&model(), &ctx, &DartConfig::default()) + let artifacts = DartGenerator::from(DartConfig::default()) + .package(&model(), &ctx) .expect("dart packages"); assert_eq!(artifacts.len(), 1); assert_eq!(artifacts[0].path, "dart/shop_kit"); @@ -130,428 +110,211 @@ fn packaged() -> String { } files .iter() - .find(|f| f.path.as_str() == "lib/shop_kit.dart") - .and_then(|f| match &f.content { - FileContent::Text(s) => Some(s.clone()), + .filter_map(|f| match &f.content { + FileContent::Text(s) => Some((f.path.to_string(), s.clone())), _ => None, }) - .expect("packaged source") + .collect() } #[test] fn names_come_from_the_identity() { - let model = model(); - let files = DartGenerator.files(&model, Utf8Path::new("out"), &DartConfig::default()); - assert_has(&files[1].contents, "name: shop_kit\n"); + let files = render_with(DartConfig::default()); + assert_has(file(&files, "pubspec.yaml"), "name: shop_kit\n"); assert_has( - &files[2].contents, + file(&files, "README.md"), "import 'package:shop_kit/shop_kit.dart';", ); + let loader = file(&files, "lib/src/runtime/loader.dart"); + assert_has(loader, "Platform.environment['SHOP_KIT_LIBRARY']"); + assert_has(loader, "? 'libshop_kit.dylib'"); + assert_has(loader, "? 'shop_kit.dll'"); + assert_has(loader, ": 'libshop_kit.so';"); + assert!( + !loader.contains("_openBundled"), + "unpackaged bindings look for bundles" + ); + for f in &files { + assert!( + !f.contents.to_lowercase().contains("weaveffi_"), + "branded symbol in {}", + f.path + ); + } let config = DartConfig { name: Some("custom".into()), ..DartConfig::default() }; - let files = DartGenerator.files(&model, Utf8Path::new("out"), &config); - assert_eq!(files[0].path.as_str(), "out/dart/lib/custom.dart"); - assert_has(&files[1].contents, "name: custom\n"); - - let out = render(); - assert_has(&out, "Platform.environment['SHOP_KIT_LIBRARY']"); - assert_has(&out, "? 'libshop_kit.dylib'"); - assert_has(&out, "? 'shop_kit.dll'"); - assert_has(&out, ": 'libshop_kit.so';"); - assert_has(&out, "if (Platform.isIOS) return DynamicLibrary.process();"); - assert!( - !out.contains("_openBundled"), - "unpackaged bindings look for bundles" + let files = render_with(config); + assert_has(file(&files, "pubspec.yaml"), "name: custom\n"); + assert_has(file(&files, "lib/custom.dart"), "part 'src/shop.dart';"); + assert_has( + file(&files, "lib/src/shop.dart"), + "part of '../custom.dart';", ); - assert!(!out.to_lowercase().contains("weaveffi_"), "branded symbol"); } #[test] fn pubspec_states_the_minimum_sdk() { - let files = DartGenerator.files(&model(), Utf8Path::new("out"), &DartConfig::default()); + let files = render_with(DartConfig::default()); assert_has( - &files[1].contents, + file(&files, "pubspec.yaml"), &format!("sdk: '>={MIN_DART_SDK} <4.0.0'"), ); assert_has( - &files[2].contents, + file(&files, "README.md"), &format!("Dart SDK `>={MIN_DART_SDK} <4.0.0`"), ); } #[test] -fn load_checks_embed_every_contract_entry() { - let model = model(); - let out = render(); - assert_has(&out, "const int _abiVersion = 4;"); - assert_has(&out, "'shop_kit_abi_version'"); - assert_has(&out, " ('shop_kit_shop_contract', [\n"); - let root = model.roots().next().expect("one root"); - let expected = entries(&model, root); - assert!(expected.len() > 10, "every declaration has an entry"); - for e in expected { +fn one_library_with_a_part_per_module() { + let files = render_with(DartConfig::default()); + let main = file(&files, "lib/shop_kit.dart"); + let parts: Vec<&str> = main + .lines() + .filter_map(|l| l.strip_prefix("part '")?.strip_suffix("';")) + .collect(); + assert_eq!( + parts, + [ + "src/runtime/loader.dart", + "src/runtime/contracts.dart", + "src/runtime/core.dart", + "src/runtime/object.dart", + "src/runtime/codec.dart", + "src/runtime/async.dart", + "src/runtime/cancel.dart", + "src/runtime/callbacks.dart", + "src/shop.dart", + // A root module named `runtime` can't land among the runtime + // sections. + "src/runtime_.dart", + "src/runtime_/loader.dart", + ] + ); + let mut rendered: Vec<&str> = files + .iter() + .map(|f| f.path.as_str()) + .filter(|p| p.starts_with("lib/src/")) + .collect(); + rendered.sort_unstable(); + let mut declared: Vec = parts.iter().map(|p| format!("lib/{p}")).collect(); + declared.sort(); + assert_eq!(rendered, declared); + for part in &parts { + let up = "../".repeat(part.matches('/').count()); assert_has( - &out, - &format!(" (0x{:016x}, 0x{:016x}, '{}'),\n", e.id, e.hash, e.path), + file(&files, &format!("lib/{part}")), + &format!("\npart of '{up}shop_kit.dart';\n"), ); } - assert_has(&out, "$path is missing from the library"); - assert_has(&out, "$path changed since these bindings were generated"); - assert_has(&out, "final class NativeLibraryError extends Error {"); - assert!( - !out.contains("checksum"), - "the revision 3 checksum survives" - ); -} - -#[test] -fn strings_cross_as_pointer_and_length() { - let out = render(); - assert_has( - &out, - "Pointer Function(Pointer, Size, Pointer, Pointer<_Error>)", - ); - assert_has(&out, "final _textBytes = utf8.encode(text);"); - assert_has( - &out, - "_shopKitShopEcho(_stage(_arena, _textBytes), _textBytes.length, _outLen, _err)", - ); - assert_has(&out, "return _takeString(_result, _outLen.value);"); - assert_has(&out, "'shop_kit_free_bytes'"); - assert!(!out.contains("dealloc"), "the revision 3 dealloc survives"); -} - -#[test] -fn composites_share_one_codec_per_type() { - let out = render(); - let name = "_packMapOfStringToListOfOptionalOfI32"; - assert_eq!(out.matches(&format!("void {name}(")).count(), 1); - assert_has( - &out, - " w.writeMap(v, (k) => w.writeString(k), (e) => _packListOfOptionalOfI32(w, e));", - ); - assert_has( - &out, - &format!("final _countsBytes = _encode(counts, {name});"), - ); - assert_has( - &out, - "return _decode(_takeBytes(_result, _outLen.value), _unpackListOfOrder);", - ); - assert_has(&out, " _packListOfCart(w, v.history);"); - assert_has(&out, " history: _unpackListOfCart(r),"); - assert!( - !out.contains("for (var i = r."), - "a call site inlines a loop" - ); -} - -#[test] -fn records_compare_by_value() { - let out = render(); - assert_has(&out, "final class Order {"); - assert_has(&out, " _deepEquals(history, other.history) &&"); - assert_has(&out, " note == other.note;"); - assert_has( - &out, - "int get hashCode => Object.hashAll([Order, cart, _deepHash(history), note]);", - ); - assert_has( - &out, - "String toString() => 'Order(cart: $cart, history: $history, note: $note)';", - ); -} - -#[test] -fn interface_class_owns_one_reference() { - let out = render(); - assert_has(&out, "final class Cart extends _NativeObject {"); - assert_has(&out, "('shop_kit_shop_Cart_clone')"); - assert_has(&out, "('shop_kit_shop_Cart_destroy')"); - assert_has( - &out, - "NativeFinalizer get _finalizer => _shopKitShopCartDestroyFinalizer;", - ); - // A dispose during an in-flight call defers the release. - assert_has(&out, "if (_calls == 0) _release();"); - assert_has(&out, "if (--_calls == 0 && _disposed) _release();"); - assert_has(&out, "factory Cart(String owner) {"); - assert_has(&out, "return Cart._(_result);"); - assert_has(&out, "final _self = _enter();"); - assert_has(&out, "static Cart restore(int id) {"); -} - -#[test] -fn throwing_calls_map_the_domain_and_others_trap() { - let out = render(); - assert_has(&out, "sealed class ShopException extends NativeException {"); - assert_has( - &out, - "final class OutOfStockException extends ShopException {", - ); - assert_has( - &out, - "return _decode(payload, (r) => OutOfStockException(r.readString(), message));", - ); - assert_has(&out, "return _runtimeException(code, message, payload);"); - // `restore` throws; `echo` can't, so a failure there is a trap. - let restore = &out[out.find("static Cart restore").unwrap()..]; - assert_has(&restore[..300], "_check(_err, _mapShopException);"); - let echo = &out[out.find("String echo(String text)").unwrap()..]; - assert_has(&echo[..300], "_check(_err);"); - assert_has(&out, "final class NativeError extends Error {"); - assert_has(&out, ": NativeError(code, message);"); -} - -#[test] -fn nullable_objects_are_borrowed_and_adopted() { - let out = render(); - assert_has(&out, "Cart? findCart(Cart? current) {"); - assert_has( - &out, - "final _currentPtr = current == null ? nullptr : _borrow(_arena, current);", - ); - assert_has(&out, "return _result == nullptr ? null : Cart._(_result);"); -} - -#[test] -fn objects_inside_records_are_cloned_tokens() { - let out = render(); - assert_has(&out, "w.writeU64(v.cart._cloneRef().address);"); - assert_has( - &out, - "cart: Cart._(Pointer.fromAddress(r.readU64())),", - ); + assert_has(file(&files, "lib/src/runtime_/loader.dart"), "int pong() {"); } #[test] -fn object_iterator_adopts_each_element() { - let out = render(); - assert_has(&out, "Iterable allCarts() sync* {"); - assert_has( - &out, - "_shopKitShopAllCartsIteratorNext(_iter, _outItem.cast(), _err)", - ); - assert_has(&out, "yield Cart._(_outItem.cast>().value);"); - assert_has( - &out, - "_shopKitShopAllCartsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor);", - ); - assert_has(&out, "_shopKitShopAllCartsIteratorDestroy(_iter);"); -} - -#[test] -fn cancellable_async_binds_a_native_token() { - let out = render(); - assert_has( - &out, - "Future wait(int ms, {CancelToken? cancelToken}) {", - ); - assert_has(&out, "final _cancel = _NativeCancel.bind(cancelToken);"); - assert_has(&out, "NativeCallable<_ShopKitShopWaitCallback>.listener(("); - assert_has( - &out, - "_shopKitShopWait(ms, _cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr);", - ); - assert_has( - &out, - "if (error != nullptr) throw _takeAsyncError(error, _trap);", - ); - assert_has(&out, "'shop_kit_cancel_token_create'"); - assert_has(&out, "class CancelledException extends NativeException {"); -} - -#[test] -fn callback_vtable_starts_with_the_header() { - let out = render(); - assert_has( - &out, - "final class _WatcherVtable extends Struct {\n @Uint32()\n external int size;\n \ - @Uint32()\n external int flags;\n \ - external Pointer)>> free;\n \ - external Pointer> onEvent;", - ); - assert_has( - &out, - " ..size = sizeOf<_WatcherVtable>()\n ..flags = 0\n ..free = _callbackFree\n", - ); -} - -#[test] -fn callback_arguments_are_copied_or_adopted() { - let out = render(); - // The object first, then the buffer (which may carry tokens). - assert_has( - &out, - " final _cart = Cart._(cart);\n \ - final _order = _decode(_copyBytes(orderPtr, orderLen), _unpackOrder);\n \ - return (_callbackTarget(ctx) as Watcher).onEvent(_readString(namePtr, nameLen), count, _order, _cart);", - ); - assert_has( - &out, - " } catch (e) {\n _foreignError(outErr, e);\n }\n return false;", - ); -} - -#[test] -fn callback_returns_cover_every_family() { - let out = render(); - assert_has( - &out, - "return (_callbackTarget(ctx) as Watcher).mood().value;", - ); - assert_has(&out, "_handOver(utf8.encode(result), outPtr, outLen);"); - assert_has( - &out, - "_handOver(_encode(result, _packOptionalOfOrder), outPtr, outLen);", - ); - assert_has(&out, " return result._cloneRef();\n"); - assert_has(&out, " return result?._cloneRef() ?? nullptr;\n"); - assert_has( - &out, - "..mood = _pin(NativeCallable<_ShopKitShopWatcherMood>.isolateLocal(_watcherMood, exceptionalReturn: 0))", - ); - assert_has( - &out, - "..favorite = _pin(NativeCallable<_ShopKitShopWatcherFavorite>.isolateLocal(_watcherFavorite))", - ); - assert_has( - &out, - "typedef _ShopKitShopWatcherLabel = Void Function(Pointer, Pointer>, Pointer, Pointer<_Error>);", - ); - assert_has(&out, "'shop_kit_alloc'"); - assert_has(&out, "outPtr.value = run;"); -} - -#[test] -fn throwing_callback_methods_report_the_domain_error() { - let out = render(); - assert_has( - &out, - " } on ShopException catch (e) {\n \ - _failCallback(outErr, e.code, e.message, _payloadOfShopException(e));\n \ - } catch (e) {\n _foreignError(outErr, e);\n }", - ); - assert_eq!( - out.matches("} on ShopException catch (e) {").count(), - 1, - "only `label` throws" - ); - assert_has( - &out, - "Uint8List? _payloadOfShopException(ShopException e) {", - ); - assert_has( - &out, - " case OutOfStockException():\n final w = _BufferWriter();\n \ - w.writeString(e.sku);\n return w.takeBytes();", - ); - assert_has(&out, " case ClosedException():\n return null;"); - assert_has(&out, "'shop_kit_error_set_payload'"); -} - -#[test] -fn void_methods_are_forwarded_from_any_thread() { - let out = render(); - assert_has( - &out, - "..onDone = _pin(NativeCallable<_ShopKitShopWatcherOnDone>.isolateGroupBound(_watcherOnDone));", - ); - assert_has( - &out, - " _CallbackMessage(ctx, 6, 2)\n ..bytes(summaryPtr, summaryLen)\n \ - ..pointer(cart)\n ..send();", - ); - assert_has( - &out, - " final a3 = a3Address == 0 ? null : Cart._(Pointer.fromAddress(a3Address));\n \ - final a2 = utf8.decode(message[2]! as Uint8List);\n target.onDone(a2, a3);", - ); -} - -#[test] -fn callback_parameters_register_the_implementation() { - let out = render(); - assert_has( - &out, - "final _watcherCtx = _registerCallback(watcher, _watcherDispatch);\n \ - _shopKitShopWatch(_watcherCtx, _watcherVtable.cast(), _err);", - ); - // An optional callback passes a null vtable for none. - assert_has( - &out, - "final _watcherCtx = watcher == null ? nullptr : _registerCallback(watcher, _watcherDispatch);", - ); - assert_has( - &out, - "_shopKitShopMaybeWatch(_watcherCtx, watcher == null ? nullptr : _watcherVtable.cast(), _err);", - ); - // No call may be a leaf call when a callback can re-enter Dart. - assert!(!out.contains("isLeaf: true);\n\nfinal _shopKitShop")); +fn colliding_names_gain_an_underscore() { + let files = render_with(DartConfig::default()); + let shop = file(&files, "lib/src/shop.dart"); + // A record named like a typed list, a field named like a keyword, a + // domain whose exception is the runtime's, and a member named like the + // wrapper's own. + assert_has(shop, "final class Int32List_ {"); + assert_has(shop, "final int class_;"); + assert_has(shop, "class NativeException_ extends NativeException {"); + assert_has(shop, "bool dispose_() {"); + // A parameter named like the cancellation token keeps its name. + assert_has( + shop, + "Future wait(int cancelToken, {CancelToken? cancelToken_}) {", + ); + assert_has(shop, "_NativeCancel.bind(cancelToken_)"); } #[test] fn calls_are_leaf_calls_without_callbacks() { - let api: Api = serde_yaml::from_str( + let model = model_of( r#" -version: "0.11.0" +version: "0.12.0" modules: - name: math functions: - { name: add, params: [{ name: a, type: i32 }, { name: b, type: i32 }], return: i32 } "#, - ) - .unwrap(); - let model = validate(&api, &Identity::named("calc"), None).unwrap(); - let files = DartGenerator.files(&model, Utf8Path::new("out"), &DartConfig::default()); - let out = &files[0].contents; - assert_has(out, "Int32 Function(Int32, Int32, Pointer<_Error>),"); - assert_has(out, "('calc_math_add', isLeaf: true);"); + "calc", + ); + let files = DartGenerator::from(DartConfig::default()).render(&model); + let math = file(&files, "lib/src/math.dart"); + assert_has(math, "Int32 Function(Int32, Int32, Pointer<_Error>),"); + assert_has(math, "('calc_math_add', isLeaf: true);"); assert!( - !out.contains("dart:isolate"), + !file(&files, "lib/calc.dart").contains("dart:isolate"), "isolate glue without callbacks" ); + // With a callback interface, no call may be a leaf call. + for f in render_with(DartConfig::default()) { + if f.path.as_str().starts_with("lib/src/shop") { + assert!(!f.contents.contains("'shop_kit_shop_watch', isLeaf")); + } + } } #[test] fn packaged_loader_resolves_the_bundle_from_the_package() { - let out = packaged(); - assert_has(&out, "import 'dart:io' show Directory, File, Platform;"); + let files = packaged(); + let text = |path: &str| { + files + .iter() + .find(|(p, _)| p == path) + .map(|(_, t)| t.as_str()) + .unwrap_or_else(|| panic!("no {path}")) + }; + assert_has( + text("lib/shop_kit.dart"), + "import 'dart:io' show Directory, File, Platform;", + ); + assert_has( + text("lib/shop_kit.dart"), + "part 'src/runtime/bundled.dart';", + ); + let bundled = text("lib/src/runtime/bundled.dart"); assert_has( - &out, + bundled, " Abi.macosArm64 => 'native/darwin-arm64/libshop_kit.dylib',", ); assert_has( - &out, + bundled, " Abi.windowsX64 => 'native/windows-x64/shop_kit.dll',", ); assert_has( - &out, + bundled, " Abi.linuxArm64 => 'native/linux-arm64/libshop_kit.so',", ); - assert_has(&out, "Uri.parse('package:shop_kit/shop_kit.dart')"); + assert_has(bundled, "Uri.parse('package:shop_kit/shop_kit.dart')"); assert_has( - &out, + bundled, "for (final root in [_packageRoot(), Directory.current.path]) {", ); + let loader = text("lib/src/runtime/loader.dart"); assert_has( - &out, + loader, " final bundled = _openBundled();\n if (bundled != null) return bundled;\n", ); - assert_has(&out, "Platform.environment['SHOP_KIT_LIBRARY']"); + assert_has(loader, "Platform.environment['SHOP_KIT_LIBRARY']"); } #[test] fn every_line_is_free_of_trailing_whitespace() { - for out in [render(), packaged()] { + let rendered = render_with(DartConfig::default()) + .into_iter() + .map(|f| (f.path.to_string(), f.contents)); + for (path, out) in rendered.chain(packaged()) { for (i, line) in out.lines().enumerate() { assert_eq!( line.trim_end(), line, - "trailing whitespace on line {}", + "trailing whitespace on {path}:{}", i + 1 ); } diff --git a/crates/weaveffi-cli/src/targets/dart/types.rs b/crates/weaveffi-cli/src/targets/dart/types.rs index af6f0bce..cf8fdeca 100644 --- a/crates/weaveffi-cli/src/targets/dart/types.rs +++ b/crates/weaveffi-cli/src/targets/dart/types.rs @@ -5,7 +5,9 @@ use crate::lang; use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_model::abi::CType; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::errors::type_name; +use weaveffi_model::model::FnBinding; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty}; /// Type names the generated library declares or uses unqualified. A user /// type with one of these names would shadow or collide with it, so it gains @@ -28,12 +30,18 @@ const RESERVED_TYPES: &[&str] = &[ "File", "Finalizable", "Float", + "Float32List", + "Float64List", "Function", "Future", "Int16", + "Int16List", "Int32", + "Int32List", "Int64", + "Int64List", "Int8", + "Int8List", "IntPtr", "Isolate", "Iterable", @@ -45,7 +53,8 @@ const RESERVED_TYPES: &[&str] = &[ "NativeException", "NativeFinalizer", "NativeFunction", - "NativeLibraryError", + "NativeLibraryException", + "NativeType", "Never", "Object", "Platform", @@ -55,14 +64,17 @@ const RESERVED_TYPES: &[&str] = &[ "StateError", "String", "Struct", + "TypedData", "Uint16", + "Uint16List", "Uint32", + "Uint32List", "Uint64", + "Uint64List", "Uint8", "Uint8List", "Union", "Uri", - "Utf8", "Void", "Zone", ]; @@ -96,6 +108,35 @@ pub(crate) fn dart_member(name: &str) -> String { lang::escape_member(&dart_ident(name), OBJECT_MEMBERS) } +/// Members every value class (a record or rich-enum variant) declares or +/// inherits: its `==`, `hashCode`, and `toString`, and `Object`'s. +const VALUE_MEMBERS: &[&str] = &["hashCode", "noSuchMethod", "runtimeType", "toString"]; + +/// Members every error-code exception declares or inherits: +/// `NativeException`'s `code` and `message`, and `Object`'s. +const ERROR_MEMBERS: &[&str] = &[ + "code", + "hashCode", + "message", + "noSuchMethod", + "runtimeType", + "toString", +]; + +/// The Dart spelling of a record or rich-enum variant field: +/// [`dart_ident`], with a trailing `_` when it would collide with a member +/// the value class declares or inherits (`to_string` is `toString_`). +pub(crate) fn dart_field(name: &str) -> String { + lang::escape_member(&dart_ident(name), VALUE_MEMBERS) +} + +/// The Dart spelling of an error code's field: like [`dart_field`], and +/// also escaped against `NativeException`'s `code` and `message` (a field +/// `message` is `message_`). +pub(crate) fn error_field(name: &str) -> String { + lang::escape_member(&dart_ident(name), ERROR_MEMBERS) +} + /// The Dart class of a user type: its name in UpperCamelCase, escaped when /// it would collide with a reserved type. pub(crate) fn dart_class(name: &str) -> String { @@ -107,9 +148,31 @@ pub(crate) fn dart_class(name: &str) -> String { } } -/// The idiomatic Dart type a [`Ty`] surfaces as. `u64` values are carried as -/// their two's-complement bit pattern in a Dart `int`. +/// The exception class of an error domain or code (`KvError` is +/// `KvException`, `KitchenErrors` is `KitchenException`, a code `NotFound` +/// is `NotFoundException`), named by the shared +/// [`type_name`](weaveffi_model::errors::type_name) and escaped like any +/// class. +pub(crate) fn exception_class(raw: &str) -> String { + dart_class(&type_name(raw, "Exception")) +} + +/// The surface Dart type of a value the bindings hand to the caller: a +/// return, a field, a callback argument. `bytes` is a `Uint8List`, and a +/// `u64` is carried as its two's-complement bit pattern in a Dart `int`. pub(crate) fn dart_type(ty: &Ty) -> String { + surface(ty, "Uint8List") +} + +/// The surface Dart type of a value the caller hands to the bindings: a +/// parameter or a callback's return. Like [`dart_type`], except that +/// `bytes` (at any depth) accepts any `List`, so a `Uint8List` and a +/// list literal both fit; every output type is assignable to it. +pub(crate) fn dart_in_type(ty: &Ty) -> String { + surface(ty, "List") +} + +fn surface(ty: &Ty, bytes: &str) -> String { match ty { Ty::Prim( Prim::I8 @@ -124,38 +187,50 @@ pub(crate) fn dart_type(ty: &Ty) -> String { Ty::Prim(Prim::F32 | Prim::F64) => "double".into(), Ty::Prim(Prim::Bool) => "bool".into(), Ty::Prim(Prim::String) => "String".into(), - Ty::Prim(Prim::Bytes) => "List".into(), - Ty::Enum(n) - | Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(n) => dart_class(n), - Ty::Optional(inner) => format!("{}?", dart_type(inner)), - Ty::List(inner) => format!("List<{}>", dart_type(inner)), - Ty::Iterator(inner) => format!("Iterable<{}>", dart_type(inner)), - Ty::Map(k, v) => format!("Map<{}, {}>", dart_type(k), dart_type(v)), + Ty::Prim(Prim::Bytes) => bytes.into(), + Ty::Enum(n) | Ty::Record(n) | Ty::RichEnum(n) | Ty::Interface(n) => dart_class(n), + Ty::Optional(inner) => format!("{}?", surface(inner, bytes)), + Ty::List(inner) => format!("List<{}>", surface(inner, bytes)), + Ty::Map(k, v) => format!("Map<{}, {}>", surface(k, bytes), surface(v, bytes)), } } -/// The wrapper class of a bare or optional interface type. -/// -/// # Panics -/// -/// Panics when `ty` names no interface; callers dispatch on the object -/// family first. -pub(crate) fn object_class(ty: &Ty) -> String { - dart_class( - ty.interface_name() - .expect("object positions are (optional) interfaces"), - ) +/// The Dart type of a wrapper parameter. +pub(crate) fn param_type(ty: &ParamTy) -> String { + match ty { + ParamTy::Value(t) => dart_in_type(t), + ParamTy::Callback { name, nullable } => { + let class = dart_class(name); + if *nullable { + format!("{class}?") + } else { + class + } + } + } +} + +/// The Dart return type of a wrapper (before any `Future<...>`). +pub(crate) fn return_type(f: &FnBinding) -> String { + match &f.ret { + None => "void".into(), + Some(RetTy::Value(t)) => dart_type(t), + Some(RetTy::Iterator(t)) => format!("Iterable<{}>", dart_type(t)), + } +} + +/// The wrapper class of an interface. +pub(crate) fn object_class(interface: &str) -> String { + dart_class(interface) } /// The `dart:ffi` (native, Dart) type pair of one C ABI slot or return. /// /// Opaque pointees (objects, iterators, vtables, cancel tokens, `void*`) are -/// `Pointer`; byte runs are `Pointer`; the error slot is -/// `Pointer<_Error>`. A bare [`CType::Named`] (an async completion callback) -/// has no generic spelling, so callers substitute the callback typedef. +/// `Pointer`; byte runs are `Pointer`; typed arrays point at +/// their element type; the error slot is `Pointer<_Error>`. A bare +/// [`CType::Named`] (an async completion callback) has no generic spelling, +/// so callers substitute the callback typedef. pub(crate) fn ffi_type(ct: &CType) -> (String, String) { let scalar = |native: &str, dart: &str| (native.to_string(), dart.to_string()); match ct { @@ -199,6 +274,28 @@ fn pointee_type(ct: &CType) -> String { } } +/// The runtime's typed-array helper for `elem` and operation `op` +/// (`stage`, `copy`, `take`, `handOver`): `_takeI32s`, `_stageF64s`, ... +pub(crate) fn slice_fn(op: &str, elem: Prim) -> String { + format!("_{op}{}s", elem.pascal()) +} + +/// The `_CallbackMessage` typed-data constant of a typed-array element. +pub(crate) fn typed_data_kind(elem: Prim) -> &'static str { + match elem { + Prim::I8 => "typedInt8", + Prim::I16 => "typedInt16", + Prim::I32 => "typedInt32", + Prim::I64 => "typedInt64", + Prim::U16 => "typedUint16", + Prim::U32 => "typedUint32", + Prim::U64 => "typedUint64", + Prim::F32 => "typedFloat32", + Prim::F64 => "typedFloat64", + other => unreachable!("{other:?} isn't a typed-array element"), + } +} + /// The private Dart variable holding the bound function of C symbol `sym`. pub(crate) fn ffi_var(sym: &str) -> String { format!("_{}", sym.to_lower_camel_case()) @@ -209,8 +306,9 @@ pub(crate) fn ffi_typedef(c_name: &str) -> String { format!("_{}", c_name.to_upper_camel_case()) } -/// The zero value of a direct type: what a callback trampoline returns when -/// its implementation threw. +/// The zero value of a scalar type: the placeholder an absent optional +/// scalar passes, and what a callback trampoline returns when its +/// implementation threw. pub(crate) fn zero_literal(ty: &Ty) -> &'static str { match ty { Ty::Prim(Prim::Bool) => "false", @@ -237,6 +335,17 @@ mod tests { assert_eq!(dart_class("Store"), "Store"); assert_eq!(dart_class("CancelToken"), "CancelToken_"); assert_eq!(dart_class("String"), "String_"); + assert_eq!(dart_class("Int32List"), "Int32List_"); + } + + #[test] + fn exceptions_never_double_the_suffix() { + assert_eq!(exception_class("KvError"), "KvException"); + assert_eq!(exception_class("KitchenErrors"), "KitchenException"); + assert_eq!(exception_class("Failure"), "FailureException"); + assert_eq!(exception_class("NOT_FOUND"), "NotFoundException"); + assert_eq!(exception_class("Native"), "NativeException_"); + assert_eq!(exception_class("Error"), "Exception_"); } #[test] @@ -249,17 +358,44 @@ mod tests { } #[test] - fn pointers_map_to_opaque_or_byte_pointers() { + fn fields_avoid_the_class_members() { + assert_eq!(dart_field("to_string"), "toString_"); + assert_eq!(dart_field("message"), "message"); + assert_eq!(error_field("message"), "message_"); + assert_eq!(error_field("code"), "code_"); + assert_eq!(error_field("key"), "key"); + } + + #[test] + fn bytes_are_typed_data_out_and_any_list_in() { + let bytes = Ty::Prim(Prim::Bytes); + let nested = Ty::Map( + Box::new(Ty::Prim(Prim::String)), + Box::new(Ty::Optional(Box::new(bytes.clone()))), + ); + assert_eq!(dart_type(&bytes), "Uint8List"); + assert_eq!(dart_in_type(&bytes), "List"); + assert_eq!(dart_type(&nested), "Map"); + assert_eq!(dart_in_type(&nested), "Map?>"); + } + + #[test] + fn pointers_map_to_opaque_typed_or_byte_pointers() { let ptr = |t| CType::ptr(t); assert_eq!(ffi_type(&ptr(CType::Error)).0, "Pointer<_Error>"); assert_eq!( ffi_type(&ptr(ptr(CType::Uint8))).0, "Pointer>" ); + assert_eq!( + ffi_type(&ptr(ptr(CType::Double))).0, + "Pointer>" + ); assert_eq!( ffi_type(&ptr(CType::Named("kv_ScanIterator".into()))).0, "Pointer" ); assert_eq!(ffi_type(&CType::Size), ("Size".into(), "int".into())); + assert_eq!(slice_fn("take", Prim::U64), "_takeU64s"); } } diff --git a/crates/weaveffi-cli/src/targets/dotnet/callbacks.rs b/crates/weaveffi-cli/src/targets/dotnet/callbacks.rs index 0b5c8701..01b96fd1 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/callbacks.rs @@ -1,55 +1,51 @@ //! Callback-interface renderers: the consumer-facing C# `interface` plus the -//! process-wide vtable and its `[UnmanagedCallersOnly]` trampolines, -//! rendering every clause of [`CallbackProtocol`]. +//! process-wide vtable and its `[UnmanagedCallersOnly]` trampolines. //! //! * One static vtable per callback interface, allocated once in native //! memory and never freed, so its address is stable for the process. It //! starts with the `{size, flags, free}` header, `size` being the struct's -//! real size as C# lays it out. +//! real size as C# lays it out and `flags` zero (methods may run on any +//! thread). //! * `ctx` is a `GCHandle` to the implementation, released by the vtable's //! `free` entry, so the implementation lives exactly as long as the //! producer holds it. -//! * Trampoline arguments are received like returns, except that strings, -//! bytes, and buffers are borrowed (copied, never freed); objects are -//! adopted into wrappers the implementation owns. -//! * Returns cross per [`RetPass`]: a direct value by value, an object as a -//! fresh strong reference (`CloneHandle()`), and a string, bytes, or -//! buffer as a `{prefix}_alloc` run written to the out slots. -//! * A thrown exception never unwinds into native code: a method declared -//! `throws` reports its module's domain exception with its code and -//! payload, and any other exception is reported with the foreign error -//! code (`-4`). Trampolines run on whatever thread the producer calls -//! from. +//! * Arguments are borrowed for the call ([`ArgPass`]): strings and buffers +//! are decoded, typed arrays and bytes are spans over the producer's +//! memory, and objects are adopted into wrappers the implementation owns. +//! * Returns cross per [`CallbackRetPass`]: a scalar as the C return, an +//! optional scalar as presence plus out slot, an object as a fresh strong +//! reference, and everything else as a `{prefix}_alloc` run the producer +//! adopts. +//! * A thrown exception never unwinds into native code: an exception of the +//! method's own domain reports its code and fields; any other is reported +//! with code -1 (or -4 from a method that can't fail). -use crate::codegen::CodeWriter; -use heck::{ToLowerCamelCase, ToUpperCamelCase}; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding}; -use weaveffi_model::plan::{CallbackProtocol, ErrorStrategy, RetPass}; +use heck::ToUpperCamelCase; +use weaveffi_model::abi::AbiParam; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, Model}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; -use crate::targets::dotnet::calls::{ - direct_to_slot, receive, write_obsolete, Own, UNMANAGED_CALLERS_ONLY, -}; -use crate::targets::dotnet::codec::write_stmt; -use crate::targets::dotnet::docs::{write_doc, write_fn_doc}; -use crate::targets::dotnet::runtime::dotnet_exception_name; +use crate::codegen::CodeWriter; +use crate::targets::dotnet::calls::UNMANAGED_CALLERS_ONLY; +use crate::targets::dotnet::codec::{read_lambda, write_lambda}; +use crate::targets::dotnet::docs::Docs; +use crate::targets::dotnet::errors::ErrCtx; use crate::targets::dotnet::types::{ - callback_interface_cs, cs_ctype, cs_type, fn_pointer_type, safe_cs_name, vtable_class_cs, Cx, + callback_interface_cs, callback_param_cs, callback_ret_cs, camel, cs_ctype, fn_pointer_type, + safe_cs_name, vtable_class_cs, Cx, }; -/// Render one callback interface declared by the module at `module_path`: -/// the `public interface I{Name}` the consumer implements, then the -/// internal class hosting its vtable. `domain` is the error domain in scope -/// for the module, which methods declared `throws` report. +/// Render one callback interface: the `public interface I{Name}` the +/// consumer implements, then the internal class hosting its vtable. pub(crate) fn render_callback_interface( w: &mut CodeWriter, - module_path: &str, + model: &Model, + docs: &Docs, cb: &CallbackInterfaceBinding, - domain: Option<&ErrorBinding>, cx: Cx<'_>, ) { - let domain = domain.map(dotnet_exception_name); - render_consumer_interface(w, cb, domain.as_deref(), cx); - render_vtable_class(w, cx, module_path, cb, domain.as_deref()); + render_consumer_interface(w, model, docs, cb, cx); + render_vtable_class(w, model, cb, cx); } fn method_cs(m: &CallbackMethodBinding) -> String { @@ -59,43 +55,48 @@ fn method_cs(m: &CallbackMethodBinding) -> String { /// The consumer-facing interface: one method per callback method. fn render_consumer_interface( w: &mut CodeWriter, + model: &Model, + docs: &Docs, cb: &CallbackInterfaceBinding, - domain: Option<&str>, cx: Cx<'_>, ) { let iface = callback_interface_cs(&cb.name); - write_doc(w, &cb.doc); + docs.summary(w, &cb.doc); w.line("/// The native library may call any method from any thread until"); - w.line("/// it releases its last reference to the implementation. Object arguments"); - w.line("/// belong to the implementation. An exception thrown by a method fails the"); - w.line("/// native library's call in progress: a method documented to throw a domain"); - w.line("/// exception reports it with its code and fields, and any other exception"); - w.line(format!( - "/// reaches the library as .", - cx.base - )); - write_obsolete(w, &cb.deprecated); + w.line("/// it releases its last reference to the implementation. Span arguments are"); + w.line("/// valid only during the call; object arguments belong to the implementation."); + w.line("/// An exception thrown by a method fails the native call in progress: an"); + w.line("/// exception of the domain a method documents is reported with its code and"); + w.line("/// fields, and any other reaches the library as a failure carrying its message."); + docs.obsolete(w, &cb.deprecated); w.line(format!("public interface {iface}")); w.block("{", "}", |w| { for (i, m) in cb.methods.iter().enumerate() { if i > 0 { w.blank(); } - let mut params = m.params.clone(); - for p in &mut params { - p.name = p.name.to_lower_camel_case(); + docs.summary(w, &m.doc); + for p in &m.params { + docs.param(w, &camel(&p.name), &p.doc); } - write_fn_doc(w, &m.doc, ¶ms); - if let (true, Some(exc)) = (m.throws, domain) { - w.line(format!( - "/// Reported to the library with its code and fields." - )); + if let ErrorStrategy::Domain(_) = &m.error { + let err = ErrCtx::new(model, &m.error, cx); + if let Some(exc) = &err.domain { + w.line(format!( + "/// Reported to the library with its code and fields." + )); + } } - write_obsolete(w, &m.deprecated); - let ret = m.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()); - let sig: Vec = params + docs.obsolete(w, &m.deprecated); + let ret = m + .ret + .as_ref() + .map(|ty| callback_ret_cs(&m.ret_pass, ty)) + .unwrap_or_else(|| "void".into()); + let sig: Vec = m + .params .iter() - .map(|p| format!("{} {}", cs_type(&p.ty), safe_cs_name(&p.name))) + .map(|p| format!("{} {}", callback_param_cs(&p.pass, &p.ty), camel(&p.name))) .collect(); w.line(format!("{ret} {}({});", method_cs(m), sig.join(", "))); } @@ -107,14 +108,12 @@ fn render_consumer_interface( /// in declaration order) and the trampolines it points at. fn render_vtable_class( w: &mut CodeWriter, - cx: Cx<'_>, - module_path: &str, + model: &Model, cb: &CallbackInterfaceBinding, - domain: Option<&str>, + cx: Cx<'_>, ) { - let protocol = cb.protocol(); let iface = callback_interface_cs(&cb.name); - let class = vtable_class_cs(module_path, &cb.name); + let class = vtable_class_cs(&cb.name); w.line(format!( "/// The process-wide {} and the trampolines behind it,", cb.vtable_tag @@ -133,7 +132,7 @@ fn render_vtable_class( for m in &cb.methods { w.line(format!( "public {} {};", - fn_pointer_type(&m.abi_params, &m.abi_ret), + fn_pointer_type(cx.ns, &m.abi.params, &m.abi.ret), safe_cs_name(&m.name) )); } @@ -146,6 +145,7 @@ fn render_vtable_class( w.block("{", "}", |w| { w.line("var vtable = (Layout*)NativeMemory.AllocZeroed((nuint)sizeof(Layout));"); w.line("vtable->Size = (uint)sizeof(Layout);"); + w.line("// Not thread-affine: every method may be called from any thread."); w.line("vtable->Flags = 0;"); w.line("vtable->Free = &FreeTrampoline;"); for m in &cb.methods { @@ -163,46 +163,77 @@ fn render_vtable_class( w.block("{", "}", |w| { w.line("Ffi.Unregister(ctx);"); }); - for (i, m) in cb.methods.iter().enumerate() { + for m in &cb.methods { w.blank(); - let reports = match protocol.method_errors[i] { - ErrorStrategy::Throws => domain, - ErrorStrategy::Trap => None, - }; - render_trampoline(w, cx, &iface, m, &protocol, i, reports); + render_trampoline(w, model, cx, &iface, m); } }); w.blank(); } -/// The name of the slot at `from_end` places before the end of `m`'s slot -/// list (`1` is `out_err`). -fn slot_from_end(m: &CallbackMethodBinding, from_end: usize) -> String { - safe_cs_name(&m.abi_params[m.abi_params.len() - from_end].name) +/// The expression receiving one borrowed argument from its slots. +fn receive_arg(cx: Cx<'_>, pass: &ArgPass, ty: &weaveffi_model::ty::Ty) -> String { + let n = |slot: &AbiParam| safe_cs_name(&slot.name); + match pass { + ArgPass::Direct { slot } => n(slot), + ArgPass::OptDirect { has, value, .. } => format!("{} ? {} : null", n(has), n(value)), + ArgPass::Slice { ptr, len, .. } | ArgPass::Bytes { ptr, len } => { + format!("Ffi.Borrow({}, {})", n(ptr), n(len)) + } + ArgPass::String { ptr, len } => format!("Ffi.ReadString({}, {})", n(ptr), n(len)), + ArgPass::Buffer { ptr, len } => format!( + "Ffi.ReadBuffer({}, {}, {})", + n(ptr), + n(len), + read_lambda(cx, ty, 0) + ), + ArgPass::Object { + slot, + nullable, + interface, + } => { + let class = cx.ty(interface); + if *nullable { + format!("{0} == IntPtr.Zero ? null : {class}.Adopt({0})", n(slot)) + } else { + format!("{class}.Adopt({})", n(slot)) + } + } + ArgPass::Callback { .. } => unreachable!("a callback method never takes a callback"), + } } /// One trampoline: receive the slots, call the implementation, and hand -/// its result back; any exception is reported through `out_err`. `domain` -/// is the exception class a `throws` method reports with its code and -/// payload. +/// its result back; any exception is reported through `out_err`. fn render_trampoline( w: &mut CodeWriter, + model: &Model, cx: Cx<'_>, iface: &str, m: &CallbackMethodBinding, - protocol: &CallbackProtocol<'_>, - index: usize, - domain: Option<&str>, ) { let sig: Vec = m - .abi_params + .abi + .params .iter() - .map(|s| format!("{} {}", cs_ctype(&s.ty), safe_cs_name(&s.name))) + .map(|s| format!("{} {}", cs_ctype(cx.ns, &s.ty), safe_cs_name(&s.name))) .collect(); - let ret = cs_ctype(&m.abi_ret); + let ret = cs_ctype(cx.ns, &m.abi.ret); let returns_value = ret != "void"; - let ctx = safe_cs_name(&m.abi_params[0].name); - let out_err = slot_from_end(m, 1); + let ctx = safe_cs_name(&m.abi.params[0].name); + let out_err = safe_cs_name( + &m.abi + .params + .last() + .expect("a callback method ends with out_err") + .name, + ); + let err = ErrCtx::new(model, &m.error, cx); + let fallback = match m.error { + ErrorStrategy::Trap => "ForeignErrorCode", + _ => "GenericErrorCode", + }; + let n = |slot: &AbiParam| safe_cs_name(&slot.name); w.line(UNMANAGED_CALLERS_ONLY); w.line(format!( "private static {ret} {}Trampoline({})", @@ -215,59 +246,58 @@ fn render_trampoline( let args: Vec = m .params .iter() - .map(|p| { - let ptr = safe_cs_name(&p.abi[0].name); - let len = p - .abi - .get(1) - .map(|s| safe_cs_name(&s.name)) - .unwrap_or_default(); - receive(cx, &p.ty, &ptr, &len, Own::Borrow) - }) + .map(|p| receive_arg(cx, &p.pass, &p.ty)) .collect(); let call = format!( "Ffi.Target<{iface}>({ctx}).{}({})", method_cs(m), args.join(", ") ); - match (&protocol.method_returns[index], &m.ret) { - (RetPass::Void, _) | (_, None) => w.line(format!("{call};")), - (RetPass::Direct, Some(ty)) => { - w.line(format!("return {};", direct_to_slot(ty, &call))) + match &m.ret_pass { + CallbackRetPass::Void => w.line(format!("{call};")), + CallbackRetPass::Direct => w.line(format!("return {call};")), + CallbackRetPass::OptDirect { out_value } => w.line(format!( + "return Ffi.ReturnOptional({call}, {});", + n(out_value) + )), + CallbackRetPass::Slice { + out_ptr, out_len, .. + } => w.line(format!( + "Ffi.ReturnArray({call}, {}, {});", + n(out_ptr), + n(out_len) + )), + CallbackRetPass::String { out_ptr, out_len } => w.line(format!( + "Ffi.ReturnString({call}, {}, {});", + n(out_ptr), + n(out_len) + )), + CallbackRetPass::Bytes { out_ptr, out_len } => w.line(format!( + "Ffi.ReturnBytes({call}, {}, {});", + n(out_ptr), + n(out_len) + )), + CallbackRetPass::Buffer { out_ptr, out_len } => { + let ty = m.ret.as_ref().expect("a buffered return has a type"); + w.line(format!( + "Ffi.ReturnBuffer({call}, {}, {}, {});", + write_lambda(ty, 0), + n(out_ptr), + n(out_len) + )) } // One strong reference the producer adopts. A null for a // required object passes through as null, which the // producer rejects as a marshalling failure (-3). - (RetPass::Object { .. }, _) => { + CallbackRetPass::Object { .. } => { w.line(format!("return {call}?.CloneHandle() ?? IntPtr.Zero;")) } - (rp, Some(ty)) => { - let (out_ptr, out_len) = (slot_from_end(m, 3), slot_from_end(m, 2)); - match rp { - RetPass::String => { - w.line(format!("Ffi.ReturnString({call}, {out_ptr}, {out_len});")) - } - RetPass::Bytes => { - w.line(format!("Ffi.ReturnBytes({call}, {out_ptr}, {out_len});")) - } - _ => { - w.line(format!("var ffiResult = {call};")); - w.line("var ffiWriter = new FfiBufferWriter();"); - w.line(write_stmt(ty, "ffiWriter", "ffiResult")); - w.line(format!( - "Ffi.ReturnBuffer(ffiWriter, {out_ptr}, {out_len});" - )) - } - } - } }; }); - if let Some(exc) = domain { + if let Some(exc) = &err.domain { w.line(format!("catch ({} e)", cx.ty(exc))); w.block("{", "}", |w| { - w.line(format!( - "Ffi.SetDomainError({out_err}, e.Code, e, e.WritePayload);" - )); + w.line(format!("Ffi.ReportDomain({out_err}, e);")); if returns_value { w.line("return default;"); } @@ -275,7 +305,7 @@ fn render_trampoline( } w.line("catch (Exception e)"); w.block("{", "}", |w| { - w.line(format!("Ffi.SetForeignError({out_err}, e);")); + w.line(format!("Ffi.Report({out_err}, e, {}.{fallback});", cx.base)); if returns_value { w.line("return default;"); } diff --git a/crates/weaveffi-cli/src/targets/dotnet/calls.rs b/crates/weaveffi-cli/src/targets/dotnet/calls.rs index 95e294ec..de220047 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/calls.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/calls.rs @@ -1,86 +1,34 @@ //! Call-path renderers: the wrapper methods for every call shape (sync, //! async, iterator), argument marshalling driven by [`ArgPass`], result -//! receiving driven by [`Family`], and the per-module static classes. +//! receiving driven by [`RetPass`], [`ResultPass`], and [`ItemPass`], and +//! the per-module static classes. //! -//! Every wrapper follows one frame: encode arguments (UTF-8 strings, value -//! buffers, callback registrations), pin them with one `fixed` statement, -//! call the import with a stack `FfiError`, check it, and receive the result. +//! Every wrapper follows one frame: encode arguments (pooled UTF-8, pooled +//! value buffers, callback registrations), pin them with `fixed`, call the +//! import with a stack `FfiError` and out slots, check the error, and +//! receive the result. -use crate::codegen::CodeWriter; -use heck::ToUpperCamelCase; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_model::abi::{AbiParam, CType}; use weaveffi_model::model::{ - AsyncBinding, CallShape, ErrorBinding, FnBinding, IteratorBinding, ModuleBinding, ParamBinding, + AsyncBinding, FnBinding, IteratorBinding, Model, ModuleBinding, ParamBinding, }; -use weaveffi_model::plan::{ArgPass, ErrorStrategy}; -use weaveffi_model::ty::{Family, Prim, Ty}; +use weaveffi_model::plan::{ArgPass, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::Ty; -use crate::targets::dotnet::codec::{decode_expr, write_stmt}; -use crate::targets::dotnet::docs::{write_doc, write_fn_doc}; -use crate::targets::dotnet::runtime::dotnet_exception_name; +use crate::codegen::CodeWriter; +use crate::targets::dotnet::codec::{read_lambda, write_call}; +use crate::targets::dotnet::docs::Docs; +use crate::targets::dotnet::errors::ErrCtx; use crate::targets::dotnet::types::{ - camel_fn, cs_ctype, cs_type, safe_cs_name, vtable_class_cs, Cx, + camel, cs_ctype, item_cs, param_cs, prim_cs, result_cs, ret_cs, safe_cs_name, vtable_class_cs, + Cx, }; /// The `[UnmanagedCallersOnly]` attribute every native entry point carries. pub(crate) const UNMANAGED_CALLERS_ONLY: &str = "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]"; -/// How a wrapper maps a non-zero error slot to an exception, rendering -/// [`ErrorStrategy`]: a throwing function uses its domain's typed -/// `FromError` (or the root exception's, without a domain in scope); a -/// non-throwing one traps through the bug exception's. -#[derive(Clone)] -pub(crate) struct ErrCtx<'a> { - cx: Cx<'a>, - map: String, - domain: Option<(String, String)>, -} - -impl<'a> ErrCtx<'a> { - /// The error context for one function. - pub(crate) fn for_fn(f: &FnBinding, error: Option<&ErrorBinding>, cx: Cx<'a>) -> Self { - match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => { - let exc = dotnet_exception_name(eb); - ErrCtx { - cx, - map: format!("{}.FromError", cx.ty(&exc)), - domain: Some((exc, eb.name.clone())), - } - } - (ErrorStrategy::Throws, None) => ErrCtx { - cx, - map: format!("{}.FromError", cx.ty(cx.base)), - domain: None, - }, - (ErrorStrategy::Trap, _) => ErrCtx { - cx, - map: format!("{}.FromError", cx.ty(cx.bug)), - domain: None, - }, - } - } - - /// The statement throwing when the local `FfiError` named `var` holds a - /// failure. - fn check(&self, var: &str) -> String { - format!( - "if ({var}.Code != 0) throw Ffi.TakeError(&{var}, {});", - self.map - ) - } - - /// Emit the `` doc line of a throwing wrapper. - fn write_doc(&self, w: &mut CodeWriter) { - if let Some((exc, ty)) = &self.domain { - w.line(format!( - "/// The call reported a {ty} code." - )); - } - } -} - /// The receiver of a wrapper: what the method is a member of. #[derive(Clone, Copy)] pub(crate) enum Receiver<'a> { @@ -93,16 +41,10 @@ pub(crate) enum Receiver<'a> { Constructor(&'a str), } -/// The `[Obsolete]` attribute line for a deprecated item. -pub(crate) fn write_obsolete(w: &mut CodeWriter, deprecated: &Option) { - if let Some(msg) = deprecated { - w.line(format!("[Obsolete(\"{}\")]", msg.replace('"', "\\\""))); - } -} - /// The argument marshalling for one parameter list: statements run before -/// the call, the buffers pinned for it, one expression per ABI slot, and the -/// callback registrations to release if the call never reaches the producer. +/// the call, the `fixed` pins it needs, one expression per ABI slot, and +/// the callback registrations to release if the call never reaches the +/// producer. struct Marshal { setup: Vec, pins: Vec, @@ -111,75 +53,91 @@ struct Marshal { } impl Marshal { - /// Plan the marshalling of `params` (already camelCased). + /// Plan the marshalling of `params`. fn new(cx: Cx<'_>, params: &[ParamBinding]) -> Self { - let mut pins = Vec::new(); - let mut args = Vec::new(); - let mut callbacks = Vec::new(); - let mut setup = Vec::new(); + let mut m = Marshal { + setup: Vec::new(), + pins: Vec::new(), + args: Vec::new(), + callbacks: Vec::new(), + }; let mut registrations = Vec::new(); for p in params { - let n = safe_cs_name(&p.name); - let local = p.name.as_str(); - match p.arg_pass() { - ArgPass::Direct { .. } => args.push(direct_to_slot(&p.ty, &n)), + let n = camel(&p.name); + let local = p.name.to_lower_camel_case(); + match &p.pass { + ArgPass::Direct { .. } => m.args.push(n), + ArgPass::OptDirect { .. } => { + m.args.push(format!("{n}.HasValue")); + m.args.push(format!("{n}.GetValueOrDefault()")); + } + ArgPass::Slice { elem, .. } => { + m.pins.push(format!("{}* {local}Ptr = {n}", prim_cs(*elem))); + m.args.push(format!("{local}Ptr")); + m.args.push(format!("(nuint){n}.Length")); + } ArgPass::String { .. } => { - setup.push(format!("var {local}Bytes = Ffi.Utf8({n});")); - pins.push(format!("{local}Ptr = {local}Bytes")); - args.push(format!("{local}Ptr")); - args.push(format!("(nuint){local}Bytes.Length")); + m.setup + .push(format!("using var {local}Utf8 = new FfiUtf8({n});")); + m.pins.push(format!("byte* {local}Ptr = {local}Utf8")); + m.args.push(format!("{local}Ptr")); + m.args.push(format!("{local}Utf8.Length")); } ArgPass::Bytes { .. } => { - pins.push(format!("{local}Ptr = {n}")); - args.push(format!("{local}Ptr")); - args.push(format!("(nuint){n}.Length")); + m.pins.push(format!("byte* {local}Ptr = {n}")); + m.args.push(format!("{local}Ptr")); + m.args.push(format!("(nuint){n}.Length")); } ArgPass::Buffer { .. } => { let writer = format!("{local}Writer"); - setup.push(format!("var {writer} = new FfiBufferWriter();")); - setup.push(write_stmt(&p.ty, &writer, &n)); - pins.push(format!("{local}Ptr = {writer}.Written")); - args.push(format!("{local}Ptr")); - args.push(format!("(nuint){writer}.Length")); + let ty = p.ty.value().expect("a buffered parameter has a value type"); + m.setup + .push(format!("using var {writer} = new FfiBufferWriter();")); + m.setup.push(format!("{};", write_call(ty, &writer, &n, 0))); + m.pins.push(format!("byte* {local}Ptr = {writer}.Written")); + m.args.push(format!("{local}Ptr")); + m.args.push(format!("(nuint){writer}.Length")); } - ArgPass::Object { nullable, .. } => { - if nullable { - let iface = - p.ty.interface_name() - .expect("object types name an interface"); - args.push(format!("{n}?.Handle ?? {}.NativeHandle.Null", cx.ty(iface))); + ArgPass::Object { + nullable, + interface, + .. + } => { + if *nullable { + m.args.push(format!( + "{n}?.Handle ?? {}.NativeHandle.Null", + cx.ty(interface) + )); } else { - args.push(format!("{n}.Handle")); + m.args.push(format!("{n}.Handle")); } } // The producer owns the registration once the call reaches // it: its vtable `free(ctx)` releases it. An absent optional // implementation passes a null vtable. ArgPass::Callback { - vtable, nullable, .. + nullable, + interface, + .. } => { let ctx = format!("{local}Ctx"); - let pointer = format!("{}.Pointer", vtable_class_for_slot(&vtable.ty)); - args.push(ctx.clone()); - if nullable { - args.push(format!("{n} == null ? IntPtr.Zero : {pointer}")); + let pointer = format!("{}.Pointer", vtable_class_cs(interface)); + m.args.push(ctx.clone()); + if *nullable { + m.args + .push(format!("{n} == null ? IntPtr.Zero : {pointer}")); } else { - args.push(pointer); + m.args.push(pointer); } registrations.push(format!("var {ctx} = Ffi.Register({n});")); - callbacks.push(ctx); + m.callbacks.push(ctx); } } } // Register last, so a failing encode above can't strand a // registration. - setup.extend(registrations); - Marshal { - setup, - pins, - args, - callbacks, - } + m.setup.extend(registrations); + m } /// Emit the setup statements. @@ -189,19 +147,46 @@ impl Marshal { } } - /// Emit `body` inside one `fixed` statement pinning every buffer, or bare + /// Emit `body` inside one stacked `fixed` statement per pin, or bare /// when nothing needs pinning. fn pinned(&self, w: &mut CodeWriter, body: impl FnOnce(&mut CodeWriter)) { if self.pins.is_empty() { body(w); return; } - w.line(format!("fixed (byte* {})", self.pins.join(", "))); + for pin in &self.pins { + w.line(format!("fixed ({pin})")); + } w.line("{"); w.scope(body); w.line("}"); } + /// Emit `call` (a statement), releasing every callback registration if + /// it throws before reaching the producer. `declare` is the declaration + /// of the variable the statement assigns, when it assigns one. + fn guarded(&self, w: &mut CodeWriter, declare: Option, call: &str) { + if self.callbacks.is_empty() { + match declare { + Some(_) => w.line(format!("var {call}")), + None => w.line(call), + }; + return; + } + if let Some(decl) = declare { + w.line(decl); + } + w.line("try"); + w.block("{", "}", |w| { + w.line(call); + }); + w.line("catch"); + w.block("{", "}", |w| { + self.unregister(w); + w.line("throw;"); + }); + } + /// The statements releasing every callback registration. fn unregister(&self, w: &mut CodeWriter) { for ctx in &self.callbacks { @@ -210,104 +195,14 @@ impl Marshal { } } -/// The C# class hosting the static vtable a callback slot points at, named -/// from the slot's vtable C type. -fn vtable_class_for_slot(vtable: &CType) -> String { - let CType::Ptr { pointee, .. } = vtable else { - unreachable!("a callback vtable slot is a pointer") - }; - let CType::VtableTag { module, name } = pointee.as_ref() else { - unreachable!("a callback vtable slot points at a vtable tag") - }; - vtable_class_cs(module, name) -} - -/// The expression converting a surface value into its by-value slot: `bool` -/// as a byte, a C-style enum as its `int`, every other scalar as is. -pub(crate) fn direct_to_slot(ty: &Ty, expr: &str) -> String { - match ty { - Ty::Prim(Prim::Bool) => format!("(byte)({expr} ? 1 : 0)"), - Ty::Enum(_) => format!("(int){expr}"), - _ => expr.to_string(), - } -} - -/// The expression converting a by-value slot into its surface type. -pub(crate) fn direct_from_slot(cx: Cx<'_>, ty: &Ty, slot: &str) -> String { - match ty { - Ty::Prim(Prim::Bool) => format!("{slot} != 0"), - Ty::Enum(name) => format!("({}){slot}", cx.ty(name)), - _ => slot.to_string(), - } -} - -/// Who owns a received `(ptr, len)` run. -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum Own { - /// The consumer: copy or decode it, then release it with `free_bytes` - /// (returns, async results, iterator items). - Take, - /// The producer, for the duration of a callback: copy or decode only. - Borrow, -} - -/// The expression receiving a value of `ty` from its slots (`ptr`, plus -/// `len` for the `(ptr, len)` families). Objects always transfer one strong -/// reference, which the new wrapper adopts. -pub(crate) fn receive(cx: Cx<'_>, ty: &Ty, ptr: &str, len: &str, own: Own) -> String { - let verb = match own { - Own::Take => "Take", - Own::Borrow => "Read", - }; - match ty.family() { - Family::Direct => direct_from_slot(cx, ty, ptr), - Family::String => format!("Ffi.{verb}String({ptr}, {len})"), - Family::Bytes => format!("Ffi.{verb}Bytes({ptr}, {len})"), - Family::Buffer => decode_expr(cx, ty, &format!("Ffi.{verb}Buffer({ptr}, {len})")), - Family::Object { nullable } => { - let class = cx.ty(ty.interface_name().expect("object types name an interface")); - if nullable { - format!("{ptr} == IntPtr.Zero ? null : {class}.Adopt({ptr})") - } else { - format!("{class}.Adopt({ptr})") - } - } - Family::Callback { .. } | Family::Iterator => unreachable!("{ty} is never received"), - } -} - -/// The public signature's parameter list. With `span`, bytes parameters are -/// `ReadOnlySpan`. -fn params_sig(params: &[ParamBinding], span: bool) -> Vec { +/// The public signature's parameter list. +fn params_sig(params: &[ParamBinding]) -> Vec { params .iter() - .map(|p| { - let ty = if span && p.ty == Ty::Prim(Prim::Bytes) { - "ReadOnlySpan".to_string() - } else { - cs_type(&p.ty) - }; - format!("{ty} {}", safe_cs_name(&p.name)) - }) + .map(|p| format!("{} {}", param_cs(&p.pass, p.ty.value()), camel(&p.name))) .collect() } -/// The argument list forwarding a `byte[]` overload to its span overload. -fn forward_args(params: &[ParamBinding]) -> String { - params - .iter() - .map(|p| { - let n = safe_cs_name(&p.name); - if p.ty == Ty::Prim(Prim::Bytes) { - format!("(ReadOnlySpan){n}") - } else { - n - } - }) - .collect::>() - .join(", ") -} - /// The slot arguments of a call: the receiver (when any), every marshalled /// parameter slot, then `extra`. fn call_args(receiver: Receiver<'_>, m: &Marshal, extra: &[String]) -> String { @@ -320,8 +215,8 @@ fn call_args(receiver: Receiver<'_>, m: &Marshal, extra: &[String]) -> String { args.join(", ") } -/// The signature line opener for a wrapper: modifiers, return type, and -/// name (or the bare class name for a constructor). +/// The modifiers, return type, and name opening a method (or the bare class +/// name of a constructor). fn opener(receiver: Receiver<'_>, ret: &str, name: &str) -> String { match receiver { Receiver::Static => format!("public static {ret} {name}"), @@ -330,113 +225,150 @@ fn opener(receiver: Receiver<'_>, ret: &str, name: &str) -> String { } } +/// The element type of a pointer slot. +fn pointee(slot: &AbiParam) -> &CType { + match &slot.ty { + CType::Ptr { pointee, .. } => pointee, + other => other, + } +} + +/// The expression adopting an object pointer into a wrapper (`null` for a +/// null pointer of a nullable object). +fn adopt(cx: Cx<'_>, ptr: &str, nullable: bool, interface: &str) -> String { + let class = cx.ty(interface); + if nullable { + format!("{ptr} == IntPtr.Zero ? null : {class}.Adopt({ptr})") + } else { + format!("{class}.Adopt({ptr})") + } +} + /// Render one wrapper (any shape) named `name`. pub(crate) fn render_callable( w: &mut CodeWriter, + docs: &Docs, f: &FnBinding, name: &str, receiver: Receiver<'_>, - err: &ErrCtx<'_>, + err: &ErrCtx, + cx: Cx<'_>, ) { - let f = camel_fn(f); - match &f.shape { - CallShape::Sync(abi) => { - let span = f.params.iter().any(|p| p.ty == Ty::Prim(Prim::Bytes)); - render_sync(w, &f, &abi.params, &abi.ret, name, receiver, err, span); - if span { - render_array_overload(w, &f, name, receiver, err); - } - } - CallShape::Async(a) => render_async(w, &f, a, name, receiver, err), - CallShape::Iterator(it) => render_iterator(w, &f, it, name, receiver, err), + if let Some(a) = f.async_binding() { + render_async(w, docs, f, a, name, receiver, err, cx); + } else if let Some(it) = f.iterator() { + render_iterator(w, docs, f, it, name, receiver, err, cx); + } else { + render_sync(w, docs, f, name, receiver, err, cx); } } /// The documentation, ``, and `[Obsolete]` lines of a wrapper. -fn write_header(w: &mut CodeWriter, f: &FnBinding, err: &ErrCtx<'_>) { - write_fn_doc(w, &f.doc, &f.params); +fn write_header(w: &mut CodeWriter, docs: &Docs, f: &FnBinding, err: &ErrCtx) { + docs.summary(w, &f.doc); + for p in &f.params { + docs.param(w, &camel(&p.name), &p.doc); + } err.write_doc(w); - write_obsolete(w, &f.deprecated); + docs.obsolete(w, &f.deprecated); +} + +/// The out-slot locals of a sync call and the `&local` arguments passing +/// them, per its [`RetPass`]. +fn ret_outs(cx: Cx<'_>, pass: &RetPass) -> (Vec, Vec) { + match pass { + RetPass::OptDirect { out_value } => ( + vec![format!( + "{} ffiValue = default;", + cs_ctype(cx.ns, pointee(out_value)) + )], + vec!["&ffiValue".into()], + ), + RetPass::Slice { .. } + | RetPass::String { .. } + | RetPass::Bytes { .. } + | RetPass::Buffer { .. } => ( + vec!["nuint ffiOutLen = 0;".into()], + vec!["&ffiOutLen".into()], + ), + _ => (Vec::new(), Vec::new()), + } +} + +/// The expression receiving a sync call's result `ffiResult`. +fn receive_ret(cx: Cx<'_>, pass: &RetPass, ty: &Ty) -> String { + match pass { + RetPass::Void | RetPass::Iterator(_) => unreachable!("no value to receive"), + RetPass::Direct => "ffiResult".into(), + RetPass::OptDirect { .. } => "ffiResult ? ffiValue : null".into(), + RetPass::Slice { .. } => "Ffi.TakeArray(ffiResult, ffiOutLen)".into(), + RetPass::String { .. } => "Ffi.TakeString(ffiResult, ffiOutLen)".into(), + RetPass::Bytes { .. } => "Ffi.TakeBytes(ffiResult, ffiOutLen)".into(), + RetPass::Buffer { .. } => format!( + "Ffi.TakeBuffer(ffiResult, ffiOutLen, {})", + read_lambda(cx, ty, 0) + ), + RetPass::Object { + nullable, + interface, + .. + } => adopt(cx, "ffiResult", *nullable, interface), + } } /// A synchronous wrapper: one call, one check, one received result. A /// constructor stores the adopted pointer as its handle instead of /// returning it. -#[allow(clippy::too_many_arguments)] fn render_sync( w: &mut CodeWriter, + docs: &Docs, f: &FnBinding, - slots: &[AbiParam], - ret: &CType, name: &str, receiver: Receiver<'_>, - err: &ErrCtx<'_>, - span: bool, + err: &ErrCtx, + cx: Cx<'_>, ) { - let ret_cs = match receiver { - Receiver::Constructor(_) => String::new(), - _ => f.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()), + let value = f.ret.as_ref().and_then(|r| r.value()); + let ret = match (receiver, value) { + (Receiver::Constructor(_), _) => String::new(), + (_, Some(ty)) => ret_cs(&f.ret_pass, ty), + (_, None) => "void".into(), }; - let cx = err.cx; let m = Marshal::new(cx, &f.params); - let has_out_len = slots.iter().any(|s| s.name == "out_len"); - let mut extra = Vec::new(); - if has_out_len { - extra.push("&ffiOutLen".to_string()); - } + let (locals, mut extra) = ret_outs(cx, &f.ret_pass); extra.push("&ffiErr".to_string()); let call = format!( "NativeMethods.{}({})", - f.c_base, + f.abi.symbol, call_args(receiver, &m, &extra) ); - write_header(w, f, err); + write_header(w, docs, f, err); w.line(format!( "{}({})", - opener(receiver, &ret_cs, name), - params_sig(&f.params, span).join(", ") + opener(receiver, &ret, name), + params_sig(&f.params).join(", ") )); w.block("{", "}", |w| { m.write_setup(w); w.line("var ffiErr = default(FfiError);"); - if has_out_len { - w.line("nuint ffiOutLen = 0;"); + for l in &locals { + w.line(l); } m.pinned(w, |w| { - let returns = f.ret.is_some(); - if m.callbacks.is_empty() { - if returns { - w.line(format!("var ffiResult = {call};")); - } else { - w.line(format!("{call};")); - } + if value.is_some() { + let decl = format!("{} ffiResult;", cs_ctype(cx.ns, &f.abi.ret)); + m.guarded(w, Some(decl), &format!("ffiResult = {call};")); } else { - if returns { - w.line(format!("{} ffiResult;", cs_ctype(ret))); - } - w.line("try"); - w.block("{", "}", |w| { - if returns { - w.line(format!("ffiResult = {call};")); - } else { - w.line(format!("{call};")); - } - }); - w.line("catch"); - w.block("{", "}", |w| { - m.unregister(w); - w.line("throw;"); - }); + m.guarded(w, None, &format!("{call};")); } w.line(err.check("ffiErr")); - match (&f.ret, receiver) { + match (value, receiver) { (Some(_), Receiver::Constructor(_)) => { w.line("Handle = new NativeHandle(ffiResult);"); } (Some(ty), _) => { - let expr = receive(cx, ty, "ffiResult", "ffiOutLen", Own::Take); - w.line(format!("return {expr};")); + w.line(format!("return {};", receive_ret(cx, &f.ret_pass, ty))); } (None, _) => {} } @@ -445,101 +377,97 @@ fn render_sync( w.blank(); } -/// The `byte[]` overload of a wrapper whose bytes parameters are -/// `ReadOnlySpan`, forwarding to it. -fn render_array_overload( - w: &mut CodeWriter, - f: &FnBinding, - name: &str, - receiver: Receiver<'_>, - err: &ErrCtx<'_>, -) { - let ret_cs = f.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()); - let args = forward_args(&f.params); - write_header(w, f, err); - let sig = format!( - "{}({})", - opener(receiver, &ret_cs, name), - params_sig(&f.params, false).join(", ") - ); - match receiver { - Receiver::Constructor(_) => { - w.line(format!("{sig} : this({args})")); - w.line("{"); - w.line("}"); - } - _ => { - w.line(format!("{sig} => {name}({args});")); - } +/// The expression receiving an async result from the completion's slots. +fn receive_result(cx: Cx<'_>, pass: &ResultPass, ty: Option<&Ty>) -> String { + let n = |slot: &AbiParam| safe_cs_name(&slot.name); + match pass { + ResultPass::Void => "default".into(), + ResultPass::Direct { result } => n(result), + ResultPass::OptDirect { has, value } => format!("{} ? {} : null", n(has), n(value)), + ResultPass::Slice { ptr, len, .. } => format!("Ffi.TakeArray({}, {})", n(ptr), n(len)), + ResultPass::String { ptr, len } => format!("Ffi.TakeString({}, {})", n(ptr), n(len)), + ResultPass::Bytes { ptr, len } => format!("Ffi.TakeBytes({}, {})", n(ptr), n(len)), + ResultPass::Buffer { ptr, len } => format!( + "Ffi.TakeBuffer({}, {}, {})", + n(ptr), + n(len), + read_lambda(cx, ty.expect("a buffered result has a type"), 0) + ), + ResultPass::Object { + result, + nullable, + interface, + .. + } => adopt(cx, &n(result), *nullable, interface), } - w.blank(); } /// An async wrapper: a `Task`-returning method launching the call with a /// pending `FfiCall` as `context`, plus the `[UnmanagedCallersOnly]` -/// completion that resolves it. A cancellable function takes a -/// `CancellationToken` linked to the native cancel token. +/// completion that resolves it. Every async method takes a +/// `CancellationToken`: a cancellable function links it to the native +/// cancel token; any other stops waiting when it fires. +#[allow(clippy::too_many_arguments)] fn render_async( w: &mut CodeWriter, + docs: &Docs, f: &FnBinding, a: &AsyncBinding, name: &str, receiver: Receiver<'_>, - err: &ErrCtx<'_>, + err: &ErrCtx, + cx: Cx<'_>, ) { - let result_cs = f.ret.as_ref().map(cs_type); - let task = match &result_cs { + let value = f.ret.as_ref().and_then(|r| r.value()); + let result = value.map(|ty| result_cs(&a.result, ty)); + let task = match &result { Some(t) => format!("Task<{t}>"), None => "Task".into(), }; - let call_ty = result_cs.clone().unwrap_or_else(|| "bool".into()); + let call_ty = result.clone().unwrap_or_else(|| "FfiVoid".into()); let complete = format!("Complete{name}"); - let cx = err.cx; let m = Marshal::new(cx, &f.params); let mut extra = Vec::new(); - if f.cancellable { + if a.cancellable() { extra.push("ffiCall.CancelToken()".to_string()); } extra.push(format!("&{complete}")); extra.push("ffiCall.Context".to_string()); let call = format!( "NativeMethods.{}({});", - a.launch.symbol, + f.abi.symbol, call_args(receiver, &m, &extra) ); - write_header(w, f, err); - if f.cancellable { - w.line("/// Cancels the native call; the task"); - w.line("/// then completes as canceled."); - } - let mut sig = params_sig(&f.params, false); - if f.cancellable { - sig.push("CancellationToken cancellationToken = default".into()); + write_header(w, docs, f, err); + if a.cancellable() { + w.line("/// Cancels the native call; the task then"); + w.line("/// completes as canceled."); + } else { + w.line("/// Stops waiting: the task completes as"); + w.line("/// canceled at once, and the native call, which can't be cancelled, finishes"); + w.line("/// in the background and releases its result."); } + let mut sig = params_sig(&f.params); + sig.push("CancellationToken cancellationToken = default".into()); let opener = match receiver { Receiver::Instance => format!("public {task} {name}"), _ => format!("public static {task} {name}"), }; w.line(format!("{opener}({})", sig.join(", "))); w.block("{", "}", |w| { - if f.cancellable { - w.line("if (cancellationToken.IsCancellationRequested)"); - w.block("{", "}", |w| { - let from = match &result_cs { - Some(t) => format!("Task.FromCanceled<{t}>(cancellationToken)"), - None => "Task.FromCanceled(cancellationToken)".into(), - }; - w.line(format!("return {from};")); - }); - } + w.line("if (cancellationToken.IsCancellationRequested)"); + w.block("{", "}", |w| { + let from = match &result { + Some(t) => format!("Task.FromCanceled<{t}>(cancellationToken)"), + None => "Task.FromCanceled(cancellationToken)".into(), + }; + w.line(format!("return {from};")); + }); m.write_setup(w); - let token = if f.cancellable { - "cancellationToken" - } else { - "" - }; - w.line(format!("var ffiCall = new FfiCall<{call_ty}>({token});")); + w.line(format!( + "var ffiCall = new FfiCall<{call_ty}>(cancellationToken);" + )); w.line("try"); w.block("{", "}", |w| { m.pinned(w, |w| { @@ -552,7 +480,7 @@ fn render_async( m.unregister(w); w.line("throw;"); }); - w.line("return ffiCall.Task;"); + w.line("return ffiCall.Launched();"); }); w.blank(); @@ -560,7 +488,7 @@ fn render_async( let slots: Vec = a .callback_params .iter() - .map(|s| format!("{} {}", cs_ctype(&s.ty), safe_cs_name(&s.name))) + .map(|s| format!("{} {}", cs_ctype(cx.ns, &s.ty), safe_cs_name(&s.name))) .collect(); w.line(UNMANAGED_CALLERS_ONLY); w.line(format!( @@ -578,21 +506,10 @@ fn render_async( w.line(format!("ffiCall.SetError(err, {});", err.map)); w.line("return;"); }); - match &f.ret { - None => { - w.line("ffiCall.SetResult(true);"); - } - Some(ty) => { - let pair = a.callback_params.iter().any(|s| s.name == "result_len"); - let (ptr, len) = if pair { - ("result_ptr", "result_len") - } else { - ("result", "") - }; - let expr = receive(cx, ty, ptr, len, Own::Take); - w.line(format!("ffiCall.SetResult({expr});")); - } - } + w.line(format!( + "ffiCall.SetResult({});", + receive_result(cx, &a.result, value) + )); }); w.line("catch (Exception e)"); w.block("{", "}", |w| { @@ -602,92 +519,154 @@ fn render_async( w.blank(); } -/// An iterator wrapper: the launcher runs eagerly (so launch errors throw -/// at the call), and the returned single-use sequence pulls one item per -/// `MoveNext` through a static `Next{Name}` helper. The native iterator is a -/// `SafeHandle`, destroyed exactly once on exhaustion, disposal, or -/// finalization. +/// The locals `_next` writes an item into, the `&local` arguments passing +/// them (in slot order), and the expression receiving the item. +fn item_outs(cx: Cx<'_>, pass: &ItemPass, elem: &Ty) -> (Vec, Vec, String) { + let local = |slot: &AbiParam, name: &str| { + format!("{} {name} = default;", cs_ctype(cx.ns, pointee(slot))) + }; + match pass { + ItemPass::Direct { out_item } => ( + vec![local(out_item, "ffiItem")], + vec!["&ffiItem".into()], + "ffiItem".into(), + ), + ItemPass::OptDirect { out_has, out_item } => ( + vec![local(out_has, "ffiHas"), local(out_item, "ffiItem")], + vec!["&ffiHas".into(), "&ffiItem".into()], + "ffiHas ? ffiItem : null".into(), + ), + ItemPass::Slice { + out_item, out_len, .. + } => ( + vec![local(out_item, "ffiItem"), local(out_len, "ffiLen")], + vec!["&ffiItem".into(), "&ffiLen".into()], + "Ffi.TakeArray(ffiItem, ffiLen)".into(), + ), + ItemPass::String { out_item, out_len } => ( + vec![local(out_item, "ffiItem"), local(out_len, "ffiLen")], + vec!["&ffiItem".into(), "&ffiLen".into()], + "Ffi.TakeString(ffiItem, ffiLen)".into(), + ), + ItemPass::Bytes { out_item, out_len } => ( + vec![local(out_item, "ffiItem"), local(out_len, "ffiLen")], + vec!["&ffiItem".into(), "&ffiLen".into()], + "Ffi.TakeBytes(ffiItem, ffiLen)".into(), + ), + ItemPass::Buffer { out_item, out_len } => ( + vec![local(out_item, "ffiItem"), local(out_len, "ffiLen")], + vec!["&ffiItem".into(), "&ffiLen".into()], + format!( + "Ffi.TakeBuffer(ffiItem, ffiLen, {})", + read_lambda(cx, elem, 0) + ), + ), + ItemPass::Object { + out_item, + nullable, + interface, + .. + } => ( + vec![local(out_item, "ffiItem")], + vec!["&ffiItem".into()], + adopt(cx, "ffiItem", *nullable, interface), + ), + } +} + +/// An iterator wrapper: a re-enumerable `IEnumerable` whose every +/// enumeration launches a new native iterator through a `Launch{Name}` +/// helper and pulls one item per `MoveNext` through a static `Next{Name}`. +/// Span parameters are copied once, since the sequence outlives the call. +#[allow(clippy::too_many_arguments)] fn render_iterator( w: &mut CodeWriter, + docs: &Docs, f: &FnBinding, it: &IteratorBinding, name: &str, receiver: Receiver<'_>, - err: &ErrCtx<'_>, + err: &ErrCtx, + cx: Cx<'_>, ) { - let elem = cs_type(&it.elem); + let elem = item_cs(&it.item, &it.elem); + let launch = format!("Launch{name}"); let next = format!("Next{name}"); - let cx = err.cx; let m = Marshal::new(cx, &f.params); - let call = format!( - "NativeMethods.{}({})", - it.launch.symbol, - call_args(receiver, &m, &["&ffiErr".to_string()]) - ); - write_header(w, f, err); - w.line("/// Streams lazily, one native call per item. The sequence can be"); - w.line("/// enumerated once; disposing its enumerator (as foreach does)"); - w.line("/// releases the native iterator."); + write_header(w, docs, f, err); + w.line("/// Streams lazily, one native call per item. Each enumeration launches a"); + w.line("/// new native iterator, so a failure to start throws from"); + w.line("/// GetEnumerator (the start of a foreach); disposing the enumerator"); + w.line("/// (as foreach does) releases it."); let ret = format!("IEnumerable<{elem}>"); w.line(format!( "{}({})", opener(receiver, &ret, name), - params_sig(&f.params, false).join(", ") + params_sig(&f.params).join(", ") + )); + w.block("{", "}", |w| { + let mut args = Vec::new(); + for p in &f.params { + let n = camel(&p.name); + if matches!(p.pass, ArgPass::Slice { .. } | ArgPass::Bytes { .. }) { + let copy = format!("{}Copy", p.name.to_lower_camel_case()); + w.line(format!("var {copy} = {n}.ToArray();")); + args.push(copy); + } else { + args.push(n); + } + } + w.line(format!( + "return new FfiSequence<{elem}>(() => {launch}({}), &{next});", + args.join(", ") + )); + }); + w.blank(); + + // The launcher: a sync call returning the native iterator. + let call = format!( + "NativeMethods.{}({})", + f.abi.symbol, + call_args(receiver, &m, &["&ffiErr".to_string()]) + ); + let modifier = match receiver { + Receiver::Instance => "private", + _ => "private static", + }; + w.line(format!( + "{modifier} FfiIteratorHandle {launch}({})", + params_sig(&f.params).join(", ") )); w.block("{", "}", |w| { m.write_setup(w); w.line("var ffiErr = default(FfiError);"); m.pinned(w, |w| { - if m.callbacks.is_empty() { - w.line(format!("var ffiResult = {call};")); - } else { - w.line("IntPtr ffiResult;"); - w.line("try"); - w.block("{", "}", |w| { - w.line(format!("ffiResult = {call};")); - }); - w.line("catch"); - w.block("{", "}", |w| { - m.unregister(w); - w.line("throw;"); - }); - } + m.guarded( + w, + Some("IntPtr ffiResult;".into()), + &format!("ffiResult = {call};"), + ); w.line(err.check("ffiErr")); w.line(format!( - "return new FfiSequence<{elem}>(new FfiIteratorHandle(ffiResult, &NativeMethods.{}), &{next});", + "return new FfiIteratorHandle(ffiResult, &NativeMethods.{});", it.destroy_symbol )); }); }); w.blank(); - // `_next` out-slots after the iterator handle, excluding `out_err`. - let outs: Vec<&AbiParam> = it - .next - .params - .iter() - .skip(1) - .filter(|s| s.name != "out_err") - .collect(); + // `_next`: one item per call; 0 means exhausted. + let (locals, mut args, item) = item_outs(cx, &it.item, &it.elem); + args.insert(0, "iter".into()); + args.push("&ffiErr".into()); w.line(format!( "private static bool {next}(FfiIteratorHandle iter, out {elem} item)" )); w.block("{", "}", |w| { - let mut args = vec!["iter".to_string()]; - for s in &outs { - let CType::Ptr { pointee, .. } = &s.ty else { - unreachable!("iterator out-slots are pointers") - }; - let local = if s.name == "out_item" { - "ffiItem" - } else { - "ffiLen" - }; - w.line(format!("{} {local} = default;", cs_ctype(pointee))); - args.push(format!("&{local}")); + for l in &locals { + w.line(l); } - args.push("&ffiErr".to_string()); w.line("var ffiErr = default(FfiError);"); w.line(format!( "var ffiMore = NativeMethods.{}({});", @@ -700,32 +679,36 @@ fn render_iterator( w.line("item = default!;"); w.line("return false;"); }); - let expr = receive(cx, &it.elem, "ffiItem", "ffiLen", Own::Take); - w.line(format!("item = {expr};")); + w.line(format!("item = {item};")); w.line("return true;"); }); w.blank(); } -/// Render one module's static class. Submodules become sibling classes named -/// by their full path (`KvStats`), so a module class never shadows a type. +/// The C# class of a module's free functions: its full path in PascalCase +/// (`KvStats` for `kv.stats`), so a module class never shadows a type. +pub(crate) fn module_class(m: &ModuleBinding) -> String { + m.segments.iter().map(|s| s.to_upper_camel_case()).collect() +} + +/// Render one module's static class. pub(crate) fn render_module_class( w: &mut CodeWriter, + model: &Model, + docs: &Docs, m: &ModuleBinding, - error: Option<&ErrorBinding>, cx: Cx<'_>, ) { if m.functions.is_empty() { return; } - let class: String = m.segments.iter().map(|s| s.to_upper_camel_case()).collect(); - write_doc(w, &m.doc); - w.line(format!("public static unsafe class {class}")); + docs.summary(w, &m.doc); + w.line(format!("public static unsafe class {}", module_class(m))); w.block("{", "}", |w| { for f in &m.functions { let name = f.name.to_upper_camel_case(); - let err = ErrCtx::for_fn(f, error, cx); - render_callable(w, f, &name, Receiver::Static, &err); + let err = ErrCtx::new(model, &f.error, cx); + render_callable(w, docs, f, &name, Receiver::Static, &err, cx); } }); w.blank(); diff --git a/crates/weaveffi-cli/src/targets/dotnet/codec.rs b/crates/weaveffi-cli/src/targets/dotnet/codec.rs index b8cf7452..9abfaac1 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/codec.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/codec.rs @@ -1,252 +1,193 @@ -//! Value-buffer codec: the C# statement writing and the expression reading -//! every wire shape, dispatched on [`Ty::wire`], and the internal -//! `FfiCodecs` class, which holds one `Write`/`Read` pair per distinct -//! composite type (`[Entry]`, `{string:i64}`, `Shape?`) so no call site -//! inlines a loop. +//! Value-buffer codec and value equality expressions. //! -//! Records and rich enums carry their own `WriteTo`/`ReadFrom` pair. The -//! runtime's `FfiBufferWriter`/`FfiBufferReader` name one method per -//! [`Prim`](weaveffi_model::ty::Prim) (`WriteI32`, `ReadString`), so -//! primitives need a single arm. An object token carries one strong -//! reference: writing one clones the wrapper's handle through the -//! interface's `_clone` symbol, and reading one adopts the pointer into a -//! new wrapper. +//! Nothing here is monomorphized per composite type: optionals, lists, and +//! maps go through the runtime's generic `FfiBufferWriter.WriteList` / +//! `FfiBufferReader.ReadList` (and friends), handed a `static` lambda for +//! the element, so `[{string:[i32]}]` is three nested calls rather than a +//! generated method per shape. Records and rich enums carry their own +//! `WriteTo`/`ReadFrom` pair. An object token carries one strong reference: +//! writing one clones the wrapper's handle, and reading one adopts the +//! pointer into a new wrapper. -use std::collections::BTreeMap; +use weaveffi_model::ty::{Prim, Ty}; -use crate::codegen::CodeWriter; -use weaveffi_model::model::{FieldBinding, Model, ParamBinding}; -use weaveffi_model::ty::{Ty, WireType}; +use crate::targets::dotnet::types::{cs_type, is_value_type, Cx}; -use crate::targets::dotnet::types::{cs_type, Cx}; - -/// The name of a composite's codec methods after `Write`/`Read`, spelled in -/// prefix order so it's unambiguous: `[Entry]` is `ListOfEntry`, -/// `{string:[i32]}` is `MapOfStringToListOfI32`, `Shape?` is -/// `OptionalOfShape`. -pub(crate) fn codec_name(t: &Ty) -> String { - match t { - Ty::Prim(p) => p.pascal().to_string(), - Ty::Optional(inner) => format!("OptionalOf{}", codec_name(inner)), - Ty::List(inner) => format!("ListOf{}", codec_name(inner)), - Ty::Map(k, v) => format!("MapOf{}To{}", codec_name(k), codec_name(v)), - other => other - .user_name() - .or_else(|| other.interface_name()) - .expect("only named types remain") - .to_string(), +/// The C# expression (a `void` call) writing `value`, the surface value of +/// `ty`, into the writer `writer`. `depth` numbers the lambda parameters of +/// nested composites so they never shadow each other. +pub(crate) fn write_call(ty: &Ty, writer: &str, value: &str, depth: usize) -> String { + match ty { + Ty::Prim(p) => format!("{writer}.Write{}({value})", p.pascal()), + Ty::Enum(_) => format!("{writer}.WriteI32((int){value})"), + Ty::Interface(_) => format!("{writer}.WriteObject({value}.CloneHandle())"), + Ty::Record(_) | Ty::RichEnum(_) => format!("{value}.WriteTo({writer})"), + Ty::Optional(inner) => { + let method = if is_value_type(inner) { + "WriteOptionalValue" + } else { + "WriteOptional" + }; + format!("{writer}.{method}({value}, {})", write_lambda(inner, depth)) + } + Ty::List(inner) => format!( + "{writer}.WriteList({value}, {})", + write_lambda(inner, depth) + ), + Ty::Map(k, v) => format!( + "{writer}.WriteMap({value}, {}, {})", + write_lambda(k, depth), + write_lambda(v, depth) + ), } } -/// The C# statement writing `expr` (the surface value of `ty`) into the -/// writer `writer`. -/// -/// An object is written as a token carrying a new strong reference -/// (`CloneHandle()`), so the encoding never hands over the reference the -/// wrapper still owns. -pub(crate) fn write_stmt(ty: &Ty, writer: &str, expr: &str) -> String { - match ty.wire() { - WireType::Prim(p) => format!("{writer}.Write{}({expr});", p.pascal()), - WireType::Enum(_) => format!("{writer}.WriteI32((int){expr});"), - WireType::Object(_) => format!("{writer}.WriteObject({expr}.CloneHandle());"), - WireType::User(_) => format!("{expr}.WriteTo({writer});"), - WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("FfiCodecs.Write{}({writer}, {expr});", codec_name(ty)) - } - } +/// A `static` lambda writing one value of `ty`: the element writer handed to +/// the runtime's generic composite methods. +pub(crate) fn write_lambda(ty: &Ty, depth: usize) -> String { + let (w, v) = (format!("w{depth}"), format!("v{depth}")); + format!("static ({w}, {v}) => {}", write_call(ty, &w, &v, depth + 1)) } /// The C# expression reading a value of `ty` from the reader `reader`; the -/// inverse of [`write_stmt`]. An object token is adopted into a new +/// inverse of [`write_call`]. An object token is adopted into a new /// wrapper, which owes the reference's release. -pub(crate) fn read_expr(cx: Cx<'_>, ty: &Ty, reader: &str) -> String { - match ty.wire() { - WireType::Prim(p) => format!("{reader}.Read{}()", p.pascal()), - WireType::Enum(name) => format!("({}){reader}.ReadI32()", cx.ty(name)), - WireType::Object(name) => format!("{}.Adopt({reader}.ReadObject())", cx.ty(name)), - WireType::User(name) => format!("{}.ReadFrom({reader})", cx.ty(name)), - WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("FfiCodecs.Read{}({reader})", codec_name(ty)) - } +pub(crate) fn read_expr(cx: Cx<'_>, ty: &Ty, reader: &str, depth: usize) -> String { + match ty { + Ty::Prim(p) => format!("{reader}.Read{}()", p.pascal()), + Ty::Enum(name) => format!("({}){reader}.ReadI32()", cx.ty(name)), + Ty::Interface(name) => format!("{}.Adopt({reader}.ReadObject())", cx.ty(name)), + Ty::Record(name) | Ty::RichEnum(name) => format!("{}.ReadFrom({reader})", cx.ty(name)), + Ty::Optional(inner) => { + let method = if is_value_type(inner) { + "ReadOptionalValue" + } else { + "ReadOptional" + }; + format!("{reader}.{method}({})", read_lambda(cx, inner, depth)) + } + Ty::List(inner) => format!("{reader}.ReadList({})", read_lambda(cx, inner, depth)), + // A dictionary isn't covariant in its value type, so the reader + // builds it with the surface types spelled out. + Ty::Map(k, v) => format!( + "{reader}.ReadMap<{}, {}>({}, {})", + cs_type(k), + cs_type(v), + read_lambda(cx, k, depth), + read_lambda(cx, v, depth) + ), } } -/// The C# expression decoding a whole value buffer of `ty` from the reader -/// expression `reader` and checking that nothing trails it. -pub(crate) fn decode_expr(cx: Cx<'_>, ty: &Ty, reader: &str) -> String { - format!( - "FfiCodecs.Decode({reader}, static r => {})", - read_expr(cx, ty, "r") - ) +/// A `static` lambda reading one value of `ty`. +pub(crate) fn read_lambda(cx: Cx<'_>, ty: &Ty, depth: usize) -> String { + let r = format!("r{depth}"); + format!("static {r} => {}", read_expr(cx, ty, &r, depth + 1)) } -/// Every distinct composite type (optional, list, or map) that crosses -/// inside a value buffer, by codec name. -fn composites(model: &Model) -> BTreeMap { - /// A type in a top-level position: a parameter, return, or iterator - /// element. `Interface?` and `Cb?` aren't buffers there. - fn top(t: &Ty, out: &mut BTreeMap) { - match t { - Ty::Iterator(elem) => top(elem, out), - _ if t.is_buffered() => nested(t, out), - _ => {} - } - } - /// A type inside a value buffer, where every optional is encoded. - fn nested(t: &Ty, out: &mut BTreeMap) { - match t { - Ty::Optional(inner) | Ty::List(inner) => { - out.insert(codec_name(t), t.clone()); - nested(inner, out); - } - Ty::Map(k, v) => { - out.insert(codec_name(t), t.clone()); - nested(k, out); - nested(v, out); - } - _ => {} - } +/// True when the surface type of `ty` compares by reference in C# (a byte +/// array, a list, or a map, possibly optional), so a record's value +/// equality has to compare its contents explicitly. +pub(crate) fn needs_deep_equality(ty: &Ty) -> bool { + match ty { + Ty::Prim(Prim::Bytes) | Ty::List(_) | Ty::Map(..) => true, + Ty::Optional(inner) => needs_deep_equality(inner), + _ => false, } - fn fields(fs: &[FieldBinding], out: &mut BTreeMap) { - for f in fs { - nested(&f.ty, out); - } - } - fn signature(ps: &[ParamBinding], ret: &Option, out: &mut BTreeMap) { - for p in ps { - top(&p.ty, out); - } - if let Some(t) = ret { - top(t, out); - } +} + +/// The C# expression comparing two values of `ty` by value: contents for +/// byte arrays, lists, and maps, default equality for everything else. +pub(crate) fn equal_expr(ty: &Ty, a: &str, b: &str, depth: usize) -> String { + match ty { + Ty::Prim(Prim::Bytes) => format!("FfiEquality.Bytes({a}, {b})"), + Ty::Optional(inner) if needs_deep_equality(inner) => equal_expr(inner, a, b, depth), + Ty::List(inner) => match equal_lambda(inner, depth) { + Some(eq) => format!("FfiEquality.Lists({a}, {b}, {eq})"), + None => format!("FfiEquality.Lists({a}, {b})"), + }, + Ty::Map(_, v) => match equal_lambda(v, depth) { + Some(eq) => format!("FfiEquality.Maps({a}, {b}, {eq})"), + None => format!("FfiEquality.Maps({a}, {b})"), + }, + _ => format!("FfiEquality.Equal({a}, {b})"), } - let mut out = BTreeMap::new(); - for m in &model.modules { - for s in &m.structs { - fields(&s.fields, &mut out); - } - for e in &m.enums { - for v in &e.variants { - fields(&v.fields, &mut out); - } - } - if let Some(eb) = &m.errors { - for c in &eb.codes { - fields(&c.fields, &mut out); - } - } - for f in m.callables() { - signature(&f.params, &f.ret, &mut out); - } - for cb in &m.callback_interfaces { - for f in &cb.methods { - signature(&f.params, &f.ret, &mut out); - } - } +} + +/// The element comparer of a collection whose elements need content +/// equality, or `None` when default equality does. +fn equal_lambda(ty: &Ty, depth: usize) -> Option { + needs_deep_equality(ty).then(|| { + let (x, y) = (format!("x{depth}"), format!("y{depth}")); + format!("static ({x}, {y}) => {}", equal_expr(ty, &x, &y, depth + 1)) + }) +} + +/// The C# expression a record's `GetHashCode` adds for one field: the value +/// itself, or a hash consistent with [`equal_expr`] for collections. +pub(crate) fn hash_expr(ty: &Ty, value: &str) -> String { + match ty { + Ty::Prim(Prim::Bytes) => format!("FfiEquality.BytesHash({value})"), + Ty::List(_) => format!("FfiEquality.ListHash({value})"), + Ty::Map(..) => format!("FfiEquality.MapHash({value})"), + Ty::Optional(inner) if needs_deep_equality(inner) => hash_expr(inner, value), + _ => value.to_string(), } - out } -/// Render the internal `FfiCodecs` class: `Decode`, which reads a whole -/// buffer, plus one `Write{Name}`/`Read{Name}` pair per composite type. -/// -/// A list or map count isn't bounded by the remaining input (elements can -/// encode to zero bytes), so a read grows its collection from a capped -/// capacity instead of preallocating whatever the count claims. A map that -/// repeats a key is malformed. -pub(crate) fn render_codecs(w: &mut CodeWriter, cx: Cx<'_>, model: &Model) { - w.line("/// The value-buffer codec of every composite type the API"); - w.line("/// carries."); - w.line("internal static class FfiCodecs"); - w.block("{", "}", |w| { - w.line("/// Reads one value that must fill the whole buffer."); - w.line( - "internal static T Decode(FfiBufferReader reader, Func read)", +#[cfg(test)] +mod tests { + use super::*; + + fn list(t: Ty) -> Ty { + Ty::List(Box::new(t)) + } + + #[test] + fn nested_composites_are_nested_generic_calls() { + let cx = Cx { + ns: "Kv", + base: "NativeException", + bug: "NativeBugException", + }; + let ty = Ty::Map( + Box::new(Ty::Prim(Prim::String)), + Box::new(list(Ty::Optional(Box::new(Ty::Prim(Prim::I32))))), ); - w.block("{", "}", |w| { - w.line("var value = read(reader);"); - w.line("reader.ExpectEnd();"); - w.line("return value;"); - }); - for t in composites(model).values() { - w.blank(); - render_pair(w, cx, t); - } - }); - w.blank(); -} + assert_eq!( + write_call(&ty, "writer", "Value", 0), + "writer.WriteMap(Value, static (w0, v0) => w0.WriteString(v0), \ + static (w0, v0) => w0.WriteList(v0, static (w1, v1) => \ + w1.WriteOptionalValue(v1, static (w2, v2) => w2.WriteI32(v2))))" + ); + assert_eq!( + read_expr(cx, &ty, "reader", 0), + "reader.ReadMap>(static r0 => r0.ReadString(), \ + static r0 => r0.ReadList(static r1 => r1.ReadOptionalValue(static r2 => r2.ReadI32())))" + ); + } -/// One composite's `Write{Name}`/`Read{Name}` pair. -fn render_pair(w: &mut CodeWriter, cx: Cx<'_>, t: &Ty) { - let name = codec_name(t); - let cs = cs_type(t); - w.line(format!( - "internal static void Write{name}(FfiBufferWriter writer, {cs} value)" - )); - w.block("{", "}", |w| match t { - Ty::Optional(inner) => { - w.line("writer.WriteBool(value != null);"); - w.line("if (value is { } present)"); - w.block("{", "}", |w| { - w.line(write_stmt(inner, "writer", "present")); - }); - } - Ty::List(inner) => { - w.line("writer.WriteLen(value.Length);"); - w.line("foreach (var item in value)"); - w.block("{", "}", |w| { - w.line(write_stmt(inner, "writer", "item")); - }); - } - Ty::Map(k, v) => { - w.line("writer.WriteLen(value.Count);"); - w.line("foreach (var entry in value)"); - w.block("{", "}", |w| { - w.line(write_stmt(k, "writer", "entry.Key")); - w.line(write_stmt(v, "writer", "entry.Value")); - }); - } - _ => unreachable!("only composites are collected"), - }); - w.blank(); - w.line(format!( - "internal static {cs} Read{name}(FfiBufferReader reader)" - )); - w.block("{", "}", |w| match t { - Ty::Optional(inner) => { - w.line(format!( - "return reader.ReadBool() ? {} : null;", - read_expr(cx, inner, "reader") - )); - } - Ty::List(inner) => { - w.line("var count = reader.ReadLen();"); - w.line(format!( - "var list = new List<{}>(reader.Capacity(count));", - cs_type(inner) - )); - w.line("for (var i = 0; i < count; i++)"); - w.block("{", "}", |w| { - w.line(format!("list.Add({});", read_expr(cx, inner, "reader"))); - }); - w.line("return list.ToArray();"); - } - Ty::Map(k, v) => { - w.line("var count = reader.ReadLen();"); - w.line(format!("var map = new {cs}(reader.Capacity(count));")); - w.line("for (var i = 0; i < count; i++)"); - w.block("{", "}", |w| { - w.line(format!("var key = {};", read_expr(cx, k, "reader"))); - w.line(format!( - "if (!map.TryAdd(key, {}))", - read_expr(cx, v, "reader") - )); - w.block("{", "}", |w| { - w.line("throw FfiBufferReader.Malformed(\"repeated map key\");"); - }); - }); - w.line("return map;"); - } - _ => unreachable!("only composites are collected"), - }); + #[test] + fn equality_compares_collection_contents() { + let bytes = Ty::Prim(Prim::Bytes); + assert_eq!( + equal_expr(&list(bytes.clone()), "A", "o.A", 0), + "FfiEquality.Lists(A, o.A, static (x0, y0) => FfiEquality.Bytes(x0, y0))" + ); + assert_eq!( + equal_expr(&list(Ty::Prim(Prim::I32)), "A", "o.A", 0), + "FfiEquality.Lists(A, o.A)" + ); + assert_eq!( + equal_expr(&Ty::Prim(Prim::F64), "A", "o.A", 0), + "FfiEquality.Equal(A, o.A)" + ); + assert!(!needs_deep_equality(&Ty::Optional(Box::new(Ty::Prim( + Prim::I64 + ))))); + assert_eq!( + hash_expr(&Ty::Optional(Box::new(bytes)), "B"), + "FfiEquality.BytesHash(B)" + ); + } } diff --git a/crates/weaveffi-cli/src/targets/dotnet/docs.rs b/crates/weaveffi-cli/src/targets/dotnet/docs.rs index fda17f9e..bc1ebdf1 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/docs.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/docs.rs @@ -1,20 +1,108 @@ -//! XML doc-comment emission: `` blocks for items and the -//! `` plus `` set for callables. +//! XML doc-comment emission, with backticked API identifiers in IDL docs +//! rewritten to their C# spelling (`` `new_op` `` becomes `NewOp`). +use std::collections::{HashMap, HashSet}; + +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use weaveffi_model::errors; +use weaveffi_model::model::Model; + +use crate::codegen::docs::{self, ApiNames, Doc, IdentKind}; use crate::codegen::CodeWriter; -use weaveffi_model::model::ParamBinding; +use crate::targets::dotnet::types::{callback_interface_cs, cs_str, xml_text}; + +/// Renders IDL doc and deprecation text in C# terms. +pub(crate) struct Docs { + names: ApiNames, + callbacks: HashSet, + codes: HashMap, +} + +impl Docs { + /// Index every identifier `model` declares. + pub(crate) fn new(model: &Model) -> Self { + let callbacks = model + .callback_interfaces() + .map(|(_, cb)| cb.name.clone()) + .collect(); + let codes = model + .error_domains() + .flat_map(|(_, d)| { + let exc = errors::exception_type_name(&d.name); + d.codes + .iter() + .map(move |c| (c.name.clone(), format!("{exc}.{}", errors::pascal(&c.name)))) + }) + .collect(); + Self { + names: ApiNames::new(model), + callbacks, + codes, + } + } + + /// The C# spelling of an API identifier, or `None` to keep it as + /// written (an ambiguous name, or a module). + fn spell(&self, ident: &str) -> Option { + Some(match self.names.kind(ident)? { + IdentKind::Function + | IdentKind::Member + | IdentKind::CallbackMethod + | IdentKind::Field => ident.to_upper_camel_case(), + IdentKind::Param => ident.to_lower_camel_case(), + IdentKind::Type if self.callbacks.contains(ident) => callback_interface_cs(ident), + IdentKind::ErrorDomain => errors::exception_type_name(ident), + IdentKind::ErrorCode => self.codes.get(ident)?.clone(), + IdentKind::Type | IdentKind::Variant | IdentKind::Module => return None, + }) + } -use crate::targets::dotnet::types::xml_text; + /// `text` as XML doc content: escaped, with every identifier code span + /// rendered as `` in C# spelling. + pub(crate) fn xml(&self, text: &str) -> String { + docs::map_code_spans(&xml_text(text), |ident| { + Some(format!( + "{}", + self.spell(ident).unwrap_or_else(|| ident.to_string()) + )) + }) + } + + /// The `[Obsolete("...")]` line for a deprecated item. + pub(crate) fn obsolete(&self, w: &mut CodeWriter, deprecated: &Option) { + let none: Option = None; + if let Some(msg) = Doc::new(&none, deprecated).deprecation(|i| self.spell(i)) { + w.line(format!("[Obsolete(\"{}\")]", cs_str(&msg))); + } + } + + /// Emit a `` for an item's doc comment, if it has one. + pub(crate) fn summary(&self, w: &mut CodeWriter, doc: &Option) { + if let Some(d) = trimmed(doc) { + element(w, "", "", &self.xml(d)); + } + } + + /// Emit one `` (or any named element) for a documented item. + /// `name` must already be the C# spelling (without the `@` escape, which + /// XML doc references omit). + pub(crate) fn param(&self, w: &mut CodeWriter, name: &str, doc: &Option) { + if let Some(d) = trimmed(doc) { + let open = format!("", name.trim_start_matches('@')); + element(w, &open, "", &self.xml(d)); + } + } +} /// The trimmed, non-empty text of an optional doc comment. -fn text(doc: &Option) -> Option<&str> { +fn trimmed(doc: &Option) -> Option<&str> { doc.as_deref().map(str::trim).filter(|d| !d.is_empty()) } -/// Emit one XML element (``, ``) holding `body`: -/// on one line when the body is a single line, else one `///` line each. -fn element(w: &mut CodeWriter, open: &str, close: &str, body: &str) { - let body = xml_text(body); +/// Emit one XML element (``, ``) holding `body`, +/// which is already XML: on one line when it's a single line, else one +/// `///` line each. +pub(crate) fn element(w: &mut CodeWriter, open: &str, close: &str, body: &str) { if body.contains('\n') { w.line(format!("/// {open}")); for line in body.lines() { @@ -26,22 +114,38 @@ fn element(w: &mut CodeWriter, open: &str, close: &str, body: &str) { } } -/// Emit a `` for an item's doc comment, if it has one. -pub(crate) fn write_doc(w: &mut CodeWriter, doc: &Option) { - if let Some(d) = text(doc) { - element(w, "", "", d); - } -} +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::test_model; -/// Emit a callable's `` plus one `` per documented -/// parameter. Parameter names must already be the C# spelling (without -/// the `@` escape, which XML doc references omit). -pub(crate) fn write_fn_doc(w: &mut CodeWriter, doc: &Option, params: &[ParamBinding]) { - write_doc(w, doc); - for p in params { - if let Some(d) = text(&p.doc) { - let open = format!("", p.name); - element(w, &open, "", d); - } + #[test] + fn identifiers_take_their_csharp_spelling() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: kv + errors: + - name: KvError + codes: [{ name: not_found, code: 1, message: missing }] + callback_interfaces: + - name: Listener + methods: [{ name: on_put, params: [{ name: new_key, type: string }] }] + functions: + - name: new_op + params: [{ name: the_key, type: string }] + return: i32 +"#, + ); + let docs = Docs::new(&model); + assert_eq!( + docs.xml("Use `new_op` with `the_key`, a `Listener`, `on_put`, & `KvError`."), + "Use NewOp with theKey, a IListener, OnPut, & KvException." + ); + assert_eq!( + docs.xml("Fails with `not_found` or `a + b`."), + "Fails with KvException.NotFound or `a + b`." + ); } } diff --git a/crates/weaveffi-cli/src/targets/dotnet/entities.rs b/crates/weaveffi-cli/src/targets/dotnet/entities.rs index 8b9653e3..e3727eb2 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/entities.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/entities.rs @@ -1,130 +1,174 @@ //! Entity renderers: C-style enums, records, rich (algebraic) enums, and the //! interface wrapper classes. -use crate::codegen::CodeWriter; -use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_model::model::{ - CallShape, EnumBinding, EnumVariantBinding, ErrorBinding, FieldBinding, InterfaceBinding, - StructBinding, + CallShape, EnumBinding, FieldBinding, InterfaceBinding, Model, StructBinding, }; -use crate::targets::dotnet::calls::{render_callable, write_obsolete, ErrCtx, Receiver}; -use crate::targets::dotnet::codec::{read_expr, write_stmt}; -use crate::targets::dotnet::docs::write_doc; -use crate::targets::dotnet::types::{cs_member, cs_type, safe_cs_name, Cx}; +use crate::codegen::CodeWriter; +use crate::targets::dotnet::calls::{render_callable, Receiver}; +use crate::targets::dotnet::codec::{ + equal_expr, hash_expr, needs_deep_equality, read_expr, write_call, +}; +use crate::targets::dotnet::docs::Docs; +use crate::targets::dotnet::errors::ErrCtx; +use crate::targets::dotnet::types::{cs_member, cs_type, field_cs, Cx}; -/// Render a C-style enum as a C# `enum` with its ABI discriminants. -pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { - write_doc(w, &e.doc); - write_obsolete(w, &e.deprecated); +/// Render a C-style enum as a C# `enum` with its ABI discriminants (an +/// `int` underneath, like the C typedef). +pub(crate) fn render_enum(w: &mut CodeWriter, docs: &Docs, e: &EnumBinding) { + docs.summary(w, &e.doc); + docs.obsolete(w, &e.deprecated); w.line(format!("public enum {}", e.name)); w.block("{", "}", |w| { for v in &e.variants { - write_doc(w, &v.doc); + docs.summary(w, &v.doc); w.line(format!("{} = {},", v.name, v.value)); } }); w.blank(); } -/// The get-only properties and positional constructor shared by records and -/// rich-enum variants. -fn render_value_members(w: &mut CodeWriter, class: &str, fields: &[FieldBinding]) { +/// The positional parameter list of a record or variant: one PascalCase +/// property per field. +fn positional(class: &str, fields: &[FieldBinding]) -> String { + fields + .iter() + .map(|f| format!("{} {}", cs_type(&f.ty), field_cs(&f.name, class))) + .collect::>() + .join(", ") +} + +/// The `` docs of a positional record's properties. +fn field_docs(w: &mut CodeWriter, docs: &Docs, class: &str, fields: &[FieldBinding]) { for f in fields { - write_doc(w, &f.doc); - w.line(format!( - "public {} {} {{ get; }}", - cs_type(&f.ty), - f.name.to_upper_camel_case() - )); - w.blank(); + docs.param(w, &field_cs(&f.name, class), &f.doc); } - let params: Vec = fields - .iter() - .map(|f| { - format!( - "{} {}", - cs_type(&f.ty), - safe_cs_name(&f.name.to_lower_camel_case()) - ) - }) - .collect(); - w.line(format!( - "/// Creates a from every field." - )); - w.line(format!("public {class}({})", params.join(", "))); +} + +/// The value equality of a record whose fields include a byte array, list, +/// or map, which a record would otherwise compare by reference: `Equals` +/// compares their contents, and `GetHashCode` agrees with it. +fn render_equality(w: &mut CodeWriter, class: &str, fields: &[FieldBinding]) { + if !fields.iter().any(|f| needs_deep_equality(&f.ty)) { + return; + } + w.line("/// True when every field of is equal to this"); + w.line("/// one's, comparing byte arrays, lists, and maps by their contents."); + w.line(format!("public bool Equals({class}? other)")); + w.block("{", "}", |w| { + w.line("return ReferenceEquals(this, other) || other is not null"); + w.indent(); + for (i, f) in fields.iter().enumerate() { + let p = field_cs(&f.name, class); + let end = if i + 1 == fields.len() { ";" } else { "" }; + w.line(format!( + "&& {}{end}", + equal_expr(&f.ty, &p, &format!("other.{p}"), 0) + )); + } + w.dedent(); + }); + w.blank(); + w.line("/// "); + w.line("public override int GetHashCode()"); w.block("{", "}", |w| { + w.line("var hash = new global::System.HashCode();"); for f in fields { w.line(format!( - "{} = {};", - f.name.to_upper_camel_case(), - safe_cs_name(&f.name.to_lower_camel_case()) + "hash.Add({});", + hash_expr(&f.ty, &field_cs(&f.name, class)) )); } + w.line("return hash.ToHashCode();"); }); + w.blank(); } -/// Emit `return new {class}(...)` reading every field in declaration (and -/// wire) order. C# evaluates arguments left to right, so the reads happen in -/// order. -fn render_field_reads(w: &mut CodeWriter, cx: Cx<'_>, class: &str, fields: &[FieldBinding]) { - if fields.is_empty() { - w.line(format!("return new {class}();")); - return; - } - w.line(format!("return new {class}(")); - w.indent(); - for (i, f) in fields.iter().enumerate() { - let sep = if i + 1 == fields.len() { ");" } else { "," }; - w.line(format!("{}{sep}", read_expr(cx, &f.ty, "reader"))); +/// `new {class}(...)` reading every field in declaration (and wire) order, +/// one argument per line after the first line when there are several. C# +/// evaluates arguments left to right, so the reads happen in order. +fn construct(cx: Cx<'_>, class: &str, fields: &[FieldBinding], indent: &str) -> String { + let reads: Vec = fields + .iter() + .map(|f| read_expr(cx, &f.ty, "reader", 0)) + .collect(); + if reads.len() < 2 { + return format!("new {class}({})", reads.join("")); } - w.dedent(); + let sep = format!(",\n{indent} "); + format!("new {class}(\n{indent} {})", reads.join(&sep)) } -/// Render a record as a sealed data class: get-only properties, a -/// positional constructor, and the internal `WriteTo`/`ReadFrom` pair +/// Render a record as a positional `sealed record` (value equality, `with`, +/// deconstruction), plus the internal `WriteTo`/`ReadFrom` pair /// implementing its value-buffer encoding (fields in declaration order). -pub(crate) fn render_record(w: &mut CodeWriter, cx: Cx<'_>, s: &StructBinding) { - write_doc(w, &s.doc); - write_obsolete(w, &s.deprecated); - w.line(format!("public sealed class {}", s.name)); +pub(crate) fn render_record(w: &mut CodeWriter, cx: Cx<'_>, docs: &Docs, s: &StructBinding) { + let name = &s.name; + docs.summary(w, &s.doc); + field_docs(w, docs, name, &s.fields); + docs.obsolete(w, &s.deprecated); + w.line(format!( + "public sealed record {name}({})", + positional(name, &s.fields) + )); w.block("{", "}", |w| { - render_value_members(w, &s.name, &s.fields); - w.blank(); + render_equality(w, name, &s.fields); w.line("internal void WriteTo(FfiBufferWriter writer)"); w.block("{", "}", |w| { for f in &s.fields { - w.line(write_stmt(&f.ty, "writer", &f.name.to_upper_camel_case())); + let value = field_cs(&f.name, name); + w.line(format!("{};", write_call(&f.ty, "writer", &value, 0))); } }); w.blank(); w.line(format!( - "internal static {} ReadFrom(FfiBufferReader reader)", - s.name + "internal static {name} ReadFrom(FfiBufferReader reader)" )); w.block("{", "}", |w| { - render_field_reads(w, cx, &s.name, &s.fields); + let indent = w.indent_str(); + w.line(format!( + "return {};", + construct(cx, name, &s.fields, &indent) + )); }); }); w.blank(); } -/// Render a rich enum as a closed class hierarchy: an abstract base with a -/// private constructor and one nested sealed class per variant -/// (`Shape.Circle`). The base hosts the codec: an `i32` tag, then the active -/// variant's fields. -pub(crate) fn render_rich_enum(w: &mut CodeWriter, cx: Cx<'_>, e: &EnumBinding) { +/// Render a rich enum as a closed record hierarchy: an abstract record with +/// a private constructor and one nested sealed record per variant +/// (`Shape.Circle`). The base hosts the codec: an `i32` tag, then the +/// active variant's fields. +pub(crate) fn render_rich_enum(w: &mut CodeWriter, cx: Cx<'_>, docs: &Docs, e: &EnumBinding) { let name = &e.name; - write_doc(w, &e.doc); - write_obsolete(w, &e.deprecated); - w.line(format!("public abstract class {name}")); + docs.summary(w, &e.doc); + docs.obsolete(w, &e.deprecated); + w.line(format!("public abstract record {name}")); w.block("{", "}", |w| { w.line(format!("private {name}()")); w.line("{"); w.line("}"); w.blank(); for v in &e.variants { - render_variant(w, name, v); + docs.summary(w, &v.doc); + field_docs(w, docs, &v.name, &v.fields); + if v.fields.is_empty() { + w.line(format!("public sealed record {} : {name};", v.name)); + } else { + w.line(format!( + "public sealed record {}({}) : {name}", + v.name, + positional(&v.name, &v.fields) + )); + if v.fields.iter().any(|f| needs_deep_equality(&f.ty)) { + w.block("{", "}", |w| render_equality(w, &v.name, &v.fields)); + } else { + w.line("{"); + w.line("}"); + } + } + w.blank(); } w.line("internal void WriteTo(FfiBufferWriter writer)"); w.block("{", "}", |w| { @@ -136,8 +180,8 @@ pub(crate) fn render_rich_enum(w: &mut CodeWriter, cx: Cx<'_>, e: &EnumBinding) w.scope(|w| { w.line(format!("writer.WriteI32({});", v.value)); for f in &v.fields { - let expr = format!("v.{}", f.name.to_upper_camel_case()); - w.line(write_stmt(&f.ty, "writer", &expr)); + let value = format!("v.{}", field_cs(&f.name, &v.name)); + w.line(format!("{};", write_call(&f.ty, "writer", &value, 0))); } w.line("break;"); }); @@ -156,38 +200,27 @@ pub(crate) fn render_rich_enum(w: &mut CodeWriter, cx: Cx<'_>, e: &EnumBinding) )); w.block("{", "}", |w| { w.line("var tag = reader.ReadI32();"); - w.line("switch (tag)"); - w.block("{", "}", |w| { - for v in &e.variants { - w.line(format!("case {}:", v.value)); - w.block("{", "}", |w| { - render_field_reads(w, cx, &v.name, &v.fields); - }); - } - w.line("default:"); - w.scope(|w| { - w.line(format!( - "throw FfiBufferReader.Malformed(\"unknown {name} tag \" + tag);" - )); - }); - }); + w.line("return tag switch"); + w.line("{"); + w.indent(); + let indent = w.indent_str(); + for v in &e.variants { + w.line(format!( + "{} => {},", + v.value, + construct(cx, &v.name, &v.fields, &indent) + )); + } + w.line(format!( + "_ => throw FfiBufferReader.Malformed(\"unknown {name} tag \" + tag)," + )); + w.dedent(); + w.line("};"); }); }); w.blank(); } -/// One nested sealed variant class of a rich enum. -fn render_variant(w: &mut CodeWriter, enum_name: &str, v: &EnumVariantBinding) { - write_doc(w, &v.doc); - w.line(format!("public sealed class {} : {enum_name}", v.name)); - w.block("{", "}", |w| { - if !v.fields.is_empty() { - render_value_members(w, &v.name, &v.fields); - } - }); - w.blank(); -} - /// Render one interface as a sealed wrapper over a `SafeHandle` subclass /// holding one strong reference. The handle's `ReleaseHandle` calls the /// interface's `_destroy`; every call passes the handle itself, so the @@ -196,13 +229,14 @@ fn render_variant(w: &mut CodeWriter, enum_name: &str, v: &EnumVariantBinding) { /// reference the same native object. pub(crate) fn render_interface( w: &mut CodeWriter, + model: &Model, + docs: &Docs, i: &InterfaceBinding, - error: Option<&ErrorBinding>, cx: Cx<'_>, ) { let name = &i.name; - write_doc(w, &i.doc); - write_obsolete(w, &i.deprecated); + docs.summary(w, &i.doc); + docs.obsolete(w, &i.deprecated); w.line(format!( "public sealed unsafe class {name} : IDisposable, IEquatable<{name}>" )); @@ -230,22 +264,22 @@ pub(crate) fn render_interface( w.blank(); for c in &i.constructors { - let err = ErrCtx::for_fn(c, error, cx); + let err = ErrCtx::new(model, &c.error, cx); let method = cs_member(&c.name, name); - let receiver = if c.name == "new" && matches!(c.shape, CallShape::Sync(_)) { + let receiver = if c.name == "new" && matches!(c.shape, CallShape::Sync) && c.iterator().is_none() { Receiver::Constructor(name) } else { Receiver::Static }; - render_callable(w, c, &method, receiver, &err); + render_callable(w, docs, c, &method, receiver, &err, cx); } for m in &i.methods { - let err = ErrCtx::for_fn(m, error, cx); - render_callable(w, m, &cs_member(&m.name, name), Receiver::Instance, &err); + let err = ErrCtx::new(model, &m.error, cx); + render_callable(w, docs, m, &cs_member(&m.name, name), Receiver::Instance, &err, cx); } for s in &i.statics { - let err = ErrCtx::for_fn(s, error, cx); - render_callable(w, s, &cs_member(&s.name, name), Receiver::Static, &err); + let err = ErrCtx::new(model, &s.error, cx); + render_callable(w, docs, s, &cs_member(&s.name, name), Receiver::Static, &err, cx); } w.line("/// Releases this wrapper's reference. The native object is"); diff --git a/crates/weaveffi-cli/src/targets/dotnet/errors.rs b/crates/weaveffi-cli/src/targets/dotnet/errors.rs new file mode 100644 index 00000000..c3bf862c --- /dev/null +++ b/crates/weaveffi-cli/src/targets/dotnet/errors.rs @@ -0,0 +1,253 @@ +//! Error domains as exception hierarchies, and the error mapping of every +//! call ([`ErrorStrategy`]). + +use heck::ToLowerCamelCase; +use weaveffi_model::errors::pascal; +use weaveffi_model::model::{FieldBinding, Model}; +use weaveffi_model::plan::ErrorStrategy; + +use crate::codegen::errors::{tables, ErrorTable}; +use crate::codegen::CodeWriter; +use crate::targets::dotnet::codec::{read_expr, write_call}; +use crate::targets::dotnet::docs::Docs; +use crate::targets::dotnet::types::{ + cs_str, cs_type, exception_field_cs, safe_cs_name, xml_text, Cx, +}; + +/// How a call maps a failure to an exception, rendering its +/// [`ErrorStrategy`]: a domain's `FromError` (typed codes, open to codes +/// added later), the root exception's for `throws: any`, or the bug +/// exception's for a call that can't fail. +#[derive(Clone)] +pub(crate) struct ErrCtx { + /// The `delegate*` expression of the mapping function. + pub map: String, + /// The exception a throwing call documents, with its description. + doc: Option<(String, String)>, + /// The domain exception a callback method reports with its code and + /// fields. + pub domain: Option, +} + +impl ErrCtx { + /// The error context of a callable (or callback method) with `error`. + pub(crate) fn new(model: &Model, error: &ErrorStrategy, cx: Cx<'_>) -> Self { + match error { + ErrorStrategy::Domain(name) => { + let exc = domain_exception(model, name); + ErrCtx { + map: format!("&{}.FromError", cx.ty(&exc)), + doc: Some(( + exc.clone(), + format!("The call failed with a {name} code."), + )), + domain: Some(exc), + } + } + ErrorStrategy::Untyped => ErrCtx { + map: format!("&{}.FromError", cx.ty(cx.base)), + doc: Some((cx.base.to_string(), "The call failed.".into())), + domain: None, + }, + ErrorStrategy::Trap => ErrCtx { + map: format!("&{}.FromError", cx.ty(cx.bug)), + doc: None, + domain: None, + }, + } + } + + /// The statement throwing when the local `FfiError` named `var` holds a + /// failure. + pub(crate) fn check(&self, var: &str) -> String { + format!( + "if ({var}.Code != 0) throw Ffi.TakeError(&{var}, {});", + self.map + ) + } + + /// Emit the `` doc line of a throwing wrapper. + pub(crate) fn write_doc(&self, w: &mut CodeWriter) { + if let Some((exc, text)) = &self.doc { + w.line(format!("/// {text}")); + } + } +} + +/// The exception class of the error domain `name`: its stem with one +/// `Exception` suffix (`KvError` is `KvException`). +pub(crate) fn domain_exception(model: &Model, name: &str) -> String { + weaveffi_model::errors::exception_type_name(&model.error_domain(name).name) +} + +/// Render every error domain's exception hierarchy. +pub(crate) fn render_domains(w: &mut CodeWriter, model: &Model, docs: &Docs, cx: Cx<'_>) { + for table in tables(model, "Exception") { + render_domain(w, &table, docs, cx); + } +} + +/// One domain: a class deriving from the root exception, with one nested +/// sealed class per code. Each code class exposes its fields as typed +/// properties and can be constructed by a callback-interface +/// implementation, which reports it (code, message, and fields) to the +/// native caller. The domain class itself stands for a code these bindings +/// don't know: domains are open, so a library may add codes. +fn render_domain(w: &mut CodeWriter, table: &ErrorTable<'_>, docs: &Docs, cx: Cx<'_>) { + let exc = &table.type_name; + let base = cx.base; + w.line(format!( + "/// The errors of the {} domain, declared by module {}.", + table.domain.name, table.module.dot_path + )); + w.line("/// Catch a nested class for one code, or this class for any of them. A code"); + w.line("/// these bindings don't know (one the library added later) surfaces as this"); + w.line(format!( + "/// class itself, with its and message." + )); + w.line(format!("public class {exc} : {base}")); + w.block("{", "}", |w| { + w.line(format!( + "internal {exc}(int code, string message) : base(code, message)" + )); + w.line("{"); + w.line("}"); + w.blank(); + for row in &table.codes { + let class = pascal(&row.code.name); + if row.code.doc.is_some() { + docs.summary(w, &row.code.doc); + } else { + w.line(format!("/// {}", xml_text(&row.code.message))); + } + render_code_class(w, exc, &class, row.code.value, &row.code.message, &row.code.fields, docs); + } + w.line("/// The exception for a raw code and its payload: a typed code"); + w.line("/// class, this class for a code it doesn't know (or a payload that doesn't"); + w.line("/// decode), or the root mapping for a runtime code."); + w.line( + "internal static new Exception FromError(int code, string message, FfiBufferReader payload)", + ); + w.block("{", "}", |w| { + w.line("if (code <= 0)"); + w.block("{", "}", |w| { + w.line(format!( + "return {}.FromError(code, message, payload);", + cx.ty(base) + )); + }); + w.line("var given = message.Length == 0 ? null : message;"); + w.line("try"); + w.block("{", "}", |w| { + w.line("switch (code)"); + w.block("{", "}", |w| { + for row in &table.codes { + let class = pascal(&row.code.name); + w.line(format!("case {class}.ErrorCode:")); + w.indent(); + if row.code.fields.is_empty() { + w.line(format!("return new {class}(given);")); + } else { + let mut args: Vec = row + .code + .fields + .iter() + .map(|f| read_expr(cx, &f.ty, "payload", 0)) + .collect(); + args.push("given".into()); + w.line(format!( + "return payload.End(new {class}({}));", + args.join(", ") + )); + } + w.dedent(); + } + }); + }); + w.line(format!("catch ({} e) when (e.Code == {base}.MarshalErrorCode)", cx.bug)); + w.block("{", "}", |w| { + w.line("// A payload that doesn't decode keeps the code and message."); + }); + w.line(format!("return new {exc}(code, message);")); + }); + }); + w.blank(); +} + +/// One code's nested exception class. +fn render_code_class( + w: &mut CodeWriter, + exc: &str, + class: &str, + value: i32, + message: &str, + fields: &[FieldBinding], + docs: &Docs, +) { + w.line(format!("public sealed class {class} : {exc}")); + w.block("{", "}", |w| { + w.line(format!( + "/// The domain code, {value}." + )); + w.line(format!("public const int ErrorCode = {value};")); + w.blank(); + for f in fields { + docs.summary(w, &f.doc); + w.line(format!( + "public {} {} {{ get; }}", + cs_type(&f.ty), + exception_field_cs(&f.name, class) + )); + w.blank(); + } + let mut params: Vec = fields + .iter() + .map(|f| format!("{} {}", cs_type(&f.ty), param_name(f))) + .collect(); + params.push("string? message = null".into()); + w.line("/// Creates the error, as a callback-interface implementation"); + w.line("/// throws it to report this code."); + for f in fields { + docs.param(w, ¶m_name(f), &f.doc); + } + w.line(format!( + "/// The message, or null for the default \"{}\".", + xml_text(message) + )); + w.line(format!( + "public {class}({}) : base(ErrorCode, message ?? \"{}\")", + params.join(", "), + cs_str(message) + )); + w.block("{", "}", |w| { + for f in fields { + w.line(format!( + "{} = {};", + exception_field_cs(&f.name, class), + param_name(f) + )); + } + }); + if !fields.is_empty() { + w.blank(); + w.line("internal override void WritePayload(FfiBufferWriter writer)"); + w.block("{", "}", |w| { + for f in fields { + let value = exception_field_cs(&f.name, class); + w.line(format!("{};", write_call(&f.ty, "writer", &value, 0))); + } + }); + } + }); + w.blank(); +} + +/// A code field's constructor parameter. +fn param_name(f: &FieldBinding) -> String { + let name = f.name.to_lower_camel_case(); + if name == "message" { + "message_".into() + } else { + safe_cs_name(&name) + } +} diff --git a/crates/weaveffi-cli/src/targets/dotnet/mod.rs b/crates/weaveffi-cli/src/targets/dotnet/mod.rs index 2c97747a..05c4119c 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/mod.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/mod.rs @@ -1,64 +1,73 @@ //! .NET binding generator. //! -//! Emits a standalone C# project over the C ABI, revision 4: a `.csproj`, +//! Emits a standalone C# project over the C ABI, revision 5: a `.csproj`, //! the generated API (`{Namespace}.cs`), the fixed runtime (`Runtime.cs`, -//! from `runtime/Runtime.cs`), and a README. Implements [`LanguageBackend`]. +//! from `runtime/Runtime.cs`), and a README. //! //! * Native calls use source-generated `[LibraryImport]` declarations over -//! blittable slots, so the bindings need no runtime marshalling and are -//! trim- and AOT-friendly. Strings cross as UTF-8 `(ptr, len)` pairs. -//! * The first native call runs `NativeMethods`' static constructor, which -//! installs a resolver honoring `{PREFIX}_LIBRARY`, then checks the ABI -//! revision and every top-level module's contract table against the -//! entries the bindings were generated with. -//! * Records and rich enums are classes encoded in the value-buffer format -//! by per-type `WriteTo`/`ReadFrom` pairs; optionals, lists, and maps by -//! one `FfiCodecs` pair per distinct composite type. +//! blittable slots with runtime marshalling disabled, so the bindings are +//! trim- and AOT-friendly. Strings cross as pooled UTF-8 `(ptr, len)` +//! pairs, typed arrays and bytes as pinned spans, and optional scalars as +//! a presence flag plus the value. +//! * Every import resolves through a `DllImportResolver` that loads the +//! library (honoring `{PREFIX}_LIBRARY`) and checks its ABI revision and +//! every top-level module's contract table once; a failure is a +//! `NativeLoadException` every call throws, and `{Namespace}Library.Check()` +//! runs the check up front. +//! * Records are positional `sealed record`s whose list and map fields are +//! `IReadOnlyList`/`IReadOnlyDictionary`, compared by content; +//! rich enums are closed record hierarchies. Values are encoded by +//! per-type `WriteTo`/`ReadFrom` pairs and the runtime's generic composite +//! codec, with no per-shape code. //! * Interfaces are sealed `IDisposable` wrappers over a `SafeHandle` //! subclass whose `ReleaseHandle` calls `_destroy`. Every import takes the //! handle itself, so the interop stub keeps the object alive for the call. -//! * A throwing call raises its domain's exception hierarchy (one nested -//! class per code, its fields as properties) or the root -//! `NativeException` for a runtime code; a failed non-throwing call is a -//! producer bug and raises `NativeBugException`; cancellation raises -//! `OperationCanceledException`. +//! * A call's [`ErrorStrategy`](weaveffi_model::plan::ErrorStrategy) picks +//! its exceptions: a domain's hierarchy (one nested class per code; an +//! unknown code is the domain class itself), the root `NativeException` +//! for `throws: any` and runtime codes, or `NativeBugException` for a call +//! that can't fail. Cancellation raises `OperationCanceledException`. //! * Callback interfaces are C# `interface`s; a passed implementation is //! pinned in a `GCHandle` and paired with one static vtable of //! `[UnmanagedCallersOnly]` trampolines. //! * Async functions return `Task`/`Task` completed by an -//! `[UnmanagedCallersOnly]` completion; cancellable ones take a -//! `CancellationToken` linked to the native cancel token. `iter` -//! returns are single-use lazily streamed `IEnumerable`s. +//! `[UnmanagedCallersOnly]` completion and take a `CancellationToken` +//! (linked to the native cancel token when the function is cancellable). +//! `iter` returns re-enumerable `IEnumerable`s, one native iterator +//! per enumeration. mod callbacks; mod calls; mod codec; mod docs; mod entities; +mod errors; +mod pack; mod package; mod pinvoke; mod runtime; mod types; -use crate::backend::{LanguageBackend, OutputFile}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{Artifact, PackageContext, PackagedFile}; +use crate::targets::Target; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; +use miette::Result; use serde::{Deserialize, Serialize}; use weaveffi_model::model::Model; use crate::targets::dotnet::callbacks::render_callback_interface; use crate::targets::dotnet::calls::render_module_class; -use crate::targets::dotnet::codec::render_codecs; +use crate::targets::dotnet::docs::Docs; use crate::targets::dotnet::entities::{ render_enum, render_interface, render_record, render_rich_enum, }; +use crate::targets::dotnet::errors::render_domains; use crate::targets::dotnet::package::{render_csproj, render_readme, Project, NATIVE_ASSETS}; use crate::targets::dotnet::pinvoke::render_native_methods; -use crate::targets::dotnet::runtime::{ - exception_names, render_domain_exception, render_runtime, RuntimeNames, -}; +use crate::targets::dotnet::runtime::{render_runtime, RuntimeNames}; use crate::targets::dotnet::types::Cx; /// Per-target configuration for [`DotnetGenerator`]. @@ -80,7 +89,15 @@ impl DotnetConfig { } /// .NET backend: a C# project binding the C ABI through `[LibraryImport]`. -pub struct DotnetGenerator; +pub struct DotnetGenerator { + config: DotnetConfig, +} + +impl From for DotnetGenerator { + fn from(config: DotnetConfig) -> Self { + Self { config } + } +} /// The text files of a generated project, at `dir`, loading `library`. fn project_files( @@ -89,47 +106,54 @@ fn project_files( config: &DotnetConfig, library: &str, ctx: Option<&PackageContext>, -) -> Vec<(camino::Utf8PathBuf, String)> { +) -> Vec<(Utf8PathBuf, String)> { let namespace = config.namespace(model); - let identity = &model.identity; let project = Project { - identity, + identity: &model.identity, namespace: &namespace, library, }; - let (base, bug) = exception_names(model, &namespace); - let names = RuntimeNames { - namespace: &namespace, - exception: &base, - bug_exception: &bug, - prefix: model.prefix(), - library, - library_env: &identity.library_env_var(), - }; + let names = RuntimeNames::new(model, &namespace); let cs = format!("{namespace}.cs"); let csproj = format!("{namespace}.csproj"); let assets = if ctx.is_some() { NATIVE_ASSETS } else { "" }; vec![ + (dir.join(&cs), render_csharp(model, &namespace, &names, &cs)), ( - dir.join(&cs), - render_csharp(model, &namespace, (&base, &bug), &cs), + dir.join("Runtime.cs"), + render_runtime(model, &namespace, &names, library, "Runtime.cs"), ), - (dir.join("Runtime.cs"), render_runtime(&names, "Runtime.cs")), (dir.join(&csproj), render_csproj(&project, &csproj, assets)), - (dir.join("README.md"), render_readme(&project, ctx)), + ( + dir.join("README.md"), + render_readme(&project, &names.library_class, ctx), + ), ] } -impl LanguageBackend for DotnetGenerator { - type Config = DotnetConfig; - +impl Target for DotnetGenerator { fn name(&self) -> &'static str { "dotnet" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { + fn fixed_files(&self) -> &'static [&'static str] { + &["*.csproj", "README.md", "Runtime.cs"] + } + + /// Run `dotnet pack` over the written project, producing the NuGet + /// package under `dotnet/`. + fn finish_package( + &self, + dist: &Utf8Path, + artifacts: &[Artifact], + ctx: &PackageContext<'_>, + ) -> Result> { + pack::dotnet_pack(dist, artifacts, ctx) + } + + fn render(&self, model: &Model) -> Vec { let library = &model.identity.library; - project_files(model, &out_dir.join("dotnet"), config, library, None) + project_files(model, &Utf8PathBuf::new(), &self.config, library, None) .into_iter() .map(|(path, text)| OutputFile::new(path, text)) .collect() @@ -139,12 +163,8 @@ impl LanguageBackend for DotnetGenerator { /// library under the `runtimes//native/` layout NuGet resolves at /// restore time; `weaveffi package` then runs `dotnet pack` on it. /// Platforms without a RID (Android, iOS, `wasm32`) have no slot. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let library = &ctx.binaries.lib_name; let mut natives = Vec::new(); for nb in &ctx.binaries.binaries { @@ -160,7 +180,7 @@ impl LanguageBackend for DotnetGenerator { )); } if natives.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } let mut files: Vec = project_files(model, Utf8Path::new(""), config, library, Some(ctx)) @@ -168,7 +188,7 @@ impl LanguageBackend for DotnetGenerator { .map(|(path, text)| PackagedFile::text(path, text)) .collect(); files.extend(natives); - Some(vec![Artifact::directory( + Ok(vec![Artifact::directory( format!("dotnet/{}", config.namespace(model)), files, )]) @@ -176,18 +196,20 @@ impl LanguageBackend for DotnetGenerator { } /// Render the generated API file: typed exceptions, every module's entities, -/// the generated half of `NativeMethods`, and the per-module static classes. +/// the per-module static classes, and the generated half of +/// `NativeMethods`. pub(crate) fn render_csharp( model: &Model, namespace: &str, - (base, bug): (&str, &str), + names: &RuntimeNames, filename: &str, ) -> String { let cx = Cx { ns: namespace, - base, - bug, + base: &names.exception, + bug: &names.bug_exception, }; + let docs = Docs::new(model); let mut w = CodeWriter::four_space(); w.line("#nullable enable"); w.blank(); @@ -205,34 +227,29 @@ pub(crate) fn render_csharp( w.line(format!("namespace {namespace};")); w.blank(); - for m in &model.modules { - if let Some(eb) = &m.errors { - render_domain_exception(&mut w, m, eb, cx); - } - } + render_domains(&mut w, model, &docs, cx); for m in &model.modules { for e in &m.enums { if e.is_rich() { - render_rich_enum(&mut w, cx, e); + render_rich_enum(&mut w, cx, &docs, e); } else { - render_enum(&mut w, e); + render_enum(&mut w, &docs, e); } } for s in &m.structs { - render_record(&mut w, cx, s); + render_record(&mut w, cx, &docs, s); } for cb in &m.callback_interfaces { - render_callback_interface(&mut w, &m.path, cb, model.error_domain(m), cx); + render_callback_interface(&mut w, model, &docs, cb, cx); } for i in &m.interfaces { - render_interface(&mut w, i, model.error_domain(m), cx); + render_interface(&mut w, model, &docs, i, cx); } } for m in &model.modules { - render_module_class(&mut w, m, model.error_domain(m), cx); + render_module_class(&mut w, model, &docs, m, cx); } - render_codecs(&mut w, cx, model); - render_native_methods(&mut w, model); + render_native_methods(&mut w, model, namespace); let mut out = render_prelude(CommentStyle::DoubleSlash); out.push_str(&tidy(&w.finish())); diff --git a/crates/weaveffi-cli/src/targets/dotnet/pack.rs b/crates/weaveffi-cli/src/targets/dotnet/pack.rs new file mode 100644 index 00000000..2597697d --- /dev/null +++ b/crates/weaveffi-cli/src/targets/dotnet/pack.rs @@ -0,0 +1,67 @@ +//! `dotnet pack` over the project directory `weaveffi package` wrote. + +use std::process::{Command, Stdio}; + +use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, IntoDiagnostic, Result}; + +use crate::package::{Artifact, PackageContext}; + +/// Run `dotnet pack` on each written project in `artifacts`, returning the +/// `.nupkg` files it produced under `dist/dotnet/`, relative to `dist`. +/// Without the .NET SDK the NuGet package is skipped. +pub(super) fn dotnet_pack( + dist: &Utf8Path, + artifacts: &[Artifact], + ctx: &PackageContext<'_>, +) -> Result> { + let mut produced = Vec::new(); + for artifact in artifacts { + let Some(csproj) = artifact + .files + .iter() + .find(|f| f.path.extension() == Some("csproj")) + else { + continue; + }; + let project = dist.join(&artifact.path).join(&csproj.path); + let out = std::path::absolute(dist.join("dotnet").as_std_path()) + .ok() + .and_then(|p| Utf8PathBuf::from_path_buf(p).ok()) + .unwrap_or_else(|| dist.join("dotnet")); + // `PackageOutputPath` rather than `--output`, which some SDKs + // mis-forward when a project path is given. + let Ok(output) = Command::new("dotnet") + .args(["pack", project.as_str(), "--configuration", "Release"]) + .arg(format!("-p:PackageOutputPath={out}")) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + else { + ctx.skip( + "the NuGet package", + format!( + "the .NET SDK isn't installed (no `dotnet` on PATH); install it and rerun, \ + or run `dotnet pack {project} -c Release`" + ), + ); + continue; + }; + if !output.status.success() { + bail!( + "`dotnet pack {project}` failed:\n{}{}", + String::from_utf8_lossy(&output.stdout).trim_end(), + String::from_utf8_lossy(&output.stderr).trim_end() + ); + } + let stem = csproj.path.file_stem().unwrap_or_default(); + for entry in out.read_dir_utf8().into_diagnostic()? { + let path = entry.into_diagnostic()?.path().to_path_buf(); + let name = path.file_name().unwrap_or_default(); + if path.extension() == Some("nupkg") && name.starts_with(&format!("{stem}.")) { + produced.push(Utf8PathBuf::from("dotnet").join(name)); + } + } + } + Ok(produced) +} diff --git a/crates/weaveffi-cli/src/targets/dotnet/package.rs b/crates/weaveffi-cli/src/targets/dotnet/package.rs index f0c7ae03..5b625344 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/package.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/package.rs @@ -2,7 +2,7 @@ //! metadata `dotnet pack` needs) and the README. //! //! Every interpolated user string routes through the shared -//! [`xml_escape`](crate::manifest::xml_escape), so markup-sensitive +//! [`xml_escape`], so markup-sensitive //! characters can't corrupt the XML. use crate::manifest::xml_escape; @@ -65,8 +65,10 @@ pub(crate) fn render_csproj(p: &Project<'_>, filename: &str, extra: &str) -> Str {meta} enable true true - - $(NoWarn);CS0618 + true + + $(NoWarn);CS0618;CS1573;CS1591 {extra} @@ -92,7 +94,11 @@ pub(crate) const NATIVE_ASSETS: &str = " /// Render the README. A packaged build lists its bundled runtime /// identifiers. -pub(crate) fn render_readme(p: &Project<'_>, ctx: Option<&PackageContext>) -> String { +pub(crate) fn render_readme( + p: &Project<'_>, + library_class: &str, + ctx: Option<&PackageContext>, +) -> String { let ns = p.namespace; let env = p.identity.library_env_var(); let (mac, linux, win) = p.identity.library_files(); @@ -133,7 +139,12 @@ pub(crate) fn render_readme(p: &Project<'_>, ctx: Option<&PackageContext>) -> St The bindings load `{lib}` with the platform's normal rules\n\ (`{mac}`, `{linux}`, or `{win}` next to the app or on the\n\ library search path). Set `{env}` to a full path to load a\n\ - specific file instead.\n\n", + specific file instead.\n\n\ + The first call checks that the loaded library matches these\n\ + bindings (its ABI revision and every declaration's contract) and\n\ + throws `NativeLoadException` when it doesn't, as does every later\n\ + call. Call `{library_class}.Check()` at startup to run the check\n\ + up front.\n\n", lib = p.library, )); out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); diff --git a/crates/weaveffi-cli/src/targets/dotnet/pinvoke.rs b/crates/weaveffi-cli/src/targets/dotnet/pinvoke.rs index 2ee31d74..5bc748f3 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/pinvoke.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/pinvoke.rs @@ -1,147 +1,166 @@ -//! The generated half of `NativeMethods`: the load-time contract check and -//! one `[LibraryImport]` per lowered C symbol, matching each callable's -//! shape. +//! The generated half of `NativeMethods`: the contract tables checked at +//! load and one `[LibraryImport]` per lowered C symbol. //! -//! Every slot is blittable (`byte*`, `nuint`, `FfiError*`, function -//! pointers) except object slots, which take the interface's `SafeHandle` -//! subclass: the source-generated stub adds a reference to the handle for -//! the duration of the call, so a wrapper that's disposed or collected -//! mid-call can't free the native object under it. +//! Every slot is blittable (`byte*`, `nuint`, `FfiError*`, enums, function +//! pointers; `bool` is marshalled as one byte) except object slots, which +//! take the interface's `SafeHandle` subclass: the source-generated stub +//! adds a reference to the handle for the duration of the call, so a +//! wrapper that's disposed or collected mid-call can't free the native +//! object under it. use std::collections::HashMap; -use crate::codegen::CodeWriter; -use weaveffi_model::abi::AbiParam; -use weaveffi_model::contract; -use weaveffi_model::model::{ - contract_symbol, AbiFn, CallShape, FnBinding, InterfaceBinding, Model, -}; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{AbiFn, FnBinding, InterfaceBinding, Model}; use weaveffi_model::plan::ArgPass; -use crate::targets::dotnet::types::{cs_ctype, fn_pointer_type, safe_cs_name}; +use crate::codegen::contract::{hex, tables}; +use crate::codegen::CodeWriter; +use crate::targets::dotnet::types::{cs_ctype, cs_str, fn_pointer_type, safe_cs_name}; /// The C# type of an interface's `SafeHandle` subclass. -pub(crate) fn handle_type(interface: &str) -> String { +fn handle_type(interface: &str) -> String { format!("{interface}.NativeHandle") } /// Slot types that differ from the plain [`cs_ctype`] mapping for one -/// callable: the receiver and every object parameter take a `SafeHandle`. -fn slot_overrides(f: &FnBinding, owner: Option<&str>) -> HashMap { +/// callable: the receiver and every object parameter take a `SafeHandle`, +/// and an async launcher's `callback` takes the completion's function +/// pointer type. +fn slot_overrides(ns: &str, f: &FnBinding, owner: Option<&str>) -> HashMap { let mut map = HashMap::new(); - if let (true, Some(owner)) = (f.has_self, owner) { - map.insert("self".to_string(), handle_type(owner)); + if let (Some(slot), Some(owner)) = (&f.receiver, owner) { + map.insert(slot.name.clone(), handle_type(owner)); } for p in &f.params { - if let ArgPass::Object { slot, .. } = p.arg_pass() { - let iface = - p.ty.interface_name() - .expect("object parameters name an interface"); - map.insert(slot.name.clone(), handle_type(iface)); + if let ArgPass::Object { + slot, interface, .. + } = &p.pass + { + map.insert(slot.name.clone(), handle_type(interface)); + } + } + if let Some(a) = f.async_binding() { + for slot in &f.abi.params { + // The completion callback is the launcher's one by-value named + // type (iterator and object handles are pointers). + if matches!(slot.ty, CType::Named(_)) { + map.insert( + slot.name.clone(), + fn_pointer_type(ns, &a.callback_params, &CType::Void), + ); + } } } map } -/// One parameter declaration for `slot`, honoring `overrides`. -fn slot_decl( - slot: &AbiParam, - overrides: &HashMap, - callback: Option<&str>, -) -> String { - let ty = match (overrides.get(&slot.name), callback) { - (Some(ty), _) => ty.clone(), - (None, Some(cb)) if slot.name == "callback" => cb.to_string(), - _ => cs_ctype(&slot.ty), - }; - format!("{ty} {}", safe_cs_name(&slot.name)) +/// One parameter declaration for `slot`, honoring `overrides`. A `bool` +/// crosses as one byte. +fn slot_decl(ns: &str, slot: &AbiParam, overrides: &HashMap) -> String { + let name = safe_cs_name(&slot.name); + match overrides.get(&slot.name) { + Some(ty) => format!("{ty} {name}"), + None if slot.ty == CType::Bool => format!("[MarshalAs(UnmanagedType.U1)] bool {name}"), + None => format!("{} {name}", cs_ctype(ns, &slot.ty)), + } } /// Emit one `[LibraryImport]` declaration. -fn import(w: &mut CodeWriter, symbol: &str, ret: &str, params: &[String]) { +fn import(w: &mut CodeWriter, symbol: &str, ret: &CType, ns: &str, params: &[String]) { w.line(format!( "[LibraryImport(LibName, EntryPoint = \"{symbol}\")]" )); w.line("[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]"); + if *ret == CType::Bool { + w.line("[return: MarshalAs(UnmanagedType.U1)]"); + } w.line(format!( - "internal static partial {ret} {symbol}({});", + "internal static partial {} {symbol}({});", + cs_ctype(ns, ret), params.join(", ") )); w.blank(); } /// Emit the import for one lowered function. -fn import_fn( - w: &mut CodeWriter, - abi: &AbiFn, - overrides: &HashMap, - callback: Option<&str>, -) { +fn import_fn(w: &mut CodeWriter, ns: &str, abi: &AbiFn, overrides: &HashMap) { let params: Vec = abi .params .iter() - .map(|s| slot_decl(s, overrides, callback)) + .map(|s| slot_decl(ns, s, overrides)) .collect(); - import(w, &abi.symbol, &cs_ctype(&abi.ret), ¶ms); + import(w, &abi.symbol, &abi.ret, ns, ¶ms); } -/// Emit every import behind one callable: the sync symbol; the async -/// launcher (its `callback` slot typed as the completion function pointer); -/// or the iterator launcher, `_next` (taking the iterator's `SafeHandle`), -/// and `_destroy`. -fn render_callable(w: &mut CodeWriter, f: &FnBinding, owner: Option<&str>) { - let overrides = slot_overrides(f, owner); - match &f.shape { - CallShape::Sync(abi) => import_fn(w, abi, &overrides, None), - CallShape::Async(a) => { - let cb = fn_pointer_type(&a.callback_params, &weaveffi_model::abi::CType::Void); - import_fn(w, &a.launch, &overrides, Some(&cb)); - } - CallShape::Iterator(it) => { - import_fn(w, &it.launch, &overrides, None); - let next_overrides = - HashMap::from([("iter".to_string(), "FfiIteratorHandle".to_string())]); - import_fn(w, &it.next, &next_overrides, None); - import(w, &it.destroy_symbol, "void", &["IntPtr iter".to_string()]); - } +/// Emit every import behind one callable: its entry point (the sync +/// symbol, async launcher, or iterator launcher) and, for an iterator, +/// `_next` (taking the iterator's `SafeHandle`) and `_destroy`. +fn render_callable(w: &mut CodeWriter, ns: &str, f: &FnBinding, owner: Option<&str>) { + import_fn(w, ns, &f.abi, &slot_overrides(ns, f, owner)); + if let Some(it) = f.iterator() { + let iter = &it.next.params[0].name; + let next_overrides = HashMap::from([(iter.clone(), "FfiIteratorHandle".to_string())]); + import_fn(w, ns, &it.next, &next_overrides); + import( + w, + &it.destroy_symbol, + &CType::Void, + ns, + &["IntPtr iter".to_string()], + ); } } /// The imports behind one interface: `_clone`, `_destroy`, and every member. -fn render_interface(w: &mut CodeWriter, i: &InterfaceBinding) { +fn render_interface(w: &mut CodeWriter, ns: &str, i: &InterfaceBinding) { + w.line(format!( + "[LibraryImport(LibName, EntryPoint = \"{}\")]", + i.clone_symbol + )); + w.line("[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]"); + w.line(format!( + "internal static partial IntPtr {}({} self);", + i.clone_symbol, + handle_type(&i.name) + )); + w.blank(); import( w, - &i.clone_symbol, - "IntPtr", - &[format!("{} self", handle_type(&i.name))], + &i.destroy_symbol, + &CType::Void, + ns, + &["IntPtr self".to_string()], ); - import(w, &i.destroy_symbol, "void", &["IntPtr self".to_string()]); - for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { - render_callable(w, f, Some(&i.name)); + for f in i.members() { + render_callable(w, ns, f, Some(&i.name)); } } /// Render the generated half of `NativeMethods`: the contract check run at /// load (every top-level module's table must carry each declaration these -/// bindings were generated with, unchanged), each root's contract import, -/// and every interface and function import in declaration order. -pub(crate) fn render_native_methods(w: &mut CodeWriter, model: &Model) { +/// bindings were generated with, unchanged) and every interface and +/// function import in declaration order. +pub(crate) fn render_native_methods(w: &mut CodeWriter, model: &Model, ns: &str) { w.line("internal static unsafe partial class NativeMethods"); w.line("{"); w.indent(); - w.line("static partial void VerifyContracts()"); + w.line("static partial void VerifyContracts(IntPtr library)"); w.block("{", "}", |w| { - for root in model.roots() { - let symbol = contract_symbol(model.prefix(), &root.name); + for table in tables(model) { w.line(format!( - "VerifyContract(\"{symbol}\", &{symbol}, new (ulong, ulong, string)[]" + "VerifyContract(library, \"{}\", new (ulong, ulong, string)[]", + table.symbol )); w.line("{"); w.indent(); - for e in contract::entries(model, root) { + for row in &table.rows { w.line(format!( - "(0x{:016x}UL, 0x{:016x}UL, \"{}\"),", - e.id, e.hash, e.path + "({}UL, {}UL, \"{}\"), // {}", + hex(row.id), + hex(row.hash), + cs_str(&row.path), + row.signature )); } w.dedent(); @@ -149,20 +168,12 @@ pub(crate) fn render_native_methods(w: &mut CodeWriter, model: &Model) { } }); w.blank(); - for root in model.roots() { - import( - w, - &contract_symbol(model.prefix(), &root.name), - "FfiContractEntry*", - &["nuint* out_len".to_string()], - ); - } for m in &model.modules { for i in &m.interfaces { - render_interface(w, i); + render_interface(w, ns, i); } for f in &m.functions { - render_callable(w, f, None); + render_callable(w, ns, f, None); } } w.dedent(); diff --git a/crates/weaveffi-cli/src/targets/dotnet/runtime.rs b/crates/weaveffi-cli/src/targets/dotnet/runtime.rs index 149bc13e..3159644e 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/runtime.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/runtime.rs @@ -1,44 +1,84 @@ //! The fixed runtime (`runtime/Runtime.cs`, spliced with the library's -//! names) and the per-domain exception hierarchies. +//! names) and the names of its public classes. -use crate::codegen::CodeWriter; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_model::errors; -use weaveffi_model::model::{ErrorBinding, ErrorCodeBinding, Model, ModuleBinding, ABI_VERSION}; - -use crate::targets::dotnet::codec::{read_expr, write_stmt}; -use crate::targets::dotnet::docs::write_doc; -use crate::targets::dotnet::types::{cs_str, cs_type, safe_cs_name, xml_text, Cx}; +use weaveffi_model::model::{Model, ABI_VERSION}; /// The runtime source shared by every generated library. const RUNTIME_CS: &str = include_str!("runtime/Runtime.cs"); -/// The names a generated library splices into the fixed runtime. -pub(crate) struct RuntimeNames<'a> { - /// The C# namespace. - pub namespace: &'a str, - /// The root exception class (see [`exception_names`]). - pub exception: &'a str, - /// The trap exception class (see [`exception_names`]). - pub bug_exception: &'a str, - /// The C symbol prefix. - pub prefix: &'a str, - /// The native library base name P/Invoke loads. - pub library: &'a str, - /// The `{PREFIX}_LIBRARY` override variable. - pub library_env: &'a str, +/// The public classes of the runtime, named so they never collide with a +/// type the API declares. +pub(crate) struct RuntimeNames { + /// The root of every error a throwing call reports (`NativeException`). + pub exception: String, + /// The trap a failed non-throwing call raises (`NativeBugException`). + pub bug_exception: String, + /// The load-time failure (`NativeLoadException`). + pub load_exception: String, + /// The static class with `Check()` (`{Namespace}Library`). + pub library_class: String, +} + +impl RuntimeNames { + /// The names for `model` in `namespace`. A name the API already declares + /// is qualified by the namespace's last segment (`KvstoreNativeException`). + pub(crate) fn new(model: &Model, namespace: &str) -> Self { + let taken = |name: &str| { + model.modules.iter().any(|m| { + m.structs.iter().any(|s| s.name == name) + || m.enums.iter().any(|e| e.name == name) + || m.interfaces.iter().any(|i| i.name == name) + || m.callback_interfaces + .iter() + .any(|c| c.name == name || format!("I{}", c.name) == name) + || m.errors + .iter() + .any(|e| errors::exception_type_name(&e.name) == name) + }) + }; + let last = namespace.rsplit('.').next().unwrap_or(namespace); + let pick = |name: &str| { + if taken(name) { + format!("{last}{name}") + } else { + name.to_string() + } + }; + let library = format!("{last}Library"); + Self { + exception: pick("NativeException"), + bug_exception: pick("NativeBugException"), + load_exception: pick("NativeLoadException"), + library_class: if taken(&library) { + format!("{last}NativeLibrary") + } else { + library + }, + } + } } -/// Render `Runtime.cs`: the fixed runtime with every placeholder replaced. -pub(crate) fn render_runtime(names: &RuntimeNames<'_>, filename: &str) -> String { +/// Render `Runtime.cs`, loading `library`: the fixed runtime with every +/// placeholder replaced. +pub(crate) fn render_runtime( + model: &Model, + namespace: &str, + names: &RuntimeNames, + library: &str, + filename: &str, +) -> String { + let identity = &model.identity; let body = RUNTIME_CS - .replace("{{NAMESPACE}}", names.namespace) - .replace("{{BUG_EXCEPTION}}", names.bug_exception) - .replace("{{EXCEPTION}}", names.exception) - .replace("{{PREFIX}}", names.prefix) - .replace("{{LIBRARY}}", names.library) - .replace("{{LIBRARY_ENV}}", names.library_env) + .replace("{{NAMESPACE}}", namespace) + .replace("{{BUG_EXCEPTION}}", &names.bug_exception) + .replace("{{LOAD_EXCEPTION}}", &names.load_exception) + .replace("{{EXCEPTION}}", &names.exception) + .replace("{{LIBRARY_CLASS}}", &names.library_class) + .replace("{{PREFIX}}", model.prefix()) + .replace("{{LIBRARY_ENV}}", &identity.library_env_var()) + .replace("{{LIBRARY}}", library) .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()); debug_assert!(!body.contains("{{"), "unfilled placeholder in Runtime.cs"); format!( @@ -47,189 +87,3 @@ pub(crate) fn render_runtime(names: &RuntimeNames<'_>, filename: &str) -> String render_trailer(CommentStyle::DoubleSlash, filename) ) } - -/// The runtime's two exception classes: the root of every error a throwing -/// call reports (`NativeException`) and the trap a failed non-throwing call -/// raises (`NativeBugException`). A name the API already declares is -/// qualified by the namespace (`KvstoreNativeException`). -pub(crate) fn exception_names(model: &Model, namespace: &str) -> (String, String) { - let taken = |name: &str| { - model.modules.iter().any(|m| { - m.structs.iter().any(|s| s.name == name) - || m.enums.iter().any(|e| e.name == name) - || m.interfaces.iter().any(|i| i.name == name) - || m.callback_interfaces.iter().any(|c| c.name == name) - || m.errors - .as_ref() - .is_some_and(|e| dotnet_exception_name(e) == name) - }) - }; - let pick = |name: &str| { - if taken(name) { - format!("{namespace}{name}") - } else { - name.to_string() - } - }; - (pick("NativeException"), pick("NativeBugException")) -} - -/// The C# exception class for one error domain: the domain stem with one -/// `Exception` suffix, so `KvError` becomes `KvException`. -pub(crate) fn dotnet_exception_name(eb: &ErrorBinding) -> String { - errors::exception_type_name(&eb.type_name) -} - -/// The nested exception class for one code (`KvException.KeyNotFound`). -fn code_class(c: &ErrorCodeBinding) -> String { - errors::pascal(&c.name) -} - -/// The exception hierarchy of the error domain `eb`, declared by `module`: -/// an abstract class deriving from the root exception with one nested -/// sealed class per code. Each code class exposes its fields as typed -/// properties and can be constructed by a callback-interface implementation, -/// which reports it (code, message, and fields) to the native caller. -/// `FromError` maps a raw error to the typed exception, decoding the -/// payload; an undeclared code falls back to the root exception. -pub(crate) fn render_domain_exception( - w: &mut CodeWriter, - module: &ModuleBinding, - eb: &ErrorBinding, - cx: Cx<'_>, -) { - let exc = dotnet_exception_name(eb); - let base = cx.base; - w.line(format!( - "/// The errors of the {} domain, declared by module {}.", - eb.name, module.dot_path - )); - w.line("/// Catch a nested class for one code, or this class for any of them."); - w.line(format!("public abstract class {exc} : {base}")); - w.block("{", "}", |w| { - w.line(format!( - "private protected {exc}(int code, string message) : base(code, message)" - )); - w.line("{"); - w.line("}"); - w.blank(); - w.line("/// Writes this code's fields, the error payload a callback"); - w.line("/// reports with it."); - w.line("internal virtual void WritePayload(FfiBufferWriter writer)"); - w.line("{"); - w.line("}"); - w.blank(); - for c in &eb.codes { - render_code_class(w, &exc, c); - } - w.line("/// The exception for a raw code and its serialized payload."); - w.line( - "internal static new Exception FromError(int code, string message, byte[]? payload)", - ); - w.block("{", "}", |w| { - w.line("var given = message.Length == 0 ? null : message;"); - w.line("switch (code)"); - w.block("{", "}", |w| { - for c in &eb.codes { - let class = code_class(c); - w.line(format!("case {class}.ErrorCode:")); - if c.fields.is_empty() { - w.indent(); - w.line(format!("return new {class}(given);")); - w.dedent(); - continue; - } - w.block("{", "}", |w| { - w.line("var reader = new FfiBufferReader(payload ?? Array.Empty());"); - w.line(format!("var e = new {class}(")); - w.indent(); - for f in &c.fields { - w.line(format!("{},", read_expr(cx, &f.ty, "reader"))); - } - w.line("given);"); - w.dedent(); - w.line("reader.ExpectEnd();"); - w.line("return e;"); - }); - } - w.line("default:"); - w.indent(); - w.line(format!( - "return {}.FromError(code, message, payload);", - cx.ty(base) - )); - w.dedent(); - }); - }); - }); - w.blank(); -} - -/// One code's nested exception class. -fn render_code_class(w: &mut CodeWriter, exc: &str, c: &ErrorCodeBinding) { - let class = code_class(c); - if c.doc.is_some() { - write_doc(w, &c.doc); - } else { - w.line(format!("/// {}", xml_text(&c.message))); - } - w.line(format!("public sealed class {class} : {exc}")); - w.block("{", "}", |w| { - w.line(format!( - "/// The domain code, {}.", - c.value - )); - w.line(format!("public const int ErrorCode = {};", c.value)); - w.blank(); - for f in &c.fields { - write_doc(w, &f.doc); - w.line(format!( - "public {} {} {{ get; }}", - cs_type(&f.ty), - f.name.to_upper_camel_case() - )); - w.blank(); - } - let mut params: Vec = c - .fields - .iter() - .map(|f| { - format!( - "{} {}", - cs_type(&f.ty), - safe_cs_name(&f.name.to_lower_camel_case()) - ) - }) - .collect(); - params.push("string? message = null".into()); - w.line("/// Creates the error with , or the"); - w.line(format!( - "/// default message \"{}\" when it's null.", - xml_text(&c.message) - )); - w.line(format!( - "public {class}({}) : base(ErrorCode, message ?? \"{}\")", - params.join(", "), - cs_str(&c.message) - )); - w.block("{", "}", |w| { - for f in &c.fields { - w.line(format!( - "{} = {};", - f.name.to_upper_camel_case(), - safe_cs_name(&f.name.to_lower_camel_case()) - )); - } - }); - if !c.fields.is_empty() { - w.blank(); - w.line("internal override void WritePayload(FfiBufferWriter writer)"); - w.block("{", "}", |w| { - for f in &c.fields { - w.line(write_stmt(&f.ty, "writer", &f.name.to_upper_camel_case())); - } - }); - } - }); - w.blank(); -} diff --git a/crates/weaveffi-cli/src/targets/dotnet/runtime/Runtime.cs b/crates/weaveffi-cli/src/targets/dotnet/runtime/Runtime.cs index 5369686a..9333c7f7 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/runtime/Runtime.cs +++ b/crates/weaveffi-cli/src/targets/dotnet/runtime/Runtime.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Buffers; +using System.Buffers.Binary; using System.Collections; using System.Collections.Generic; using System.Reflection; @@ -10,6 +12,10 @@ using System.Threading; using System.Threading.Tasks; +// Every native signature is blittable (`bool` slots are marshalled as one +// byte by the source generator), so the runtime never marshals on its own. +[assembly: DisableRuntimeMarshalling] + namespace {{NAMESPACE}}; /// The root of every error a throwing call reports. Each typed domain @@ -19,11 +25,13 @@ namespace {{NAMESPACE}}; /// negative . public class {{EXCEPTION}} : Exception { - /// An untyped producer error. + /// An untyped producer error (a throws: any call's + /// failure). public const int GenericErrorCode = -1; /// The producer panicked; the message carries the panic text. public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. + /// A value couldn't be converted at the boundary (an argument the + /// producer rejected, or a callback result it couldn't accept). public const int MarshalErrorCode = -3; /// A callback-interface implementation failed; the message carries /// the implementation's message. @@ -37,15 +45,23 @@ public class {{EXCEPTION}} : Exception public int Code { get; } /// Creates an exception carrying a native error code. + /// The native error code. + /// The error message. public {{EXCEPTION}}(int code, string message) : base(message) { Code = code; } + /// Writes the fields of a domain error, the payload a callback + /// reports with its code. A code without fields writes nothing. + internal virtual void WritePayload(FfiBufferWriter writer) + { + } + /// Maps a code outside every declared domain to its exception: /// cancellation to , anything else /// to this type. - internal static Exception FromError(int code, string message, byte[]? payload) + internal static Exception FromError(int code, string message, FfiBufferReader payload) { if (code == CancelledErrorCode) { @@ -67,6 +83,8 @@ public sealed class {{BUG_EXCEPTION}} : InvalidOperationException /// Creates the exception for a failed call that can't /// fail. + /// The native error code. + /// The producer's message. public {{BUG_EXCEPTION}}(int code, string message) : base($"native call failed with code {code}: {message}") { @@ -76,7 +94,7 @@ public sealed class {{BUG_EXCEPTION}} : InvalidOperationException /// Maps a failure of a call that isn't declared to throw: /// cancellation to , anything else /// to this type. - internal static Exception FromError(int code, string message, byte[]? payload) + internal static Exception FromError(int code, string message, FfiBufferReader payload) { if (code == {{EXCEPTION}}.CancelledErrorCode) { @@ -86,16 +104,49 @@ internal static Exception FromError(int code, string message, byte[]? payload) } } -/// Maps a native error code, message, and serialized payload to the -/// exception a call throws. -internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); +/// The native library couldn't be loaded, or the loaded library +/// doesn't match these bindings (another ABI revision, or a declaration that +/// is missing or changed). Every later call into the library throws the same +/// exception. +public sealed class {{LOAD_EXCEPTION}} : Exception +{ + /// Creates the exception with a message and the failure that + /// caused it, if any. + /// What went wrong. + /// The underlying failure, if any. + public {{LOAD_EXCEPTION}}(string message, Exception? inner = null) : base(message, inner) + { + } +} + +/// The native {{LIBRARY}} library behind these +/// bindings. +public static class {{LIBRARY_CLASS}} +{ + /// The C ABI revision these bindings were generated + /// against. + public const uint AbiVersion = {{ABI_VERSION}}; + + /// Loads the native library and checks that it matches these + /// bindings: its ABI revision and the contract entry of every declaration + /// they use. Calling it is optional: the first call into the library runs + /// the same check and throws the same exception. Calling it again after + /// a success does nothing. + /// The library can't be found or + /// loaded, or it doesn't match these bindings. + public static void Check() + { + NativeMethods.Load(null); + } +} /// The native {{PREFIX}}_error slot. [StructLayout(LayoutKind.Sequential)] internal unsafe struct FfiError { public int Code; - public byte* Message; + public byte* MessagePtr; + public nuint MessageLen; public byte* PayloadPtr; public nuint PayloadLen; } @@ -109,16 +160,45 @@ internal struct FfiContractEntry public ulong Hash; } -/// Marshalling helpers shared by every generated call. -internal static unsafe class Ffi +/// The result type of an async call that returns nothing. +internal readonly struct FfiVoid +{ +} + +/// The UTF-8 bytes of a string argument, in a pooled buffer the +/// native side borrows for the call as a (pointer, length) pair, so interior +/// NULs survive. Pin it with fixed; dispose it after the call. +internal readonly ref struct FfiUtf8 { - /// The UTF-8 bytes of a string argument. The native side borrows - /// them as a (pointer, length) pair, so interior NULs survive. - internal static byte[] Utf8(string value) + private readonly byte[] _bytes; + + /// The number of bytes. + internal readonly nuint Length; + + internal FfiUtf8(string value) + { + var count = Encoding.UTF8.GetByteCount(value); + _bytes = count == 0 ? Array.Empty() : ArrayPool.Shared.Rent(count); + Length = (nuint)Encoding.UTF8.GetBytes(value, _bytes); + } + + public ref byte GetPinnableReference() { - return Encoding.UTF8.GetBytes(value); + return ref MemoryMarshal.GetArrayDataReference(_bytes); } + public void Dispose() + { + if (_bytes.Length > 0) + { + ArrayPool.Shared.Return(_bytes); + } + } +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ /// Decodes a borrowed UTF-8 run; the native side keeps /// ownership. internal static string ReadString(byte* ptr, nuint len) @@ -133,11 +213,22 @@ internal static byte[] ReadBytes(byte* ptr, nuint len) return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); } - /// A reader over a copy of a borrowed value buffer; the native - /// side keeps ownership. - internal static FfiBufferReader ReadBuffer(byte* ptr, nuint len) + /// A borrowed typed array (or byte run) as a span, valid for the + /// duration of a callback. A zero-length run may have a dangling + /// pointer, which an empty span never reads. + internal static ReadOnlySpan Borrow(T* ptr, nuint len) where T : unmanaged + { + return len == 0 ? ReadOnlySpan.Empty : new ReadOnlySpan(ptr, checked((int)len)); + } + + /// Decodes a borrowed value buffer, which must hold exactly one + /// value; the native side keeps ownership. + internal static T ReadBuffer(byte* ptr, nuint len, Func read) { - return new FfiBufferReader(ReadBytes(ptr, len)); + var reader = new FfiBufferReader(ptr, len); + var value = read(reader); + reader.ExpectEnd(); + return value; } /// Decodes a returned UTF-8 string and releases it. @@ -166,93 +257,115 @@ internal static byte[] TakeBytes(byte* ptr, nuint len) } } - /// Copies a returned value buffer, releases it, and returns a - /// reader over the copy. - internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + /// Copies a returned typed array of + /// elements and releases its len * sizeof(T) bytes. + internal static T[] TakeArray(T* ptr, nuint len) where T : unmanaged { - return new FfiBufferReader(TakeBytes(ptr, len)); + if (ptr == null || len == 0) + { + return Array.Empty(); + } + try + { + return new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + finally + { + NativeMethods.FreeBytes((byte*)ptr, len * (nuint)sizeof(T)); + } + } + + /// Decodes a returned value buffer and releases it. + internal static T TakeBuffer(byte* ptr, nuint len, Func read) + { + try + { + return ReadBuffer(ptr, len, read); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } } /// Releases a caller-owned error slot and returns the exception - /// it described. - internal static Exception TakeError(FfiError* err, FfiErrorMap map) + /// makes of it. + internal static Exception TakeError(FfiError* err, delegate* map) { - var code = err->Code; - var message = ErrorMessage(err); - var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); - NativeMethods.ErrorClear(err); - return map(code, message, payload.Length == 0 ? null : payload); + try + { + return Describe(err, map); + } + finally + { + NativeMethods.ErrorClear(err); + } } - /// Releases a heap-boxed async error and returns the exception it - /// described. - internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + /// Releases a heap-boxed async error and returns the exception + /// makes of it. + internal static Exception TakeBoxedError(FfiError* err, delegate* map) { - code = err->Code; - var message = ErrorMessage(err); - var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); - NativeMethods.ErrorFree(err); - return map(code, message, payload.Length == 0 ? null : payload); + try + { + return Describe(err, map); + } + finally + { + NativeMethods.ErrorFree(err); + } } - private static string ErrorMessage(FfiError* err) + private static Exception Describe(FfiError* err, delegate* map) { - return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + var message = ReadString(err->MessagePtr, err->MessageLen); + return map(err->Code, message, new FfiBufferReader(err->PayloadPtr, err->PayloadLen)); } /// Reports a failed callback-interface method to the producer /// through out_err: with the exception's - /// message and, for a domain error with fields, the encoded fields as - /// its payload. The producer copies both. - internal static void SetError(FfiError* err, int code, Exception e, FfiBufferWriter? payload = null) + /// message. The producer copies it. + internal static void Report(FfiError* err, Exception e, int code) { - var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); - fixed (byte* message = bytes) + using var message = new FfiUtf8(e.Message); + fixed (byte* ptr = message) { - NativeMethods.ErrorSet(err, code, message); - } - if (payload != null && payload.Length > 0) - { - fixed (byte* ptr = payload.Written) - { - NativeMethods.ErrorSetPayload(err, ptr, (nuint)payload.Length); - } + NativeMethods.ErrorSet(err, code, ptr, message.Length); } } - /// Reports a domain exception a throwing callback-interface - /// method raised: its positive , its message, and - /// the fields encodes. A failure to - /// encode them is reported as a foreign error instead. - internal static void SetDomainError(FfiError* err, int code, Exception e, Action writePayload) + /// Reports an exception of the method's own error domain: its + /// code, its message, and its fields as the payload, so the producer sees + /// exactly that domain error. A payload that can't be encoded is reported + /// as a generic failure instead. + internal static void ReportDomain(FfiError* err, {{EXCEPTION}} e) { - var payload = new FfiBufferWriter(); + using var payload = new FfiBufferWriter(); try { - writePayload(payload); + e.WritePayload(payload); } catch (Exception inner) { - SetForeignError(err, inner); + Report(err, inner, {{EXCEPTION}}.GenericErrorCode); return; } - SetError(err, code, e, payload); - } - - /// Reports a failed callback-interface method that isn't a - /// declared domain error: the producer receives the foreign error code - /// with the exception's message. - internal static void SetForeignError(FfiError* err, Exception e) - { - SetError(err, {{EXCEPTION}}.ForeignErrorCode, e); + Report(err, e, e.Code); + if (payload.Length > 0) + { + fixed (byte* ptr = payload.Written) + { + NativeMethods.ErrorSetPayload(err, ptr, (nuint)payload.Length); + } + } } /// Hands to the producer through a /// callback method's out slots, as a run allocated with /// {{PREFIX}}_alloc that the producer adopts. - internal static void Return(ReadOnlySpan bytes, byte** outPtr, nuint* outLen) + private static void Return(ReadOnlySpan bytes, byte** outPtr, nuint* outLen) { - var run = NativeMethods.Alloc((nuint)bytes.Length); + var run = bytes.Length == 0 ? null : NativeMethods.Alloc((nuint)bytes.Length); if (run == null && bytes.Length > 0) { throw new OutOfMemoryException("the native allocator returned null"); @@ -265,7 +378,11 @@ internal static void Return(ReadOnlySpan bytes, byte** outPtr, nuint* outL /// Returns a string from a callback method. internal static void ReturnString(string value, byte** outPtr, nuint* outLen) { - Return(Encoding.UTF8.GetBytes(value), outPtr, outLen); + using var utf8 = new FfiUtf8(value); + fixed (byte* ptr = utf8) + { + Return(new ReadOnlySpan(ptr, (int)utf8.Length), outPtr, outLen); + } } /// Returns bytes from a callback method. @@ -274,12 +391,36 @@ internal static void ReturnBytes(byte[] value, byte** outPtr, nuint* outLen) Return(value, outPtr, outLen); } - /// Returns an encoded value buffer from a callback method. - internal static void ReturnBuffer(FfiBufferWriter writer, byte** outPtr, nuint* outLen) + /// Returns a typed array from a callback method: a run of + /// Length * sizeof(T) bytes and the element count. + internal static void ReturnArray(T[] value, T** outPtr, nuint* outLen) where T : unmanaged { + nuint count; + Return(MemoryMarshal.AsBytes(value.AsSpan()), (byte**)outPtr, &count); + *outLen = (nuint)value.Length; + } + + /// Returns an encoded value from a callback method. + internal static void ReturnBuffer(T value, Action write, byte** outPtr, nuint* outLen) + { + using var writer = new FfiBufferWriter(); + write(writer, value); Return(writer.Written, outPtr, outLen); } + /// Returns an optional scalar from a callback method: the + /// presence as the C return, the value through + /// . + internal static bool ReturnOptional(T? value, T* outValue) where T : unmanaged + { + if (value is { } present) + { + *outValue = present; + return true; + } + return false; + } + /// Pins a callback-interface implementation for the producer and /// returns the ctx it hands back to every trampoline. The /// producer releases it through the vtable's free. An absent @@ -306,9 +447,121 @@ internal static T Target(IntPtr ctx) where T : class } } -/// Imports of the native runtime surface plus the load-time checks. -/// The generated API declares its own imports, and the contract it was -/// generated with, in the other half of this partial class. +/// Value equality for record fields whose C# type compares by +/// reference: byte arrays, lists, and maps compare their contents. +internal static class FfiEquality +{ + /// Default equality (what a record compares its fields + /// with). + internal static bool Equal(T a, T b) + { + return EqualityComparer.Default.Equals(a, b); + } + + internal static bool Bytes(byte[]? a, byte[]? b) + { + return ReferenceEquals(a, b) || a is not null && b is not null && a.AsSpan().SequenceEqual(b); + } + + internal static bool Lists(IReadOnlyList? a, IReadOnlyList? b) + { + return Lists(a, b, EqualityComparer.Default); + } + + internal static bool Lists(IReadOnlyList? a, IReadOnlyList? b, Func equal) + { + if (ReferenceEquals(a, b)) + { + return true; + } + if (a is null || b is null || a.Count != b.Count) + { + return false; + } + for (var i = 0; i < a.Count; i++) + { + if (!equal(a[i], b[i])) + { + return false; + } + } + return true; + } + + private static bool Lists(IReadOnlyList? a, IReadOnlyList? b, EqualityComparer comparer) + { + if (ReferenceEquals(a, b)) + { + return true; + } + if (a is null || b is null || a.Count != b.Count) + { + return false; + } + for (var i = 0; i < a.Count; i++) + { + if (!comparer.Equals(a[i], b[i])) + { + return false; + } + } + return true; + } + + internal static bool Maps(IReadOnlyDictionary? a, IReadOnlyDictionary? b) + { + var comparer = EqualityComparer.Default; + return Maps(a, b, comparer.Equals); + } + + internal static bool Maps(IReadOnlyDictionary? a, IReadOnlyDictionary? b, Func equal) + { + if (ReferenceEquals(a, b)) + { + return true; + } + if (a is null || b is null || a.Count != b.Count) + { + return false; + } + foreach (var entry in a) + { + if (!b.TryGetValue(entry.Key, out var other) || !equal(entry.Value, other)) + { + return false; + } + } + return true; + } + + internal static int BytesHash(byte[]? value) + { + if (value is null) + { + return 0; + } + var hash = new HashCode(); + hash.AddBytes(value); + return hash.ToHashCode(); + } + + /// A list's hash: its count, which equal lists share. + internal static int ListHash(IReadOnlyList? value) + { + return value?.Count ?? -1; + } + + /// A map's hash: its count, which equal maps share. + internal static int MapHash(IReadOnlyDictionary? value) + { + return value?.Count ?? -1; + } +} + +/// The native library handle, the load-time checks, and the imports +/// of the native runtime surface. The generated API declares its own +/// imports, and the contract it was generated with, in the other half of this +/// partial class. internal static unsafe partial class NativeMethods { /// The native library's base name. @@ -318,13 +571,14 @@ internal static unsafe partial class NativeMethods /// path. internal const string LibraryEnvVar = "{{LIBRARY_ENV}}"; - /// The C ABI revision these bindings were generated - /// against. - internal const uint AbiVersion = {{ABI_VERSION}}; + private static readonly object Gate = new object(); + private static IntPtr s_library; + private static {{LOAD_EXCEPTION}}? s_failure; - // Runs before the first native call through this class, so a library - // built for another ABI revision or missing a declaration these bindings - // use fails here instead of misreading memory later. + // Every import resolves through `Resolve`, which loads the library and + // checks it once, so a library built for another ABI revision or missing + // a declaration these bindings use fails before any call can misread + // memory, with an exception the caller can catch. static NativeMethods() { try @@ -333,59 +587,103 @@ static NativeMethods() } catch (InvalidOperationException) { - // The assembly already has a resolver; default probing still applies. + // The assembly already has a resolver; only an explicit Check() + // runs the load-time checks then. } - uint found; - try + } + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + return name == LibName ? Load(searchPath) : IntPtr.Zero; + } + + /// The checked library handle, loading it on first use. A + /// failure is remembered and thrown again by every later call. + internal static IntPtr Load(DllImportSearchPath? searchPath) + { + lock (Gate) { - found = AbiVersionOf(); + if (s_failure != null) + { + throw s_failure; + } + if (s_library != IntPtr.Zero) + { + return s_library; + } + var library = IntPtr.Zero; + try + { + library = Open(searchPath); + Verify(library); + s_library = library; + return library; + } + catch ({{LOAD_EXCEPTION}} e) + { + if (library != IntPtr.Zero) + { + NativeLibrary.Free(library); + } + s_failure = e; + throw; + } } - catch (EntryPointNotFoundException e) + } + + private static IntPtr Open(DllImportSearchPath? searchPath) + { + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + if (!string.IsNullOrEmpty(path)) { - throw new InvalidOperationException( - $"the loaded {LibName} library doesn't export {{PREFIX}}_abi_version; these bindings expect ABI revision {AbiVersion}", e); + try + { + return NativeLibrary.Load(path); + } + catch (Exception e) when (e is DllNotFoundException or BadImageFormatException) + { + throw new {{LOAD_EXCEPTION}}($"couldn't load {path} (named by {LibraryEnvVar}): {e.Message}", e); + } } - if (found != AbiVersion) + if (NativeLibrary.TryLoad(LibName, typeof(NativeMethods).Assembly, searchPath, out var library)) { - throw new InvalidOperationException( - $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + return library; } - VerifyContracts(); + throw new {{LOAD_EXCEPTION}}( + $"couldn't find the {LibName} native library; put it next to the app or on the library search path, or set {LibraryEnvVar} to its full path"); } - static partial void VerifyContracts(); - - private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + private static void Verify(IntPtr library) { - if (name != LibName) + if (!NativeLibrary.TryGetExport(library, "{{PREFIX}}_abi_version", out var abiVersion)) { - return IntPtr.Zero; + throw new {{LOAD_EXCEPTION}}( + $"the loaded {LibName} library doesn't export {{PREFIX}}_abi_version; these bindings expect ABI revision {{{LIBRARY_CLASS}}.AbiVersion}"); } - var path = Environment.GetEnvironmentVariable(LibraryEnvVar); - return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + var found = ((delegate* unmanaged[Cdecl])abiVersion)(); + if (found != {{LIBRARY_CLASS}}.AbiVersion) + { + throw new {{LOAD_EXCEPTION}}( + $"ABI mismatch: these bindings expect revision {{{LIBRARY_CLASS}}.AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyContracts(library); } - /// Fails the load unless the loaded library's contract table, - /// read through (the import of - /// ), carries every - /// declaration with an unchanged signature hash. Declarations the - /// library has and these bindings don't are fine. - private static void VerifyContract( - string symbol, - delegate* table, - (ulong Id, ulong Hash, string Path)[] expected) + static partial void VerifyContracts(IntPtr library); + + /// Fails the load unless the library's contract table + /// carries every + /// declaration with an unchanged signature hash. Declarations the library + /// has and these bindings don't are fine. + private static void VerifyContract(IntPtr library, string symbol, (ulong Id, ulong Hash, string Path)[] expected) { - nuint len = 0; - FfiContractEntry* entries; - try + if (!NativeLibrary.TryGetExport(library, symbol, out var table)) { - entries = table(&len); - } - catch (EntryPointNotFoundException e) - { - throw new InvalidOperationException( - $"the loaded {LibName} library doesn't export {symbol}; regenerate the bindings or rebuild the library", e); + throw new {{LOAD_EXCEPTION}}( + $"the loaded {LibName} library doesn't export {symbol}; regenerate the bindings or rebuild the library"); } + nuint len = 0; + var entries = ((delegate* unmanaged[Cdecl])table)(&len); var hashes = new Dictionary(checked((int)len)); for (nuint i = 0; i < len; i++) { @@ -395,24 +693,20 @@ private static void VerifyContract( { if (!hashes.TryGetValue(id, out var actual)) { - throw new InvalidOperationException( + throw new {{LOAD_EXCEPTION}}( $"{path} is missing from the library {LibName}; regenerate the bindings or rebuild the library"); } if (actual != hash) { - throw new InvalidOperationException( + throw new {{LOAD_EXCEPTION}}( $"{path} changed since these bindings were generated; regenerate the bindings or rebuild the library {LibName}"); } } } - [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_abi_version")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial uint AbiVersionOf(); - [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_error_set")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void ErrorSet(FfiError* err, int code, byte* message); + internal static partial void ErrorSet(FfiError* err, int code, byte* messagePtr, nuint messageLen); [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_error_set_payload")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] @@ -448,9 +742,11 @@ private static void VerifyContract( } /// One pending async call: the task it completes, the native -/// context that finds it again, and, for a cancellable function, the -/// native cancel token linked to the caller's -/// . +/// context that finds it again, and the caller's +/// . A cancellable function links the token to +/// a native cancel token; for any other function, cancelling abandons the +/// wait (the task completes as canceled at once, and the native call +/// finishes in the background and releases its result). internal sealed class FfiCall { private readonly TaskCompletionSource _tcs = @@ -463,20 +759,18 @@ internal sealed class FfiCall private bool _completed; private bool _cancelling; - internal FfiCall(CancellationToken cancellation = default) + internal FfiCall(CancellationToken cancellation) { _cancellation = cancellation; _self = GCHandle.Alloc(this); } - /// The task the completion resolves. - internal Task Task => _tcs.Task; - /// The context argument for the launcher. internal IntPtr Context => GCHandle.ToIntPtr(_self); - /// Creates the native cancel token for the launcher and cancels - /// it when the caller's fires. + /// Creates the native cancel token for a cancellable launcher + /// and cancels it when the caller's + /// fires. internal IntPtr CancelToken() { _token = NativeMethods.CancelTokenCreate(); @@ -487,6 +781,24 @@ internal IntPtr CancelToken() return _token; } + /// The task, once the launcher returned. A call without a native + /// cancel token stops waiting when the caller's token fires. + internal Task Launched() + { + if (_token == IntPtr.Zero && _cancellation.CanBeCanceled) + { + lock (_gate) + { + if (!_completed) + { + _registration = _cancellation.Register( + static state => ((FfiCall)state!).StopWaiting(), this); + } + } + } + return _tcs.Task; + } + /// Recovers the call behind a completion's context. Runs /// exactly once per launch, on a producer thread. internal static FfiCall Complete(IntPtr context) @@ -506,9 +818,14 @@ internal void Abandon() Release(); } + /// Completes the task with the call's result. When the caller + /// stopped waiting, a disposable result is disposed instead. internal void SetResult(T value) { - _tcs.TrySetResult(value); + if (!_tcs.TrySetResult(value) && value is IDisposable disposable) + { + disposable.Dispose(); + } } internal void SetException(Exception e) @@ -518,10 +835,10 @@ internal void SetException(Exception e) /// Faults (or cancels, for the cancelled code) the task from a /// heap-boxed error, releasing the box. - internal unsafe void SetError(FfiError* err, FfiErrorMap map) + internal unsafe void SetError(FfiError* err, delegate* map) { - var e = Ffi.TakeBoxedError(err, map, out var code); - if (code == {{EXCEPTION}}.CancelledErrorCode) + var e = Ffi.TakeBoxedError(err, map); + if (e is OperationCanceledException) { _tcs.TrySetCanceled(_cancellation); } @@ -531,6 +848,11 @@ internal unsafe void SetError(FfiError* err, FfiErrorMap map) } } + private void StopWaiting() + { + _tcs.TrySetCanceled(_cancellation); + } + // The registration callback and the completion race: whichever runs // last destroys the consumer's token reference, so a cancel never // touches a destroyed token. @@ -559,10 +881,6 @@ private void Cancel() private void Release() { - if (_token == IntPtr.Zero) - { - return; - } bool destroy; lock (_gate) { @@ -572,7 +890,10 @@ private void Release() if (destroy) { _registration.Dispose(); - NativeMethods.CancelTokenDestroy(_token); + if (_token != IntPtr.Zero) + { + NativeMethods.CancelTokenDestroy(_token); + } } } } @@ -600,38 +921,49 @@ protected override bool ReleaseHandle() } } -/// A lazily streamed sequence backed by a native iterator. It can be -/// enumerated once; each MoveNext pulls one item. -internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +/// A lazily streamed sequence backed by native iterators. Each +/// enumeration launches a new native iterator (so a launch failure throws +/// from GetEnumerator), and each MoveNext pulls one +/// item. +internal sealed unsafe class FfiSequence : IEnumerable { - private readonly FfiIteratorHandle _iterator; + private readonly Func _launch; private readonly delegate* _next; - private int _started; - private T _current = default!; - internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + internal FfiSequence(Func launch, delegate* next) { - _iterator = iterator; + _launch = launch; _next = next; } - public T Current => _current; - - object? IEnumerator.Current => _current; - public IEnumerator GetEnumerator() { - if (Interlocked.Exchange(ref _started, 1) != 0) - { - throw new InvalidOperationException("this sequence can be enumerated only once"); - } - return this; + return new FfiEnumerator(_launch(), _next); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } +} + +/// One enumeration of an : owns its +/// native iterator, released on exhaustion or disposal. +internal sealed unsafe class FfiEnumerator : IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private T _current = default!; + + internal FfiEnumerator(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; public bool MoveNext() { @@ -643,13 +975,14 @@ public bool MoveNext() { return true; } + _current = default!; _iterator.Dispose(); return false; } public void Reset() { - throw new NotSupportedException("a native iterator can't be reset"); + throw new NotSupportedException("a native iterator can't be reset; enumerate the sequence again"); } public void Dispose() @@ -659,10 +992,11 @@ public void Dispose() } /// Serializes values into the value-buffer wire format -/// (little-endian, packed). -internal sealed class FfiBufferWriter +/// (little-endian, packed) in a pooled buffer, returned on +/// . +internal sealed class FfiBufferWriter : IDisposable { - private byte[] _buf = new byte[64]; + private byte[] _buf = ArrayPool.Shared.Rent(256); private int _len; /// The bytes written so far. @@ -671,11 +1005,28 @@ internal sealed class FfiBufferWriter /// The number of bytes written so far. internal int Length => _len; + public void Dispose() + { + var buf = _buf; + _buf = Array.Empty(); + _len = 0; + if (buf.Length > 0) + { + ArrayPool.Shared.Return(buf); + } + } + private Span Grow(int extra) { if (_len + extra > _buf.Length) { - Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + var grown = ArrayPool.Shared.Rent(Math.Max(_buf.Length * 2, _len + extra)); + _buf.AsSpan(0, _len).CopyTo(grown); + if (_buf.Length > 0) + { + ArrayPool.Shared.Return(_buf); + } + _buf = grown; } var span = _buf.AsSpan(_len, extra); _len += extra; @@ -688,23 +1039,23 @@ private Span Grow(int extra) internal void WriteU8(byte v) => Grow(1)[0] = v; - internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + internal void WriteI16(short v) => BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); - internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + internal void WriteU16(ushort v) => BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); - internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + internal void WriteI32(int v) => BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); - internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + internal void WriteU32(uint v) => BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); - internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + internal void WriteI64(long v) => BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); - internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + internal void WriteU64(ulong v) => BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); - internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + internal void WriteF32(float v) => BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); - internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + internal void WriteF64(double v) => BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); - internal void WriteLen(int len) => WriteU32((uint)len); + private void WriteLen(int len) => WriteU32((uint)len); internal void WriteString(string v) { @@ -722,21 +1073,61 @@ internal void WriteBytes(byte[] v) /// Writes an object token. The token must carry its own strong /// reference (a fresh clone), which the reader adopts. internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); + + internal void WriteList(IReadOnlyList value, Action write) + { + WriteLen(value.Count); + for (var i = 0; i < value.Count; i++) + { + write(this, value[i]); + } + } + + internal void WriteMap(IReadOnlyDictionary value, Action writeKey, Action writeValue) + { + WriteLen(value.Count); + foreach (var entry in value) + { + writeKey(this, entry.Key); + writeValue(this, entry.Value); + } + } + + internal void WriteOptional(T? value, Action write) where T : class + { + WriteBool(value is not null); + if (value is not null) + { + write(this, value); + } + } + + internal void WriteOptionalValue(T? value, Action write) where T : struct + { + WriteBool(value.HasValue); + if (value is { } present) + { + write(this, present); + } + } } -/// Decodes values from the value-buffer wire format. A malformed -/// buffer is a bug on the side that encoded it and throws -/// with the marshalling code. -internal sealed class FfiBufferReader +/// Decodes values from a native value buffer, in place. The buffer +/// must stay alive while it's read. A malformed buffer is a bug on the side +/// that encoded it and throws with the +/// marshalling code. +internal sealed unsafe class FfiBufferReader { private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); - private readonly byte[] _data; + private readonly byte* _data; + private readonly int _len; private int _pos; - internal FfiBufferReader(byte[] data) + internal FfiBufferReader(byte* data, nuint len) { - _data = data; + _data = len == 0 ? null : data; + _len = checked((int)len); } /// The exception for a buffer that breaks the wire @@ -748,11 +1139,11 @@ internal FfiBufferReader(byte[] data) private ReadOnlySpan Take(int n) { - if (_data.Length - _pos < n) + if (_len - _pos < n) { throw Malformed("buffer exhausted"); } - var span = _data.AsSpan(_pos, n); + var span = new ReadOnlySpan(_data + _pos, n); _pos += n; return span; } @@ -771,26 +1162,26 @@ internal bool ReadBool() internal byte ReadU8() => Take(1)[0]; - internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + internal short ReadI16() => BinaryPrimitives.ReadInt16LittleEndian(Take(2)); - internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + internal ushort ReadU16() => BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); - internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + internal int ReadI32() => BinaryPrimitives.ReadInt32LittleEndian(Take(4)); - internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + internal uint ReadU32() => BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); - internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + internal long ReadI64() => BinaryPrimitives.ReadInt64LittleEndian(Take(8)); - internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + internal ulong ReadU64() => BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); - internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + internal float ReadF32() => BinaryPrimitives.ReadSingleLittleEndian(Take(4)); - internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + internal double ReadF64() => BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); /// A collection count. Elements can be zero-sized, so the count - /// isn't checked against the remaining bytes; callers cap their - /// preallocation with . - internal int ReadLen() + /// isn't checked against the remaining bytes; collections cap their + /// preallocation instead. + private int ReadLen() { var len = ReadU32(); if (len > int.MaxValue) @@ -800,10 +1191,6 @@ internal int ReadLen() return (int)len; } - /// A preallocation size for elements - /// that a malformed count can't inflate. - internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); - internal string ReadString() { var bytes = Take(ReadLen()); @@ -811,7 +1198,7 @@ internal string ReadString() { return Utf8Strict.GetString(bytes); } - catch (ArgumentException) + catch (DecoderFallbackException) { throw Malformed("string is not valid UTF-8"); } @@ -830,11 +1217,60 @@ internal IntPtr ReadObject() return (IntPtr)(nint)token; } + internal T[] ReadList(Func read) + { + var count = ReadLen(); + if (count == 0) + { + return Array.Empty(); + } + // A malformed count can't inflate the allocation past the input. + var items = new List(Math.Min(count, _len - _pos)); + for (var i = 0; i < count; i++) + { + items.Add(read(this)); + } + return items.ToArray(); + } + + internal Dictionary ReadMap(Func readKey, Func readValue) where K : notnull + { + var count = ReadLen(); + var map = new Dictionary(Math.Min(count, _len - _pos)); + for (var i = 0; i < count; i++) + { + var key = readKey(this); + if (!map.TryAdd(key, readValue(this))) + { + throw Malformed("repeated map key"); + } + } + return map; + } + + internal T? ReadOptional(Func read) where T : class + { + return ReadBool() ? read(this) : null; + } + + internal T? ReadOptionalValue(Func read) where T : struct + { + return ReadBool() ? read(this) : null; + } + internal void ExpectEnd() { - if (_pos != _data.Length) + if (_pos != _len) { throw Malformed("trailing bytes"); } } + + /// Returns , read from this buffer, + /// after checking that nothing trails it. + internal T End(T value) + { + ExpectEnd(); + return value; + } } diff --git a/crates/weaveffi-cli/src/targets/dotnet/tests.rs b/crates/weaveffi-cli/src/targets/dotnet/tests.rs index faa27d0b..11c36195 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/tests.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/tests.rs @@ -1,15 +1,10 @@ -//! Rendering tests over a small API exercising every C ABI revision 4 shape -//! the .NET wrapper distinguishes: the contract check, objects, nullable -//! objects, objects in records and lists, composite codecs, iterators of -//! objects, typed errors with fields, a callback interface returning every -//! family (one method `throws`) passed both required and optional, async -//! and cancellable functions, and bytes parameters. +//! .NET-specific rendering tests: project naming and configuration, the +//! spliced runtime, identifier escaping, and runtime-class collisions. The +//! `kitchen_sink` snapshot pins the rest of the generated API. -use crate::backend::LanguageBackend; use crate::package::PackageContext; use crate::platform::{BinarySet, NativeBinary, Platform}; -use camino::Utf8Path; -use weaveffi_model::contract::entries; +use crate::targets::Target; use weaveffi_model::ir::Api; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; @@ -17,84 +12,51 @@ use weaveffi_model::validate::validate; use crate::targets::dotnet::{DotnetConfig, DotnetGenerator}; -/// A `bus` module with an error domain (one code carrying a field), a -/// `Ticker` interface, a `Bundle` record holding objects, a `Subscriber` -/// callback interface, and free functions covering nullable objects, an -/// iterator of objects, async and cancellable calls, required and optional -/// callback parameters, bytes, and a nested composite; plus a nested -/// `bus.deep_inner` module inheriting the domain. +/// A `bus` module whose names collide with what the generated C# declares +/// or reserves: a record field spelled like its record and like synthesized +/// record members, keyword parameters, an interface member spelled like the +/// wrapper's `Dispose`, a code field spelled like `Exception.Message`, and a +/// record named like the runtime's root exception. const FIXTURE: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: bus errors: - name: BusError - codes: - - { name: Closed, code: 1, message: "bus closed" } - - { name: Full, code: 2, message: "bus full", fields: [{ name: capacity, type: u32 }, { name: topic, type: string }] } - enums: - - name: Level - variants: [{ name: Low, value: 0 }, { name: High, value: 1 }] + - name: BusError + codes: + - { name: Closed, code: 1, message: "bus \"closed\"" } + - { name: Full, code: 2, message: "bus full", fields: [{ name: message, type: string }] } structs: - - name: Bundle + - name: Token fields: - - { name: primary, type: Ticker } - - { name: others, type: "[Ticker]" } - - { name: label, type: string } + - { name: token, type: i64 } + - { name: to_string, type: string } + - { name: tags, type: "[string]" } + - name: NativeException + fields: + - { name: code, type: i32 } interfaces: - name: Ticker - doc: A counter the producer owns. constructors: - { name: new, params: [{ name: start, type: i32 }] } methods: - - { name: value, return: i32 } - callback_interfaces: - - name: Subscriber - doc: Receives bus events. - methods: - - name: on_message - params: - - { name: text, type: string } - - { name: weight, type: i32 } - - { name: bundle, type: Bundle } - - { name: ticker, type: Ticker } - return: bool - - { name: on_close, params: [{ name: code, type: i64 }] } - - { name: level, return: Level } - - { name: label, return: string, throws: true } - - { name: payload, return: bytes } - - { name: latest, return: "Bundle?" } - - { name: favorite, return: Ticker } - - { name: spare, return: "Ticker?" } + - { name: dispose, params: [{ name: class, type: bool }] } + - { name: ticker, return: i32 } functions: - - { name: maybe_ticker, params: [{ name: input, type: "Ticker?" }], return: "Ticker?" } - - { name: tickers, return: "iter" } - - { name: subscribe, params: [{ name: listener, type: Subscriber }] } - - { name: maybe_subscribe, params: [{ name: listener, type: "Subscriber?" }], throws: true } - - { name: spawn, return: Ticker, async: true } - - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } - - { name: roundtrip_bundle, params: [{ name: b, type: Bundle }], return: Bundle } - - { name: bundle, return: Bundle } - - { name: digest, params: [{ name: data, type: bytes }, { name: label, type: string }], return: string } - - { name: tally, params: [{ name: counts, type: "{string:[i32?]}" }], return: "[Bundle]", throws: true } - modules: - - name: deep_inner - functions: - - { name: drain, return: u32, throws: true } + - { name: send, params: [{ name: event, type: string }, { name: in, type: "i32?" }], return: Token, throws: BusError } "#; fn fixture() -> Model { - let api: Api = serde_yaml::from_str(FIXTURE).expect("fixture parses"); + let api: Api = serde_saphyr::from_str(FIXTURE).expect("fixture parses"); validate(&api, &Identity::named("bus-lib"), None).expect("fixture validates") } /// Every generated file's text, keyed by file name. fn render_files(config: &DotnetConfig) -> Vec<(String, String)> { - let model = fixture(); - DotnetGenerator - .files(&model, Utf8Path::new("out"), config) + DotnetGenerator::from(config.clone()) + .render(&fixture()) .into_iter() - .map(|f| (f.path.file_name().unwrap().to_string(), f.contents)) + .map(|f| (f.path.to_string(), f.contents)) .collect() } @@ -112,30 +74,11 @@ fn file(files: &[(String, String)], name: &str) -> String { .clone() } -fn render() -> String { - file(&render_files(&DotnetConfig::default()), "BusLib.cs") -} - -fn runtime() -> String { - file(&render_files(&DotnetConfig::default()), "Runtime.cs") -} - #[track_caller] fn assert_has(out: &str, needle: &str) { assert!(out.contains(needle), "missing `{needle}` in:\n{out}"); } -/// The text of the C# member starting at `start`, up to the next blank -/// line at its indentation. -fn member<'a>(out: &'a str, start: &str) -> &'a str { - let at = out - .find(start) - .unwrap_or_else(|| panic!("no `{start}` in:\n{out}")); - let rest = &out[at..]; - let end = rest.find("\n\n").unwrap_or(rest.len()); - &rest[..end] -} - #[test] fn names_come_from_the_identity() { let files = render_files(&DotnetConfig::default()); @@ -151,14 +94,9 @@ fn names_come_from_the_identity() { &runtime, "internal const string LibraryEnvVar = \"BUS_LIB_LIBRARY\";", ); - assert_has(&runtime, "EntryPoint = \"bus_lib_abi_version\""); - assert_has(&runtime, "internal const uint AbiVersion = 4;"); - assert_has(&runtime, "public class NativeException : Exception"); - assert_has( - &runtime, - "public sealed class NativeBugException : InvalidOperationException", - ); - assert_has(&runtime, "public const int CancelledErrorCode = -5;"); + assert_has(&runtime, "\"bus_lib_abi_version\""); + assert_has(&runtime, "public static class BusLibLibrary"); + assert_has(&runtime, "public const uint AbiVersion = 5;"); assert!( !runtime.contains("{{"), "unreplaced placeholder:\n{runtime}" @@ -166,6 +104,8 @@ fn names_come_from_the_identity() { let csproj = file(&files, "BusLib.csproj"); assert_has(&csproj, "BusLib"); assert_has(&csproj, "BusLib"); + assert_has(&csproj, "net8.0"); + assert_has(&file(&files, "README.md"), "`BusLibLibrary.Check()`"); for (name, text) in &files { let body: String = text .lines() @@ -189,456 +129,45 @@ fn configured_namespace_wins() { &file(&files, "Acme.Bus.csproj"), "Acme.Bus", ); -} - -#[test] -fn the_runtime_surface_is_revision_4() { - let runtime = runtime(); - assert_has(&runtime, "EntryPoint = \"bus_lib_alloc\""); - assert_has(&runtime, "internal static partial byte* Alloc(nuint len);"); - assert_has(&runtime, "EntryPoint = \"bus_lib_error_set_payload\""); - assert!(!runtime.contains("_dealloc"), "{runtime}"); - assert!(!runtime.to_lowercase().contains("checksum"), "{runtime}"); -} - -#[test] -fn load_checks_the_abi_revision_and_every_root_contract() { - let model = fixture(); - let out = render(); - assert_has(&out, "static partial void VerifyContracts()"); - assert_has( - &out, - "VerifyContract(\"bus_lib_bus_contract\", &bus_lib_bus_contract, new (ulong, ulong, string)[]", - ); - assert_has( - &out, - "internal static partial FfiContractEntry* bus_lib_bus_contract(nuint* out_len);", - ); - let root = model.roots().next().unwrap(); - let expected = entries(&model, root); - assert!(expected.iter().any(|e| e.path == "bus.deep_inner.drain")); - for e in &expected { - assert_has( - &out, - &format!("(0x{:016x}UL, 0x{:016x}UL, \"{}\"),", e.id, e.hash, e.path), - ); - } - let runtime = runtime(); - assert_has(&runtime, "VerifyContracts();"); - assert_has(&runtime, "is missing from the library"); - assert_has(&runtime, "changed since these bindings were generated"); -} - -#[test] -fn interfaces_wrap_a_safe_handle() { - let out = render(); - assert_has( - &out, - "public sealed unsafe class Ticker : IDisposable, IEquatable", - ); - assert_has(&out, "internal sealed class NativeHandle : SafeHandle"); - assert_has(&out, "NativeMethods.bus_lib_bus_Ticker_destroy(handle);"); - assert!( - !out.contains("~Ticker()"), - "the SafeHandle is the finalizer" - ); - // Every import borrows through the handle, never a raw pointer. - assert_has( - &out, - "internal static partial int bus_lib_bus_Ticker_value(Ticker.NativeHandle self, FfiError* out_err);", - ); - assert_has( - &out, - "internal static partial IntPtr bus_lib_bus_Ticker_clone(Ticker.NativeHandle self);", - ); - assert_has( - &out, - "var ffiResult = NativeMethods.bus_lib_bus_Ticker_value(Handle, &ffiErr);", - ); - assert_has(&out, "public Ticker(int start)"); - assert_has(&out, "Handle = new NativeHandle(ffiResult);"); - assert_has( - &out, - "return NativeMethods.bus_lib_bus_Ticker_clone(Handle);", - ); -} - -#[test] -fn nullable_objects_pass_the_null_handle() { - let out = render(); - assert_has(&out, "public static Ticker? MaybeTicker(Ticker? input)"); - assert_has( - &out, - "NativeMethods.bus_lib_bus_maybe_ticker(input?.Handle ?? global::BusLib.Ticker.NativeHandle.Null, &ffiErr);", - ); - assert_has( - &out, - "return ffiResult == IntPtr.Zero ? null : global::BusLib.Ticker.Adopt(ffiResult);", - ); -} - -#[test] -fn objects_in_records_use_cloned_tokens_and_adopt_on_read() { - let out = render(); - assert_has(&out, "public sealed class Bundle"); - assert_has(&out, "writer.WriteObject(Primary.CloneHandle());"); - assert_has(&out, "FfiCodecs.WriteListOfTicker(writer, Others);"); - assert_has(&out, "writer.WriteObject(item.CloneHandle());"); - assert_has(&out, "global::BusLib.Ticker.Adopt(reader.ReadObject()),"); - assert_has( - &out, - "list.Add(global::BusLib.Ticker.Adopt(reader.ReadObject()));", - ); -} - -#[test] -fn composites_get_one_codec_pair_each() { - let out = render(); - for name in [ - "ListOfTicker", - "ListOfBundle", - "ListOfOptionalOfI32", - "MapOfStringToListOfOptionalOfI32", - "OptionalOfBundle", - ] { - assert_eq!( - out.matches(&format!("internal static void Write{name}(")) - .count(), - 1, - "{name}" - ); - assert_eq!( - out.matches(&format!(" Read{name}(FfiBufferReader reader)")) - .count(), - 1, - "{name}" - ); - } - // The call site writes through the pair instead of inlining a loop. - let tally = member(&out, "public static Bundle[] Tally("); - assert_has( - tally, - "FfiCodecs.WriteMapOfStringToListOfOptionalOfI32(countsWriter, counts);", - ); - assert!(!tally.contains("foreach"), "{tally}"); - // A repeated map key is malformed. + // The library class takes the namespace's last segment. assert_has( - &out, - "throw FfiBufferReader.Malformed(\"repeated map key\");", + &file(&files, "Runtime.cs"), + "public static class BusLibrary", ); } #[test] -fn type_references_survive_a_member_of_the_same_name() { - let out = render(); - // Inside `Bus`, the method `Bundle()` would shadow the type `Bundle` in - // an expression, so static accesses go through the global namespace. - assert_has(&out, "public static Bundle Bundle()"); - assert_has( - &out, - "return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::BusLib.Bundle.ReadFrom(r));", - ); -} - -#[test] -fn strings_and_bytes_cross_as_pointer_and_length() { - let out = render(); - assert_has( - &out, - "internal static partial byte* bus_lib_bus_digest(byte* data_ptr, nuint data_len, byte* label_ptr, nuint label_len, nuint* out_len, FfiError* out_err);", - ); - assert_has( - &out, - "public static string Digest(ReadOnlySpan data, string label)", - ); - assert_has(&out, "var labelBytes = Ffi.Utf8(label);"); - assert_has(&out, "fixed (byte* dataPtr = data, labelPtr = labelBytes)"); - assert_has(&out, "return Ffi.TakeString(ffiResult, ffiOutLen);"); - // The byte[] overload forwards to the span one. - assert_has( - &out, - "public static string Digest(byte[] data, string label) => Digest((ReadOnlySpan)data, label);", - ); -} - -#[test] -fn errors_follow_the_throws_flag() { - let out = render(); - // A domain is an abstract class with one nested class per code; fields - // are typed properties and the payload decodes into them. - assert_has(&out, "public abstract class BusException : NativeException"); - assert_has(&out, "public sealed class Full : BusException"); - assert_has(&out, "public const int ErrorCode = 2;"); - assert_has(&out, "public uint Capacity { get; }"); - assert_has(&out, "public string Topic { get; }"); - assert_has( - &out, - "public Full(uint capacity, string topic, string? message = null) : base(ErrorCode, message ?? \"bus full\")", - ); - assert_has(&out, "case Full.ErrorCode:"); - assert_has(&out, "return new Closed(given);"); - // A throwing call maps through its domain, a non-throwing one traps. - let tally = member(&out, "public static Bundle[] Tally("); - assert_has( - tally, - "throw Ffi.TakeError(&ffiErr, global::BusLib.BusException.FromError);", - ); - let digest = member(&out, "public static string Digest(ReadOnlySpan"); - assert_has( - digest, - "throw Ffi.TakeError(&ffiErr, global::BusLib.NativeBugException.FromError);", - ); - // A nested module inherits the domain, and the doc names the - // declaring module's dotted path. - let drain = member(&out, "public static uint Drain()"); - assert_has(drain, "global::BusLib.BusException.FromError"); - assert_has(&out, "declared by module bus."); - let runtime = runtime(); - assert_has(&runtime, "native call failed with code {code}: {message}"); - assert_has(&runtime, "return new OperationCanceledException(message);"); -} - -#[test] -fn the_domain_doc_names_the_dotted_module_path() { - let yaml = r#" -version: "0.11.0" -modules: - - name: edge_cases - errors: - name: EdgeError - codes: [{ name: Busy, code: 7, message: "busy" }] - functions: - - { name: poke, throws: true } -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let model = validate(&api, &Identity::named("edge"), None).unwrap(); - let out = super::render_csharp( - &model, - "Edge", - ("NativeException", "NativeBugException"), - "Edge.cs", - ); - assert_has(&out, "declared by module edge_cases."); - assert!(!out.contains("edge.cases"), "{out}"); -} - -#[test] -fn iterators_stream_through_a_safe_handle() { - let out = render(); - assert_has(&out, "public static IEnumerable Tickers()"); - assert_has( - &out, - "return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.bus_lib_bus_TickersIterator_destroy), &NextTickers);", - ); - assert_has( - &out, - "private static bool NextTickers(FfiIteratorHandle iter, out Ticker item)", - ); - assert_has( - &out, - "internal static partial int bus_lib_bus_TickersIterator_next(FfiIteratorHandle iter, IntPtr* out_item, FfiError* out_err);", - ); - assert_has(&out, "item = global::BusLib.Ticker.Adopt(ffiItem);"); -} - -#[test] -fn async_calls_complete_through_an_unmanaged_callback() { - let out = render(); - assert_has(&out, "public static Task Spawn()"); - assert_has(&out, "var ffiCall = new FfiCall();"); - assert_has( - &out, - "NativeMethods.bus_lib_bus_spawn(&CompleteSpawn, ffiCall.Context);", - ); - assert_has( - &out, - "internal static partial void bus_lib_bus_spawn(delegate* unmanaged[Cdecl] callback, IntPtr context);", - ); - assert_has( - &out, - "private static void CompleteSpawn(IntPtr context, FfiError* err, IntPtr result)", - ); - assert_has( - &out, - "ffiCall.SetResult(global::BusLib.Ticker.Adopt(result));", - ); - assert_has(&out, "ffiCall.Abandon();"); -} - -#[test] -fn cancellable_calls_take_a_cancellation_token() { - let out = render(); - assert_has( - &out, - "public static Task Wait(long ms, CancellationToken cancellationToken = default)", - ); - assert_has(&out, "return Task.FromCanceled(cancellationToken);"); - assert_has(&out, "var ffiCall = new FfiCall(cancellationToken);"); - assert_has( - &out, - "NativeMethods.bus_lib_bus_wait(ms, ffiCall.CancelToken(), &CompleteWait, ffiCall.Context);", - ); - let runtime = runtime(); - assert_has(&runtime, "_tcs.TrySetCanceled(_cancellation);"); - assert_has(&runtime, "NativeMethods.CancelTokenCancel(_token);"); -} - -#[test] -fn callback_vtables_start_with_the_header() { - let out = render(); - assert_has( - &out, - "internal static unsafe class FfiVtable_bus_Subscriber", - ); - let layout = member(&out, "private struct Layout"); - let header = [ - "public uint Size;", - "public uint Flags;", - "public delegate* unmanaged[Cdecl] Free;", - "public delegate* unmanaged[Cdecl] on_message;", - ]; - let mut at = 0; - for field in header { - let found = layout[at..] - .find(field) - .unwrap_or_else(|| panic!("`{field}` out of order in:\n{layout}")); - at += found + field.len(); - } - assert_has(&out, "vtable->Size = (uint)sizeof(Layout);"); - assert_has(&out, "vtable->Flags = 0;"); - assert_has(&out, "vtable->Free = &FreeTrampoline;"); - assert_has(&out, "vtable->on_message = &OnMessageTrampoline;"); -} - -#[test] -fn callback_methods_receive_borrowed_arguments_and_adopt_objects() { - let out = render(); - assert_has(&out, "public interface ISubscriber"); - assert_has( - &out, - "bool OnMessage(string text, int weight, Bundle bundle, Ticker ticker);", - ); - let trampoline = member(&out, "private static byte OnMessageTrampoline("); - assert_has( - trampoline, - "private static byte OnMessageTrampoline(IntPtr ctx, byte* text_ptr, nuint text_len, int weight, byte* bundle_ptr, nuint bundle_len, IntPtr ticker, FfiError* out_err)", - ); - assert_has(trampoline, "Ffi.ReadString(text_ptr, text_len)"); - assert_has( - trampoline, - "FfiCodecs.Decode(Ffi.ReadBuffer(bundle_ptr, bundle_len), static r => global::BusLib.Bundle.ReadFrom(r))", - ); - assert_has(trampoline, "global::BusLib.Ticker.Adopt(ticker)"); - assert_has(trampoline, "Ffi.SetForeignError(out_err, e);"); - // Arguments are borrowed: nothing is freed inside a trampoline. - assert!(!trampoline.contains("Take"), "{trampoline}"); -} - -#[test] -fn callback_methods_return_every_family() { - let out = render(); - assert_has(&out, "Level Level();"); - assert_has(&out, "string Label();"); - assert_has(&out, "byte[] Payload();"); - assert_has(&out, "Bundle? Latest();"); - assert_has(&out, "Ticker Favorite();"); - assert_has(&out, "Ticker? Spare();"); - // Direct values by value. - assert_has( - member(&out, "private static int LevelTrampoline("), - "return (int)Ffi.Target(ctx).Level();", - ); - // Strings, bytes, and buffers as `{prefix}_alloc` runs in the out slots. - let label = member(&out, "private static void LabelTrampoline("); - assert_has( - label, - "LabelTrampoline(IntPtr ctx, byte** out_ptr, nuint* out_len, FfiError* out_err)", - ); - assert_has( - label, - "Ffi.ReturnString(Ffi.Target(ctx).Label(), out_ptr, out_len);", - ); - assert_has( - member(&out, "private static void PayloadTrampoline("), - "Ffi.ReturnBytes(Ffi.Target(ctx).Payload(), out_ptr, out_len);", - ); - let latest = member(&out, "private static void LatestTrampoline("); - assert_has( - latest, - "FfiCodecs.WriteOptionalOfBundle(ffiWriter, ffiResult);", - ); - assert_has(latest, "Ffi.ReturnBuffer(ffiWriter, out_ptr, out_len);"); - // Objects as a fresh reference; a null passes through for the producer - // to judge. - assert_has( - member(&out, "private static IntPtr FavoriteTrampoline("), - "return Ffi.Target(ctx).Favorite()?.CloneHandle() ?? IntPtr.Zero;", - ); - assert_has( - member(&out, "private static IntPtr SpareTrampoline("), - "return Ffi.Target(ctx).Spare()?.CloneHandle() ?? IntPtr.Zero;", - ); - let runtime = runtime(); - assert_has( - &runtime, - "var run = NativeMethods.Alloc((nuint)bytes.Length);", - ); -} - -#[test] -fn throwing_callback_methods_report_the_domain_error() { - let out = render(); - let label = member(&out, "private static void LabelTrampoline("); - assert_has(label, "catch (global::BusLib.BusException e)"); - assert_has( - label, - "Ffi.SetDomainError(out_err, e.Code, e, e.WritePayload);", - ); - assert_has(label, "catch (Exception e)"); - // Only `throws` methods report the domain; the rest are foreign errors. - let payload = member(&out, "private static void PayloadTrampoline("); - assert!(!payload.contains("BusException"), "{payload}"); - // Each code with fields encodes them as the payload. - assert_has( - &out, - "internal override void WritePayload(FfiBufferWriter writer)\n {\n writer.WriteU32(Capacity);\n writer.WriteString(Topic);", - ); - let runtime = runtime(); - assert_has( - &runtime, - "NativeMethods.ErrorSetPayload(err, ptr, (nuint)payload.Length);", - ); -} - -#[test] -fn passing_a_callback_interface_registers_it_after_encoding() { - let out = render(); - assert_has(&out, "public static void Subscribe(ISubscriber listener)"); - assert_has(&out, "var listenerCtx = Ffi.Register(listener);"); - assert_has( - &out, - "NativeMethods.bus_lib_bus_subscribe(listenerCtx, FfiVtable_bus_Subscriber.Pointer, &ffiErr);", - ); - // A call that never reaches the producer releases the registration. - assert_has(&out, "Ffi.Unregister(listenerCtx);"); +fn runtime_classes_step_around_api_types() { + let files = render_files(&DotnetConfig::default()); + let runtime = file(&files, "Runtime.cs"); + // The API declares a `NativeException` record. + assert_has(&runtime, "public class BusLibNativeException : Exception"); + assert_has(&runtime, "public sealed class NativeBugException"); + let api = file(&files, "BusLib.cs"); + assert_has(&api, "public sealed record NativeException(int Code)"); + assert_has(&api, "public class BusException : BusLibNativeException"); } #[test] -fn an_absent_optional_callback_passes_a_null_vtable() { - let out = render(); +fn identifiers_are_escaped() { + let api = file(&render_files(&DotnetConfig::default()), "BusLib.cs"); + // Record properties never collide with the record or its members. assert_has( - &out, - "public static void MaybeSubscribe(ISubscriber? listener)", + &api, + "public sealed record Token(long Token_, string ToString_, IReadOnlyList Tags)", ); + // Keywords are verbatim identifiers; locals derive from the bare name. + assert_has(&api, "public static Token Send(string @event, int? @in)"); + assert_has(&api, "using var eventUtf8 = new FfiUtf8(@event);"); + assert_has(&api, "public void Dispose_(bool @class)"); + assert_has(&api, "public int Ticker_()"); + // A code field spelled like `Exception.Message` and a quoted message. + assert_has(&api, "public string Message_ { get; }"); assert_has( - &out, - "NativeMethods.bus_lib_bus_maybe_subscribe(listenerCtx, listener == null ? IntPtr.Zero : FfiVtable_bus_Subscriber.Pointer, &ffiErr);", - ); - let runtime = runtime(); - assert_has( - &runtime, - "return implementation == null ? IntPtr.Zero : GCHandle.ToIntPtr(GCHandle.Alloc(implementation));", + &api, + "public Full(string message_, string? message = null) : base(ErrorCode, message ?? \"bus full\")", ); + assert_has(&api, "message ?? \"bus \\\"closed\\\"\""); } #[test] @@ -657,8 +186,8 @@ fn package_bundles_desktop_runtimes_only() { binaries.insert(NativeBinary::new(p, format!("/tmp/{}/lib", p.id()))); } let ctx = PackageContext::new(&binaries); - let artifacts = DotnetGenerator - .package(&model, &ctx, &DotnetConfig::default()) + let artifacts = DotnetGenerator::from(DotnetConfig::default()) + .package(&model, &ctx) .expect("dotnet packages"); assert_eq!(artifacts.len(), 1); assert_eq!(artifacts[0].path, "dotnet/BusLib"); diff --git a/crates/weaveffi-cli/src/targets/dotnet/types.rs b/crates/weaveffi-cli/src/targets/dotnet/types.rs index 8589fb8d..58c13039 100644 --- a/crates/weaveffi-cli/src/targets/dotnet/types.rs +++ b/crates/weaveffi-cli/src/targets/dotnet/types.rs @@ -1,11 +1,17 @@ -//! C# type mapping: the idiomatic surface type of every IR type, the -//! blittable spelling of every C ABI type, identifier escaping, and +//! C# type mapping: the surface type of every IR type in every position, +//! the blittable spelling of every C ABI type, identifier escaping, and //! string-literal escaping. +//! +//! Value types have one surface everywhere (a list is an +//! `IReadOnlyList`, a map an `IReadOnlyDictionary`). The families +//! that cross the boundary without a value buffer refine it there: a typed +//! array (`[f64]`) or `bytes` parameter is a `ReadOnlySpan` pinned in +//! place, and the same types come back as arrays. use crate::lang; use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_model::abi::{AbiParam, CType}; -use weaveffi_model::model::FnBinding; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; use weaveffi_model::ty::{Prim, Ty}; /// The names every renderer shares. @@ -35,37 +41,120 @@ pub(crate) fn callback_interface_cs(name: &str) -> String { } /// The internal static class hosting one callback interface's vtable and -/// trampolines, named from the declaring module's path and the name, like -/// its C vtable tag. -pub(crate) fn vtable_class_cs(module_path: &str, name: &str) -> String { - format!("FfiVtable_{module_path}_{}", name) +/// trampolines (`FfiListenerVtable`). +pub(crate) fn vtable_class_cs(name: &str) -> String { + format!("Ffi{name}Vtable") } -/// The idiomatic C# surface type for one IR type, as it appears in wrapper -/// signatures, properties, and locals. +/// The C# keyword or BCL name of a scalar primitive. +pub(crate) fn prim_cs(p: Prim) -> &'static str { + match p { + Prim::I8 => "sbyte", + Prim::I16 => "short", + Prim::I32 => "int", + Prim::I64 => "long", + Prim::U8 => "byte", + Prim::U16 => "ushort", + Prim::U32 => "uint", + Prim::U64 => "ulong", + Prim::F32 => "float", + Prim::F64 => "double", + Prim::Bool => "bool", + Prim::String => "string", + Prim::Bytes => "byte[]", + } +} + +/// The surface type of a value type, as a record field, a buffered +/// parameter or return, and every nested position. pub(crate) fn cs_type(ty: &Ty) -> String { match ty { - Ty::Prim(Prim::I8) => "sbyte".into(), - Ty::Prim(Prim::I16) => "short".into(), - Ty::Prim(Prim::I32) => "int".into(), - Ty::Prim(Prim::U8) => "byte".into(), - Ty::Prim(Prim::U16) => "ushort".into(), - Ty::Prim(Prim::U32) => "uint".into(), - Ty::Prim(Prim::I64) => "long".into(), - Ty::Prim(Prim::U64) => "ulong".into(), - Ty::Prim(Prim::F32) => "float".into(), - Ty::Prim(Prim::F64) => "double".into(), - Ty::Prim(Prim::Bool) => "bool".into(), - Ty::Prim(Prim::String) => "string".into(), - Ty::Prim(Prim::Bytes) => "byte[]".into(), + Ty::Prim(p) => prim_cs(*p).into(), // Records, rich enums, C-style enums, and interfaces surface under // their (global) name, because every module shares one namespace. Ty::Record(name) | Ty::RichEnum(name) | Ty::Enum(name) | Ty::Interface(name) => name.into(), Ty::Optional(inner) => format!("{}?", cs_type(inner)), - Ty::List(inner) => format!("{}[]", cs_type(inner)), - Ty::Iterator(inner) => format!("IEnumerable<{}>", cs_type(inner)), - Ty::Map(k, v) => format!("Dictionary<{}, {}>", cs_type(k), cs_type(v)), - Ty::CallbackInterface(name) => callback_interface_cs(name), + Ty::List(inner) => format!("IReadOnlyList<{}>", cs_type(inner)), + Ty::Map(k, v) => format!("IReadOnlyDictionary<{}, {}>", cs_type(k), cs_type(v)), + } +} + +/// True when the surface type of `ty` is a C# value type, which matters for +/// `T?` (a `Nullable` rather than an annotation). +pub(crate) fn is_value_type(ty: &Ty) -> bool { + match ty { + Ty::Prim(p) => !matches!(p, Prim::String | Prim::Bytes), + Ty::Enum(_) => true, + _ => false, + } +} + +/// The element type of a typed array. +fn array_of(elem: Prim) -> String { + format!("{}[]", prim_cs(elem)) +} + +/// The type of a parameter the caller passes: typed arrays and bytes as a +/// span pinned for the call, a callback interface as its C# interface. +pub(crate) fn param_cs(pass: &ArgPass, value: Option<&Ty>) -> String { + match pass { + ArgPass::Slice { elem, .. } => format!("ReadOnlySpan<{}>", prim_cs(*elem)), + ArgPass::Bytes { .. } => "ReadOnlySpan".into(), + ArgPass::Callback { + interface, + nullable, + .. + } => { + let iface = callback_interface_cs(interface); + if *nullable { + format!("{iface}?") + } else { + iface + } + } + _ => cs_type(value.expect("only callback parameters have no value type")), + } +} + +/// The type a sync call returns. +pub(crate) fn ret_cs(pass: &RetPass, ty: &Ty) -> String { + match pass { + RetPass::Slice { elem, .. } => array_of(*elem), + _ => cs_type(ty), + } +} + +/// The type an async call's task carries. +pub(crate) fn result_cs(pass: &ResultPass, ty: &Ty) -> String { + match pass { + ResultPass::Slice { elem, .. } => array_of(*elem), + _ => cs_type(ty), + } +} + +/// The type an iterator yields. +pub(crate) fn item_cs(pass: &ItemPass, ty: &Ty) -> String { + match pass { + ItemPass::Slice { elem, .. } => array_of(*elem), + _ => cs_type(ty), + } +} + +/// The type a callback-interface method receives: typed arrays and bytes +/// as spans borrowed for the call. +pub(crate) fn callback_param_cs(pass: &ArgPass, ty: &Ty) -> String { + match pass { + ArgPass::Slice { elem, .. } => format!("ReadOnlySpan<{}>", prim_cs(*elem)), + ArgPass::Bytes { .. } => "ReadOnlySpan".into(), + _ => cs_type(ty), + } +} + +/// The type a callback-interface method returns. +pub(crate) fn callback_ret_cs(pass: &CallbackRetPass, ty: &Ty) -> String { + match pass { + CallbackRetPass::Slice { elem, .. } => array_of(*elem), + _ => cs_type(ty), } } @@ -82,17 +171,18 @@ fn is_opaque(ty: &CType) -> bool { ) } -/// The blittable C# spelling of one C ABI type. Every pointer to an opaque -/// type is an `IntPtr`; every other pointer is a real C# pointer -/// (`byte*`, `FfiError*`, `byte**`), so no runtime marshalling is involved. -/// `bool` crosses as its one-byte representation. -pub(crate) fn cs_ctype(ty: &CType) -> String { +/// The blittable C# spelling of one C ABI type, qualified with `ns` for the +/// API's enums. Every pointer to an opaque type is an `IntPtr`; every other +/// pointer is a real C# pointer (`byte*`, `FfiError*`, `double**`), so no +/// runtime marshalling is involved. +pub(crate) fn cs_ctype(ns: &str, ty: &CType) -> String { match ty { CType::Int8 => "sbyte".into(), CType::Int16 => "short".into(), - CType::Int32 | CType::Enum { .. } => "int".into(), + CType::Int32 => "int".into(), CType::Int64 => "long".into(), - CType::Uint8 | CType::Bool | CType::Char => "byte".into(), + CType::Uint8 | CType::Char => "byte".into(), + CType::Bool => "bool".into(), CType::Uint16 => "ushort".into(), CType::Uint32 => "uint".into(), CType::Uint64 => "ulong".into(), @@ -101,8 +191,10 @@ pub(crate) fn cs_ctype(ty: &CType) -> String { CType::Size => "nuint".into(), CType::Void => "void".into(), CType::Error => "FfiError".into(), + // A C# enum is an `int` underneath, the same as the C typedef. + CType::Enum { name, .. } => format!("global::{ns}.{name}"), CType::Ptr { pointee, .. } if is_opaque(pointee) => "IntPtr".into(), - CType::Ptr { pointee, .. } => format!("{}*", cs_ctype(pointee)), + CType::Ptr { pointee, .. } => format!("{}*", cs_ctype(ns, pointee)), CType::StructTag { .. } | CType::VtableTag { .. } | CType::CancelToken @@ -112,9 +204,9 @@ pub(crate) fn cs_ctype(ty: &CType) -> String { /// The `delegate* unmanaged[Cdecl]<...>` function-pointer type for a C /// function taking `params` and returning `ret`. -pub(crate) fn fn_pointer_type(params: &[AbiParam], ret: &CType) -> String { - let mut parts: Vec = params.iter().map(|p| cs_ctype(&p.ty)).collect(); - parts.push(cs_ctype(ret)); +pub(crate) fn fn_pointer_type(ns: &str, params: &[AbiParam], ret: &CType) -> String { + let mut parts: Vec = params.iter().map(|p| cs_ctype(ns, &p.ty)).collect(); + parts.push(cs_ctype(ns, ret)); format!("delegate* unmanaged[Cdecl]<{}>", parts.join(", ")) } @@ -127,7 +219,19 @@ pub(crate) fn xml_text(s: &str) -> String { /// Escapes a string for embedding in a C# string literal. pub(crate) fn cs_str(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '\\' => out.push_str("\\\\"), + '"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + '\0' => out.push_str("\\0"), + c => out.push(c), + } + } + out } /// Escape `name` with the `@` verbatim prefix when C# reserves it. @@ -139,6 +243,11 @@ pub(crate) fn safe_cs_name(name: &str) -> String { } } +/// A parameter or local spelled in lowerCamelCase, escaped. +pub(crate) fn camel(name: &str) -> String { + safe_cs_name(&name.to_lower_camel_case()) +} + /// Members every interface wrapper class declares or inherits: its handle /// plumbing (`Handle`, `Adopt`, `CloneHandle`, the nested `NativeHandle`), /// `IDisposable.Dispose`, and `object`'s members. Statics share the @@ -159,12 +268,50 @@ const OBJECT_MEMBERS: &[&str] = &[ "ToString", ]; -/// The C# name of a member of the interface `class`: PascalCase, with a -/// trailing `_` when it would collide with a member the wrapper declares or -/// inherits (`dispose` is `Dispose_`) or with the class's own name, which -/// C# reserves for constructors. -pub(crate) fn cs_member(name: &str, class: &str) -> String { - let member = lang::escape_member(&name.to_upper_camel_case(), OBJECT_MEMBERS); +/// Members every record (and rich-enum variant) declares or inherits: the +/// synthesized record members, `object`'s members, and the codec pair. +const RECORD_MEMBERS: &[&str] = &[ + "Clone", + "Deconstruct", + "EqualityContract", + "Equals", + "Finalize", + "GetHashCode", + "GetType", + "MemberwiseClone", + "PrintMembers", + "ReadFrom", + "ReferenceEquals", + "ToString", + "WriteTo", +]; + +/// Members every domain exception class declares or inherits. +const EXCEPTION_MEMBERS: &[&str] = &[ + "Code", + "Data", + "ErrorCode", + "Equals", + "FromError", + "GetBaseException", + "GetHashCode", + "GetObjectData", + "GetType", + "HResult", + "HelpLink", + "InnerException", + "Message", + "Source", + "StackTrace", + "TargetSite", + "ToString", + "WritePayload", +]; + +/// PascalCase `name`, with a trailing `_` when it would collide with one of +/// `members` or with `class`, which C# reserves for constructors. +fn member_in(name: &str, class: &str, members: &[&str]) -> String { + let member = lang::escape_member(&name.to_upper_camel_case(), members); if member == class { format!("{member}_") } else { @@ -172,15 +319,21 @@ pub(crate) fn cs_member(name: &str, class: &str) -> String { } } -/// A copy of `f` whose parameter names are lowerCamelCase, the C# parameter -/// convention. Only wrapper signatures and their locals derive from these -/// names; ABI slot names keep the IDL spelling. -pub(crate) fn camel_fn(f: &FnBinding) -> FnBinding { - let mut f = f.clone(); - for p in &mut f.params { - p.name = p.name.to_lower_camel_case(); - } - f +/// The C# name of a member of the interface wrapper `class` (`dispose` is +/// `Dispose_`). +pub(crate) fn cs_member(name: &str, class: &str) -> String { + member_in(name, class, OBJECT_MEMBERS) +} + +/// The C# property of a record or rich-enum variant field (`to_string` is +/// `ToString_`, a `token` field of a `Token` record is `Token_`). +pub(crate) fn field_cs(name: &str, class: &str) -> String { + member_in(name, class, RECORD_MEMBERS) +} + +/// The C# property of an error code's field. +pub(crate) fn exception_field_cs(name: &str, class: &str) -> String { + member_in(name, class, EXCEPTION_MEMBERS) } #[cfg(test)] @@ -188,11 +341,23 @@ mod tests { use super::*; #[test] - fn interface_members_avoid_the_wrapper_members() { + fn members_avoid_what_the_class_declares() { assert_eq!(cs_member("dispose", "Store"), "Dispose_"); assert_eq!(cs_member("to_string", "Store"), "ToString_"); assert_eq!(cs_member("handle", "Store"), "Handle_"); assert_eq!(cs_member("store", "Store"), "Store_"); assert_eq!(cs_member("close", "Store"), "Close"); + assert_eq!(field_cs("token", "Token"), "Token_"); + assert_eq!(field_cs("equality_contract", "Item"), "EqualityContract_"); + assert_eq!(field_cs("label", "Item"), "Label"); + assert_eq!(exception_field_cs("message", "NotFound"), "Message_"); + assert_eq!(exception_field_cs("code", "NotFound"), "Code_"); + } + + #[test] + fn strings_escape_for_literals() { + assert_eq!(cs_str("a\"b\\c\nd\0"), "a\\\"b\\\\c\\nd\\0"); + assert_eq!(camel("class"), "@class"); + assert_eq!(camel("ttl_seconds"), "ttlSeconds"); } } diff --git a/crates/weaveffi-cli/src/targets/go/callbacks.rs b/crates/weaveffi-cli/src/targets/go/callbacks.rs index 7e584115..a0c5ba80 100644 --- a/crates/weaveffi-cli/src/targets/go/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/go/callbacks.rs @@ -10,14 +10,17 @@ use crate::cabi::c_param_name; use crate::codegen::CodeWriter; use weaveffi_model::abi::{AbiParam, CType}; -use weaveffi_model::model::{CallShape, CallbackInterfaceBinding, CallbackMethodBinding, Model}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; +use weaveffi_model::model::{ + CallShape, CallbackInterfaceBinding, CallbackMethodBinding, CallbackParamBinding, Model, +}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; +use weaveffi_model::ty::Ty; use crate::targets::go::calls::{completion_trampoline, vtable_accessor}; use crate::targets::go::codec::{func, read_fn, write_fn}; -use crate::targets::go::docs::{GoDoc, Kind}; -use crate::targets::go::names::{self, domain_type, pascal}; -use crate::targets::go::types::{cgo_type, from_c_direct, go_type, strip_const, to_c_direct}; +use crate::targets::go::docs::GoDoc; +use crate::targets::go::names::{self, pascal}; +use crate::targets::go::types::{cgo_pointee, cgo_type, go_type, prim_type, strip_const}; use crate::targets::go::Ctx; /// The C name of the exported trampoline behind one vtable entry; @@ -69,8 +72,8 @@ pub(crate) fn emit_preamble_decls(w: &mut CodeWriter, model: &Model) { for m in &cb.methods { w.line(extern_decl( &trampoline(&cb.c_tag, &m.name), - &m.abi_ret, - &m.abi_params, + &m.abi.ret, + &m.abi.params, prefix, )); } @@ -87,6 +90,8 @@ pub(crate) fn emit_preamble_decls(w: &mut CodeWriter, model: &Model) { "};", |w| { w.line(format!(".size = sizeof({tag}),")); + // Go may run a callback on any thread, so the vtable isn't + // thread-affine. w.line(".flags = 0,"); w.line(format!(".free = {free},")); for m in &cb.methods { @@ -94,13 +99,13 @@ pub(crate) fn emit_preamble_decls(w: &mut CodeWriter, model: &Model) { // An entry whose C signature has `const` pointers is // cast to the field's exact type, since the exported // Go function is declared const-free. - if m.abi_params.iter().any(|p| strip_const(&p.ty) != p.ty) { + if m.abi.params.iter().any(|p| strip_const(&p.ty) != p.ty) { let types: Vec = - m.abi_params.iter().map(|p| p.ty.render_c(prefix)).collect(); + m.abi.params.iter().map(|p| p.ty.render_c(prefix)).collect(); w.line(format!( ".{} = ({} (*)({})){tramp},", c_param_name(&m.name), - m.abi_ret.render_c(prefix), + m.abi.ret.render_c(prefix), types.join(", ") )); } else { @@ -128,14 +133,14 @@ pub(crate) fn emit_preamble_decls(w: &mut CodeWriter, model: &Model) { } /// The Go method signature of one callback method: its parameters, and its -/// result, as `(T, error)` or `error` when it throws. +/// result, as `(T, error)` or `error` when it declares errors. fn method_sig(m: &CallbackMethodBinding) -> String { let params: Vec = m .params .iter() .map(|p| format!("{} {}", names::method_param(&p.name), go_type(&p.ty))) .collect(); - let ret = match (&m.ret, m.throws) { + let ret = match (&m.ret, m.error.throws()) { (Some(ty), true) => format!(" ({}, error)", go_type(ty)), (Some(ty), false) => format!(" {}", go_type(ty)), (None, true) => " error".into(), @@ -153,39 +158,48 @@ pub(crate) fn render_callback_interface( cb: &CallbackInterfaceBinding, ) { let name = pascal(&cb.name); - let domain = ctx.domain.map(domain_type); - GoDoc::new(&name, cb.doc.as_deref(), Kind::Value, None) + let (doc, deprecated) = ctx.docs.of(&cb.doc, &cb.deprecated); + GoDoc::decl(&name, doc) .para(&format!( "Implement {name} in Go and pass the value to the functions that take it. \ The native library may call its methods from any thread until it \ - releases the implementation. A method that panics, or a throwing method \ - that returns an error outside its domain, fails the native call in \ - progress as a callback failure (code -4)." + releases the implementation. A method that panics fails the native call \ + in progress as a callback failure (code -4)." )) - .deprecated(cb.deprecated.as_deref()) + .deprecated(deprecated) .emit(w); w.block(format!("type {name} interface {{"), "}", |w| { for m in &cb.methods { - let kind = Kind::callable(m.ret.is_some()); - let mut doc = GoDoc::new(&pascal(&m.name), m.doc.as_deref(), kind, None) - .params(&m.params, names::method_param); - if m.throws { - let line = match &domain { - Some(d) => format!( - "Return a {d} to report that code, with its fields, to the native caller." - ), - None => "A returned error reaches the native caller as code -4.".into(), - }; - doc = doc.para(&line); + let (doc, deprecated) = ctx.docs.of(&m.doc, &m.deprecated); + let params = m.params.iter().filter_map(|p| { + let (doc, _) = ctx.docs.of(&p.doc, &None); + doc.map(|d| (names::method_param(&p.name), d)) + }); + let mut doc = GoDoc::plain(doc).params(params); + match &m.error { + ErrorStrategy::Domain(d) => { + doc = doc.para(&format!( + "Return a {} to report one of its codes, with its fields, to \ + the native caller; any other error reports code -1 with its \ + text.", + ctx.names.domain(d).iface + )); + } + ErrorStrategy::Untyped => { + doc = doc.para( + "A returned error reaches the native caller as code -1 with its text.", + ); + } + ErrorStrategy::Trap => {} } - doc.deprecated(m.deprecated.as_deref()).emit(w); + doc.deprecated(deprecated).emit(w); w.line(method_sig(m)); } }); w.blank(); for m in &cb.methods { - render_trampoline(w, ctx, cb, m, &name, domain.as_deref()); + render_trampoline(w, ctx, cb, m, &name); } let free = trampoline(&cb.c_tag, "free"); @@ -198,34 +212,43 @@ pub(crate) fn render_callback_interface( } /// The Go expression converting one callback argument's C slots into the -/// Go value the implementation receives: strings, bytes, and buffers are -/// borrowed for the call, so they're copied or decoded; an object transfers +/// Go value the implementation receives: strings, bytes, typed arrays, and +/// buffers are borrowed for the call, so they're copied or decoded; an +/// optional scalar is joined from its flag and value; an object transfers /// one strong reference, adopted into a new wrapper. -fn argument(p: &weaveffi_model::model::ParamBinding) -> String { - match p.arg_pass() { +fn argument(p: &CallbackParamBinding) -> String { + let s = |slot: &weaveffi_model::abi::AbiParam| names::slot(&slot.name); + match &p.pass { + ArgPass::Direct { slot } => format!("{}({})", go_type(&p.ty), s(slot)), + ArgPass::OptDirect { has, value, inner } => { + format!( + "wvOptional(bool({}), {}({}))", + s(has), + go_type(inner), + s(value) + ) + } + ArgPass::Slice { ptr, len, elem } => { + format!( + "wvBorrowSlice[{}]({}, {})", + prim_type(*elem), + s(ptr), + s(len) + ) + } + ArgPass::String { ptr, len } => format!("wvBorrowString({}, {})", s(ptr), s(len)), + ArgPass::Bytes { ptr, len } => format!("wvBorrowBytes({}, {})", s(ptr), s(len)), ArgPass::Buffer { ptr, len } => format!( "wvDecodeBorrowed({}, {}, {})", - names::slot(&ptr.name), - names::slot(&len.name), + s(ptr), + s(len), read_fn(&p.ty) ), - ArgPass::String { ptr, len } => format!( - "wvBorrowString({}, {})", - names::slot(&ptr.name), - names::slot(&len.name) - ), - ArgPass::Bytes { ptr, len } => format!( - "wvBorrowBytes({}, {})", - names::slot(&ptr.name), - names::slot(&len.name) - ), - ArgPass::Object { slot, .. } => { - let n = p.ty.interface_name().expect("an object names an interface"); - format!("wvAdopt{}({})", pascal(n), names::slot(&slot.name)) - } - ArgPass::Direct { slot } => from_c_direct(&names::slot(&slot.name), &p.ty), + ArgPass::Object { + slot, interface, .. + } => format!("wvAdopt{}({})", pascal(interface), s(slot)), ArgPass::Callback { .. } => { - unreachable!("validation rejects callback interfaces as callback-method parameters") + unreachable!("a callback method parameter is never a callback") } } } @@ -233,11 +256,12 @@ fn argument(p: &weaveffi_model::model::ParamBinding) -> String { /// Render the exported trampoline behind one vtable entry. It recovers the /// implementation from the context, converts the arguments, calls the Go /// method, and hands the result back: a direct value as the C return, an -/// object as a fresh strong reference the native library adopts (nil stays -/// null), and a string, bytes, or buffer as a run allocated with the -/// library's allocator in the out slots. A throwing method's error is -/// reported through `out_err` with its domain code and payload (code -4 -/// for any other error), and a panic is recovered into code -4; nothing +/// optional scalar as the C return (present) plus its out slot, an object +/// as a fresh strong reference the native library adopts (nil stays null), +/// and a string, bytes, typed array, or buffer as a run allocated with the +/// library's allocator in the out slots. A returned error is reported +/// through `out_err`: a code of the method's domain with its payload, or +/// code -1 with its text; a panic is recovered into code -4. Nothing /// unwinds through the C frame. fn render_trampoline( w: &mut CodeWriter, @@ -245,36 +269,36 @@ fn render_trampoline( cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding, iface: &str, - domain: Option<&str>, ) { let tramp = trampoline(&cb.c_tag, &m.name); - let formals: Vec = m - .abi_params + let params = &m.abi.params; + let formals: Vec = params .iter() .map(|s| format!("{} {}", names::slot(&s.name), cgo_type(&s.ty, ctx.prefix))) .collect(); - let slot_named = |n: &str| { - m.abi_params - .iter() - .find(|p| p.name == n) - .map(|p| names::slot(&p.name)) - }; - let first = names::slot(&m.abi_params[0].name); - let out_err = names::slot(&m.abi_params[m.abi_params.len() - 1].name); - let direct_ret = m.abi_ret != CType::Void; + let first = names::slot(¶ms[0].name); + let out_err = names::slot(¶ms[params.len() - 1].name); + let direct_ret = m.abi.ret != CType::Void; let ret_sig = if direct_ret { - format!(" (cRet {})", cgo_type(&m.abi_ret, ctx.prefix)) + format!(" (cRet {})", cgo_type(&m.abi.ret, ctx.prefix)) } else { String::new() }; - let pass = RetPass::of(m.ret.as_ref()); - let throws = m.error_strategy() == ErrorStrategy::Throws; let args: Vec = m.params.iter().map(argument).collect(); let call = format!( "wvCallback[{iface}]({first}).{}({})", pascal(&m.name), args.join(", ") ); + let failed = match &m.error { + ErrorStrategy::Domain(d) => format!( + "wvCallbackFailed[{}]({out_err}, err)", + ctx.names.domain(d).iface + ), + ErrorStrategy::Untyped | ErrorStrategy::Trap => { + format!("wvCallbackError({out_err}, err)") + } + }; w.line(format!("//export {tramp}")); w.block( @@ -282,11 +306,7 @@ fn render_trampoline( "}", |w| { w.line(format!("defer wvRecoverCallback({out_err})")); - let failed = format!( - "wvCallbackFailed[{}]({out_err}, err)", - domain.unwrap_or("error") - ); - match (&m.ret, throws) { + match (&m.ret, m.error.throws()) { (None, false) => { w.line(call); return; @@ -308,34 +328,59 @@ fn render_trampoline( }); } } - let ty = m.ret.as_ref().expect("a returning method"); - match pass { - RetPass::Direct => { + let ty: &Ty = m.ret.as_ref().expect("a returning method"); + let s = |slot: &weaveffi_model::abi::AbiParam| names::slot(&slot.name); + match &m.ret_pass { + CallbackRetPass::Direct => { + w.line(format!("return {}(ret)", cgo_type(&m.abi.ret, ctx.prefix))); + } + CallbackRetPass::OptDirect { out_value } => { + w.line("cPresent, cValue := wvPresent(ret)"); + w.line(format!( + "*{} = {}(cValue)", + s(out_value), + cgo_pointee(&out_value.ty, ctx.prefix) + )); w.line(format!( - "return {}", - to_c_direct("ret", &m.abi_ret, ctx.prefix) + "return {}(cPresent)", + cgo_type(&m.abi.ret, ctx.prefix) )); } - RetPass::Object { .. } => { + CallbackRetPass::Object { .. } => { w.line("return ret.share()"); } - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - let out_ptr = slot_named("out_ptr").expect("a run return has out_ptr"); - let out_len = slot_named("out_len").expect("a run return has out_len"); - let hand = match pass { - RetPass::String => format!("wvHandOverString(ret, {out_ptr}, {out_len})"), - RetPass::Bytes => format!("wvHandOverBytes(ret, {out_ptr}, {out_len})"), - _ => format!( - "wvHandOverBytes(wvEncode(ret, {}), {out_ptr}, {out_len})", - write_fn(ty) - ), - }; - w.line(hand); - if direct_ret { - w.line("return"); - } + CallbackRetPass::Slice { + out_ptr, out_len, .. + } => { + w.line(format!( + "wvHandOverSlice(ret, {}, {})", + s(out_ptr), + s(out_len) + )); + } + CallbackRetPass::String { out_ptr, out_len } => { + w.line(format!( + "wvHandOverString(ret, {}, {})", + s(out_ptr), + s(out_len) + )); + } + CallbackRetPass::Bytes { out_ptr, out_len } => { + w.line(format!( + "wvHandOverBytes(ret, {}, {})", + s(out_ptr), + s(out_len) + )); + } + CallbackRetPass::Buffer { out_ptr, out_len } => { + w.line(format!( + "wvHandOverBytes(wvEncode(ret, {}), {}, {})", + write_fn(ty), + s(out_ptr), + s(out_len) + )); } - RetPass::Void => unreachable!("a returning method"), + CallbackRetPass::Void => unreachable!("a returning method"), } }, ); diff --git a/crates/weaveffi-cli/src/targets/go/calls.rs b/crates/weaveffi-cli/src/targets/go/calls.rs index 30d2b4a0..d5300b4d 100644 --- a/crates/weaveffi-cli/src/targets/go/calls.rs +++ b/crates/weaveffi-cli/src/targets/go/calls.rs @@ -1,23 +1,24 @@ //! Call rendering: sync, async, and iterator wrappers and the argument and //! result marshalling they share. //! -//! Marshalling follows the shared plan: each parameter's passing contract -//! comes from [`ParamBinding::arg_pass`] and each result's receiving -//! contract from [`RetPass::of`], so this module only spells those -//! contracts in Go. +//! Marshalling reads the model's stored contracts: each parameter's +//! [`ArgPass`], and the result's [`RetPass`] (sync), [`ResultPass`] (async +//! completion), or [`ItemPass`] (iterator element), so this module only +//! spells those contracts in Go. How a failure surfaces comes from the +//! callable's +//! [`ErrorStrategy`](weaveffi_model::plan::ErrorStrategy). use crate::codegen::CodeWriter; use weaveffi_model::abi::CType; use weaveffi_model::model::{AsyncBinding, CallShape, FnBinding, IteratorBinding, ParamBinding}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; -use weaveffi_model::ty::Ty; +use weaveffi_model::plan::{ArgPass, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{Prim, RetTy, Ty}; use crate::targets::go::codec::{read_fn, write_fn}; -use crate::targets::go::docs::{GoDoc, Kind}; -use crate::targets::go::entities::domain_mapper; +use crate::targets::go::docs::GoDoc; use crate::targets::go::names::{self, pascal}; use crate::targets::go::types::{ - cgo_type, from_c_direct, go_type, go_zero, strip_const, to_c_direct, + cgo_pointee, cgo_type, go_type, go_zero, param_type, prim_type, ret_type, strip_const, }; use crate::targets::go::Ctx; @@ -28,48 +29,65 @@ pub(crate) struct Receiver<'a> { pub(crate) ty: &'a str, } -// ── Errors ── - -/// How a wrapper reports a non-zero error slot: a throwing callable returns -/// `(T, error)` with the failure mapped onto its domain's code types; any -/// other callable panics with an `*Error`, since its failure is a bug. -struct ErrCtx { - throws: bool, - /// The domain's mapping helper (`wvKvError`), or `None` for the generic - /// `*Error` when no domain is in scope. - mapper: Option, +/// Render the Go wrapper of one callable: a package function (a free +/// function, constructor, or static) or, with `recv`, a method. +pub(crate) fn render_function( + w: &mut CodeWriter, + ctx: &Ctx, + f: &FnBinding, + go_name: &str, + recv: Option, +) { + match (&f.shape, f.iterator()) { + (CallShape::Async(ab), _) => render_async(w, ctx, f, ab, go_name, recv), + (CallShape::Sync, Some(it)) => render_iterator(w, ctx, f, it, go_name, recv), + (CallShape::Sync, None) => render_sync(w, ctx, f, go_name, recv), + } } -impl ErrCtx { - fn of(ctx: &Ctx, f: &FnBinding) -> Self { - Self { - throws: f.error_strategy() == ErrorStrategy::Throws, - mapper: ctx.domain.map(domain_mapper), - } - } +// ── Shared pieces ── - /// The Go expression converting the `wvFailure` value `failure` into an - /// `error`. - fn map(&self, failure: &str) -> String { - match &self.mapper { - Some(m) => format!("{m}({failure})"), - None => format!("{failure}.err()"), - } - } +/// How a value the native library hands over arrives, whatever the +/// position: the shape of its slots, independent of the slot names. +enum Received<'a> { + /// A scalar or C-style enum by value. + Direct, + /// A presence flag and a scalar. + OptDirect, + /// A typed array of the primitive (pointer and element count). + Slice(Prim), + /// A UTF-8 run. + String, + /// A byte run. + Bytes, + /// A value buffer. + Buffer, + /// An owned object reference of the interface. + Object(&'a str), +} - /// The return type (with its leading space) of a sync wrapper. - fn ret_sig(&self, ret: Option<&Ty>) -> String { - match (ret, self.throws) { - (Some(r), true) => format!(" ({}, error)", go_type(r)), - (Some(r), false) => format!(" {}", go_type(r)), - (None, true) => " error".into(), - (None, false) => String::new(), +/// The Go expression receiving one value of type `ty` the native library +/// hands over in the slots `a` (and `b`, the length or value slot), taking +/// ownership: a direct value converted, an optional scalar joined, a typed +/// array, string, or bytes copied and released, a buffer decoded and +/// released, an object adopted (a null `Interface?` adopts to nil). +fn receive(ty: &Ty, how: &Received, a: &str, b: &str) -> String { + match how { + Received::Direct => format!("{}({a})", go_type(ty)), + Received::OptDirect => { + let Ty::Optional(inner) = ty else { + unreachable!("an optional scalar has an optional type") + }; + format!("wvOptional(bool({a}), {}({b}))", go_type(inner)) } + Received::Slice(p) => format!("wvTakeSlice[{}]({a}, {b})", prim_type(*p)), + Received::String => format!("wvTakeString({a}, {b})"), + Received::Bytes => format!("wvTakeBytes({a}, {b})"), + Received::Buffer => format!("wvDecode({a}, {b}, {})", read_fn(ty)), + Received::Object(i) => format!("wvAdopt{}({a})", pascal(i)), } } -// ── Shared pieces ── - /// The wrapper's signature line. fn header(recv: Option, go_name: &str, params: &[String], ret_sig: &str) -> String { let params = params.join(", "); @@ -81,37 +99,48 @@ fn header(recv: Option, go_name: &str, params: &[String], ret_sig: &st /// The wrapper's Go parameter list. fn go_params(f: &FnBinding) -> Vec { - let is_async = f.is_async(); let mut params = Vec::new(); - if is_async { + if f.is_async() { params.push("ctx context.Context".to_string()); } params.extend( f.params .iter() - .map(|p| format!("{} {}", names::param(&p.name, f), go_type(&p.ty))), + .map(|p| format!("{} {}", names::param(&p.name, f), param_type(&p.ty))), ); params } -/// The receiver borrow every method opens with: `s.native()` panics when -/// the wrapper was already closed, and the deferred release keeps the -/// object alive (even across a concurrent `Close`) until the call returns. -fn emit_receiver(w: &mut CodeWriter, args: &mut Vec) { - w.line("cSelf := s.native()"); - w.line("defer s.ref.release()"); - args.push("cSelf".into()); +/// The opening of every wrapper body: a package function runs the +/// load-time checks (see `Check`), and a method borrows its receiver. +/// `s.native()` panics when the wrapper was already closed, and the +/// deferred release keeps the object alive (even across a concurrent +/// `Close`) until the call returns. +fn emit_prologue(w: &mut CodeWriter, recv: Option, args: &mut Vec) { + if recv.is_some() { + w.line("cSelf := s.native()"); + w.line("defer s.release()"); + args.push("cSelf".into()); + } else { + w.line("wvLoaded()"); + } +} + +/// The C type's cgo spelling with `const` dropped. +fn cgo(ct: &CType, ctx: &Ctx) -> String { + cgo_type(&strip_const(ct), ctx.prefix) } /// Emit the staging statements for one parameter and push its C argument /// expressions, per its [`ArgPass`]. /// -/// Strings and bytes pass a borrowed view of the Go memory (cgo pins it -/// for the call); a buffered value is encoded first. An object parameter -/// borrows the wrapper's pointer for the call, like a receiver, and a nil -/// `Interface?` passes null. A callback implementation is kept in a handle -/// table, passed as the context, beside the interface's static vtable; a -/// nil `Cb?` passes a null vtable. +/// Strings, bytes, and typed arrays pass a borrowed view of the Go memory +/// (cgo keeps it in place for the call); a buffered value is encoded first, +/// and an optional scalar splits into its flag and value. An object +/// parameter borrows the wrapper's pointer for the call, like a receiver, +/// and a nil `Interface?` passes null. A callback implementation is kept in +/// a handle table, passed as the context, beside the interface's static +/// vtable; a nil `Cb?` (or a typed nil) passes no callback. fn emit_param( w: &mut CodeWriter, ctx: &Ctx, @@ -121,11 +150,19 @@ fn emit_param( ) { let name = names::param(&p.name, f); let c = format!("c{}", pascal(&p.name)); - match p.arg_pass() { - ArgPass::Buffer { .. } => { + match &p.pass { + ArgPass::Direct { slot } => args.push(format!("{}({name})", cgo(&slot.ty, ctx))), + ArgPass::OptDirect { has, value, .. } => { + w.line(format!("{c}Has, {c} := wvPresent({name})")); + args.extend([ + format!("{}({c}Has)", cgo(&has.ty, ctx)), + format!("{}({c})", cgo(&value.ty, ctx)), + ]); + } + ArgPass::Slice { ptr, .. } => { w.line(format!( - "{c}Ptr, {c}Len := wvBytes(wvEncode({name}, {}))", - write_fn(&p.ty) + "{c}Ptr, {c}Len := wvSliceIn[{}]({name})", + cgo_pointee(&ptr.ty, ctx.prefix) )); args.extend([format!("{c}Ptr"), format!("{c}Len")]); } @@ -137,268 +174,291 @@ fn emit_param( w.line(format!("{c}Ptr, {c}Len := wvBytes({name})")); args.extend([format!("{c}Ptr"), format!("{c}Len")]); } - ArgPass::Object { slot, nullable } => { - if nullable { - w.line(format!( - "var {c} {}", - cgo_type(&strip_const(&slot.ty), ctx.prefix) - )); + ArgPass::Buffer { .. } => { + let ty = p.ty.value().expect("a buffered parameter is a value"); + w.line(format!( + "{c}Ptr, {c}Len := wvBytes(wvEncode({name}, {}))", + write_fn(ty) + )); + args.extend([format!("{c}Ptr"), format!("{c}Len")]); + } + ArgPass::Object { slot, nullable, .. } => { + if *nullable { + w.line(format!("var {c} {}", cgo(&slot.ty, ctx))); w.block(format!("if {name} != nil {{"), "}", |w| { w.line(format!("{c} = {name}.native()")); - w.line(format!("defer {name}.ref.release()")); + w.line(format!("defer {name}.release()")); }); } else { w.line(format!("{c} := {name}.native()")); - w.line(format!("defer {name}.ref.release()")); + w.line(format!("defer {name}.release()")); } args.push(c); } ArgPass::Callback { - vtable, nullable, .. + vtable, + nullable, + interface, + .. } => { let CType::Ptr { pointee, .. } = &vtable.ty else { unreachable!("a callback vtable slot is a pointer") }; let accessor = format!("C.{}()", vtable_accessor(&pointee.render_c(ctx.prefix))); - if nullable { - w.line(format!("var {c}Ctx unsafe.Pointer")); + if *nullable { w.line(format!( - "var {c}Vtable {}", - cgo_type(&strip_const(&vtable.ty), ctx.prefix) + "{c}Ctx, {c}Vtable := wvOptionalCallback({name}, {accessor})" )); - w.block(format!("if {name} != nil {{"), "}", |w| { - w.line(format!( - "{c}Ctx, {c}Vtable = wvNewCallback({name}), {accessor}" - )); - }); args.extend([format!("{c}Ctx"), format!("{c}Vtable")]); } else { - args.extend([format!("wvNewCallback({name})"), accessor]); + args.extend([ + format!("wvNewCallback({name}, \"{}\")", pascal(interface)), + accessor, + ]); } } - ArgPass::Direct { slot } => args.push(to_c_direct(&name, &slot.ty, ctx.prefix)), - } -} - -/// The Go expression receiving one value the native library hands over, -/// per its [`RetPass`]: a direct value converted, a string or bytes copied -/// and released, a buffer decoded and released, an object adopted (a null -/// `Interface?` adopts to nil). `len` names the length slot of a run. -fn receive(ty: &Ty, pass: &RetPass, ptr: &str, len: &str) -> String { - match pass { - RetPass::Void => unreachable!("a present value is never void"), - RetPass::Direct => from_c_direct(ptr, ty), - RetPass::String => format!("wvTakeString({ptr}, {len})"), - RetPass::Bytes => format!("wvTakeBytes({ptr}, {len})"), - RetPass::Buffer => format!("wvDecode({ptr}, {len}, {})", read_fn(ty)), - RetPass::Object { .. } => { - let n = ty - .interface_name() - .expect("an object result names an interface"); - format!("wvAdopt{}({ptr})", pascal(n)) - } } } -/// `true` when a value received per `pass` arrives as a `(ptr, len)` run. -fn has_len(pass: &RetPass) -> bool { - matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer) -} - /// The doc comment of a wrapper: its IDL doc and documented parameters, /// then `extra` paragraphs, then `Deprecated:`. -fn wrapper_doc(f: &FnBinding, go_name: &str, extra: &[&str]) -> GoDoc { - let kind = Kind::callable(f.ret.is_some()); - let mut doc = - GoDoc::new(go_name, f.doc.as_deref(), kind, None).params(&f.params, |n| names::param(n, f)); +fn wrapper_doc(ctx: &Ctx, f: &FnBinding, go_name: &str, extra: &[String]) -> GoDoc { + let (doc, deprecated) = ctx.docs.of(&f.doc, &f.deprecated); + let params = f.params.iter().filter_map(|p| { + let (doc, _) = ctx.docs.of(&p.doc, &None); + doc.map(|d| (names::param(&p.name, f), d)) + }); + let mut doc = GoDoc::decl(go_name, doc).params(params); for p in extra { doc = doc.para(p); } - doc.deprecated(f.deprecated.as_deref()) + doc.deprecated(deprecated) +} + +/// The value type a callable returns, if any (the element type for an +/// iterator). +fn ret_value(f: &FnBinding) -> Option<&Ty> { + f.ret.as_ref().map(RetTy::elem) } // ── Sync ── -/// Render a sync or iterator callable: the Go wrapper marshalling the -/// parameters, calling the C symbol, checking the error slot (a typed -/// `(T, error)` when throwing, a panic otherwise), and receiving the -/// result. With `recv` set, it's a method passing the wrapper's pointer -/// first. -pub(crate) fn render_sync( +/// Render a sync callable: the Go wrapper marshalling the parameters, +/// calling the C symbol, checking the error slot, and receiving the +/// result. A callable that declares errors returns `(T, error)` (its +/// domain's code types, or an `*Error` when it fails with any error); any +/// other panics with an `*Error`. +fn render_sync( w: &mut CodeWriter, ctx: &Ctx, f: &FnBinding, go_name: &str, recv: Option, ) { - if let CallShape::Iterator(ib) = &f.shape { - render_iterator(w, ctx, f, ib, go_name, recv); - return; - } - let err = ErrCtx::of(ctx, f); - let pass = RetPass::of(f.ret.as_ref()); - wrapper_doc(f, go_name, &[]).emit(w); - w.block( - header(recv, go_name, &go_params(f), &err.ret_sig(f.ret.as_ref())), - "}", - |w| { - let mut args = Vec::new(); - if recv.is_some() { - emit_receiver(w, &mut args); - } - for p in &f.params { - emit_param(w, ctx, &mut args, p, f); + let throws = f.error.throws(); + let ret = ret_value(f); + let ret_sig = match (ret, throws) { + (Some(r), true) => format!(" ({}, error)", go_type(r)), + (Some(r), false) => format!(" {}", go_type(r)), + (None, true) => " error".into(), + (None, false) => String::new(), + }; + wrapper_doc(ctx, f, go_name, &[]).emit(w); + w.block(header(recv, go_name, &go_params(f), &ret_sig), "}", |w| { + let mut args = Vec::new(); + emit_prologue(w, recv, &mut args); + for p in &f.params { + emit_param(w, ctx, &mut args, p, f); + } + let how = match &f.ret_pass { + RetPass::Void => None, + RetPass::Direct => Some((Received::Direct, "")), + RetPass::OptDirect { out_value } => { + w.line(format!( + "var cOut {}", + cgo_pointee(&out_value.ty, ctx.prefix) + )); + args.push("&cOut".into()); + Some((Received::OptDirect, "cOut")) } - if has_len(&pass) { - w.line("var cRetLen C.size_t"); - args.push("&cRetLen".into()); + RetPass::Slice { elem, .. } => Some((Received::Slice(*elem), "cRetLen")), + RetPass::String { .. } => Some((Received::String, "cRetLen")), + RetPass::Bytes { .. } => Some((Received::Bytes, "cRetLen")), + RetPass::Buffer { .. } => Some((Received::Buffer, "cRetLen")), + RetPass::Object { interface, .. } => Some((Received::Object(interface), "")), + RetPass::Iterator(_) => unreachable!("iterators render separately"), + }; + if let Some((_, "cRetLen")) = &how { + w.line("var cRetLen C.size_t"); + args.push("&cRetLen".into()); + } + w.line(format!("var cErr C.{}_error", ctx.prefix)); + args.push("&cErr".into()); + let call = format!("C.{}({})", f.abi.symbol, args.join(", ")); + match &how { + Some(_) => w.line(format!("cRet := {call}")), + None => w.line(call), + }; + if throws { + let mapped = ctx.map_failure(&f.error, "wvTakeError(&cErr)"); + w.block("if cErr.code != 0 {", "}", |w| { + match ret { + Some(ty) => w.line(format!("return {}, {mapped}", go_zero(ty))), + None => w.line(format!("return {mapped}")), + }; + }); + } else { + w.line("wvTrap(&cErr)"); + } + let ok = if throws { ", nil" } else { "" }; + match (&how, ret) { + (Some((how, b)), Some(ty)) => { + w.line(format!("return {}{ok}", receive(ty, how, "cRet", b))); } - w.line(format!("var cErr C.{}_error", ctx.prefix)); - args.push("&cErr".into()); - let call = format!("C.{}({})", f.c_base, args.join(", ")); - match &f.ret { - Some(_) => w.line(format!("cRet := {call}")), - None => w.line(call), - }; - let ok = if err.throws { ", nil" } else { "" }; - if err.throws { - let mapped = err.map("wvTakeError(&cErr)"); - w.block("if cErr.code != 0 {", "}", |w| { - match &f.ret { - Some(ty) => w.line(format!("return {}, {mapped}", go_zero(ty))), - None => w.line(format!("return {mapped}")), - }; - }); - } else { - w.line("wvTrap(&cErr)"); + _ if throws => { + w.line("return nil"); } - match &f.ret { - Some(ty) => { - w.line(format!( - "return {}{ok}", - receive(ty, &pass, "cRet", "cRetLen") - )); - } - None if err.throws => { - w.line("return nil"); - } - None => {} - } - }, - ); + _ => {} + } + }); w.blank(); } -/// Render an `iter` callable as Go's lazy sequence idiom: `iter.Seq[T]`, -/// or `iter.Seq2[T, error]` when it throws, where a launch or `next` -/// failure is yielded as a final `(zero, err)` pair. The native iterator is -/// launched on each range, `next` runs once per element, and `destroy` runs -/// exactly once when the range ends, early or not. +// ── Iterators ── + +/// Render an `iter` callable as Go's lazy sequence idiom over the +/// runtime's `wvSeq` (`iter.Seq[T]`) or, when it declares errors, `wvSeq2` +/// (`iter.Seq2[T, error]`, a failure yielded as a final `(zero, err)` +/// pair). The wrapper supplies the launch, whose cursor pulls one element +/// per step through `_next` and destroys the iterator. fn render_iterator( w: &mut CodeWriter, ctx: &Ctx, f: &FnBinding, - ib: &IteratorBinding, + it: &IteratorBinding, go_name: &str, recv: Option, ) { - let err = ErrCtx::of(ctx, f); - let proto = ib.protocol(f); - let elem = &ib.elem; + let throws = f.error.throws(); + let elem = &it.elem; let elem_go = go_type(elem); - let CType::Ptr { pointee, .. } = &ib.next.params[1].ty else { - unreachable!("an iterator's item slot is a pointer") - }; - let item_ty = cgo_type(&strip_const(pointee), ctx.prefix); - let with_len = has_len(&proto.elem); - let (seq, yield_ty) = if err.throws { - ( - format!("iter.Seq2[{elem_go}, error]"), - format!("func({elem_go}, error) bool"), - ) - } else { - ( - format!("iter.Seq[{elem_go}]"), - format!("func({elem_go}) bool"), - ) - }; - - let notes = if err.throws { - "The sequence launches the native iterator each time it's ranged over and \ - pulls one element per step, yielding a failure as a final (zero value, \ - error) pair; the iterator is released when the range ends, early or not." - } else { - "The sequence launches the native iterator each time it's ranged over and \ - pulls one element per step; the iterator is released when the range ends, \ - early or not." - }; - wrapper_doc(f, go_name, &[notes]).emit(w); - - // Surfaces a non-zero error slot: a throwing sequence yields the mapped - // error and stops; any other traps. - let check = |w: &mut CodeWriter, slot: &str| { - if err.throws { - let mapped = err.map(&format!("wvTakeError(&{slot})")); - w.block(format!("if {slot}.code != 0 {{"), "}", |w| { - w.line(format!("yield({}, {mapped})", go_zero(elem))); - w.line("return"); - }); - } else { - w.line(format!("wvTrap(&{slot})")); - } - }; + let seq = ret_type(f.ret.as_ref().expect("an iterator returns"), throws); + let mut note = format!( + "{go_name} returns a lazy sequence: each range over it launches the native \ + iterator, pulls one element per step, and releases the iterator when the \ + loop ends, early or not." + ); + if throws { + note.push_str(" A failure is yielded as a final (zero value, error) pair."); + } + wrapper_doc(ctx, f, go_name, &[note]).emit(w); + let p = ctx.prefix; w.block( header(recv, go_name, &go_params(f), &format!(" {seq}")), "}", |w| { - w.block(format!("return func(yield {yield_ty}) {{"), "}", |w| { + if recv.is_none() { + w.line("wvLoaded()"); + } + let (seq_fn, tail) = if throws { + ("wvSeq2", format!(", {})", ctx.mapper(&f.error))) + } else { + ("wvSeq", ")".to_string()) + }; + w.line(format!( + "return {seq_fn}(func(cErr *C.{p}_error) wvCursor[{elem_go}] {{" + )); + w.scope(|w| { let mut args = Vec::new(); if recv.is_some() { - emit_receiver(w, &mut args); + emit_prologue(w, recv, &mut args); } - for p in &f.params { - emit_param(w, ctx, &mut args, p, f); + for param in &f.params { + emit_param(w, ctx, &mut args, param, f); } - w.line(format!("var cErr C.{}_error", ctx.prefix)); - args.push("&cErr".into()); - w.line(format!( - "cIter := C.{}({})", - ib.launch.symbol, - args.join(", ") - )); - check(w, "cErr"); - w.line(format!("defer C.{}(cIter)", ib.destroy_symbol)); - w.block("for {", "}", |w| { - w.line(format!("var cItem {item_ty}")); - let next_args = if with_len { - w.line("var cItemLen C.size_t"); - "cIter, &cItem, &cItemLen, &cIterErr" - } else { - "cIter, &cItem, &cIterErr" - }; - w.line(format!("var cIterErr C.{}_error", ctx.prefix)); - w.line(format!("more := C.{}({next_args}) != 0", ib.next.symbol)); - check(w, "cIterErr"); - w.block("if !more {", "}", |w| { - w.line("return"); - }); - let item = receive(elem, &proto.elem, "cItem", "cItemLen"); - let call = if err.throws { - format!("if !yield({item}, nil) {{") - } else { - format!("if !yield({item}) {{") - }; - w.block(call, "}", |w| { - w.line("return"); - }); + args.push("cErr".into()); + w.line(format!("it := C.{}({})", f.abi.symbol, args.join(", "))); + w.block(format!("return wvCursor[{elem_go}]{{"), "}", |w| { + w.block( + format!("next: func(cErr *C.{p}_error) ({elem_go}, bool) {{"), + "},", + |w| emit_next(w, ctx, it, elem), + ); + w.line(format!( + "destroy: func() {{ C.{}(it) }},", + it.destroy_symbol + )); }); }); + w.line(format!("}}{tail}")); }, ); w.blank(); } +/// The body of an iterator cursor's `next`: declare the item slots, call +/// `_next`, and receive the element. +fn emit_next(w: &mut CodeWriter, ctx: &Ctx, it: &IteratorBinding, elem: &Ty) { + let pointee = |slot: &weaveffi_model::abi::AbiParam| cgo_pointee(&slot.ty, ctx.prefix); + let (how, slots): (Received, Vec<(&str, String)>) = match &it.item { + ItemPass::Direct { out_item } => (Received::Direct, vec![("cItem", pointee(out_item))]), + ItemPass::OptDirect { out_has, out_item } => ( + Received::OptDirect, + vec![("cHas", pointee(out_has)), ("cItem", pointee(out_item))], + ), + ItemPass::Slice { + out_item, + out_len, + elem, + } => ( + Received::Slice(*elem), + vec![("cItem", pointee(out_item)), ("cItemLen", pointee(out_len))], + ), + ItemPass::String { out_item, out_len } => ( + Received::String, + vec![("cItem", pointee(out_item)), ("cItemLen", pointee(out_len))], + ), + ItemPass::Bytes { out_item, out_len } => ( + Received::Bytes, + vec![("cItem", pointee(out_item)), ("cItemLen", pointee(out_len))], + ), + ItemPass::Buffer { out_item, out_len } => ( + Received::Buffer, + vec![("cItem", pointee(out_item)), ("cItemLen", pointee(out_len))], + ), + ItemPass::Object { + out_item, + interface, + .. + } => ( + Received::Object(interface), + vec![("cItem", pointee(out_item))], + ), + }; + let mut args = vec!["it".to_string()]; + for (name, ty) in &slots { + w.line(format!("var {name} {ty}")); + args.push(format!("&{name}")); + } + args.push("cErr".into()); + w.block( + format!("if C.{}({}) == 0 {{", it.next.symbol, args.join(", ")), + "}", + |w| { + w.line(format!("return {}, false", go_zero(elem))); + }, + ); + let (a, b) = match slots.as_slice() { + [(a, _)] => (*a, ""), + [(a, _), (b, _)] => (*a, *b), + _ => unreachable!("an item has one or two slots"), + }; + // An optional scalar's slots are (flag, value); `receive` wants them in + // that order too. + w.line(format!("return {}, true", receive(elem, &how, a, b))); +} + // ── Async ── /// The C name of the exported Go trampoline behind an async completion @@ -418,13 +478,13 @@ pub(crate) fn vtable_accessor(vtable_tag: &str) -> String { /// converts the result and delivers it on the call's channel, and a /// blocking wrapper taking a leading `context.Context`. /// -/// Every async wrapper returns an `error`. A cancellable one creates a -/// native cancel token, cancels it when `ctx` is done, and returns -/// `ctx.Err()` for the cancelled completion; any other returns `ctx.Err()` -/// as soon as `ctx` is done and abandons the result. A failure maps onto -/// the domain when the function throws and panics with an `*Error` -/// otherwise. -pub(crate) fn render_async( +/// Every async wrapper returns an `error` and never panics on a failure: +/// a function that declares a domain returns its code types, any other an +/// `*Error`. A cancellable one creates a native cancel token, cancels it +/// when `ctx` is done, and returns `ctx.Err()` for the cancelled +/// completion; any other returns `ctx.Err()` as soon as `ctx` is done and +/// abandons the result (see the runtime's `wvAwait`). +fn render_async( w: &mut CodeWriter, ctx: &Ctx, f: &FnBinding, @@ -432,13 +492,9 @@ pub(crate) fn render_async( go_name: &str, recv: Option, ) { - let err = ErrCtx::of(ctx, f); - let proto = ab.protocol(f); let tramp = completion_trampoline(&ab.callback_type); - let val_ty = f - .ret - .as_ref() - .map_or_else(|| "struct{}".to_string(), go_type); + let ret = ret_value(f); + let val_ty = ret.map_or_else(|| "struct{}".to_string(), go_type); let formals: Vec = ab .callback_params @@ -447,22 +503,39 @@ pub(crate) fn render_async( .collect(); let context = names::slot(&ab.callback_params[0].name); let err_slot = names::slot(&ab.callback_params[1].name); - let results: Vec = ab.callback_params[2..] - .iter() - .map(|s| names::slot(&s.name)) - .collect(); + let slot = |s: &weaveffi_model::abi::AbiParam| names::slot(&s.name); + let value = match (&ab.result, ret) { + (ResultPass::Void, _) | (_, None) => "struct{}{}".to_string(), + (ResultPass::Direct { result }, Some(ty)) => { + receive(ty, &Received::Direct, &slot(result), "") + } + (ResultPass::OptDirect { has, value }, Some(ty)) => { + receive(ty, &Received::OptDirect, &slot(has), &slot(value)) + } + (ResultPass::Slice { ptr, len, elem }, Some(ty)) => { + receive(ty, &Received::Slice(*elem), &slot(ptr), &slot(len)) + } + (ResultPass::String { ptr, len }, Some(ty)) => { + receive(ty, &Received::String, &slot(ptr), &slot(len)) + } + (ResultPass::Bytes { ptr, len }, Some(ty)) => { + receive(ty, &Received::Bytes, &slot(ptr), &slot(len)) + } + (ResultPass::Buffer { ptr, len }, Some(ty)) => { + receive(ty, &Received::Buffer, &slot(ptr), &slot(len)) + } + ( + ResultPass::Object { + result, interface, .. + }, + Some(ty), + ) => receive(ty, &Received::Object(interface), &slot(result), ""), + }; w.line(format!("//export {tramp}")); w.block( format!("func {tramp}({}) {{", formals.join(", ")), "}", |w| { - let value = match &f.ret { - None => "struct{}{}".to_string(), - Some(ty) => { - let len = results.get(1).map_or("", String::as_str); - receive(ty, &proto.result, &results[0], len) - } - }; w.block( format!("wvComplete({context}, {err_slot}, func() {val_ty} {{"), "})", @@ -474,23 +547,20 @@ pub(crate) fn render_async( ); w.blank(); - let notes = if f.cancellable { - "It blocks until the call completes. Cancelling ctx cancels the native \ - call, which then returns ctx.Err()." + let note = if f.cancellable() { + format!( + "{go_name} blocks until the call completes. Cancelling ctx cancels the \ + native call, which then returns ctx.Err()." + ) } else { - "It blocks until the call completes or ctx is done; in the latter case it \ - returns ctx.Err() at once and the native result is discarded." + format!( + "{go_name} blocks until the call completes or ctx is done; in the latter \ + case it returns ctx.Err() at once and the native result is discarded." + ) }; - let kind = Kind::callable(f.ret.is_some()); - let mut doc = - GoDoc::new(go_name, f.doc.as_deref(), kind, None).params(&f.params, |n| names::param(n, f)); - if doc.is_empty() { - doc = doc.para("Calls the native async function."); - } - doc = doc.para(notes); - doc.deprecated(f.deprecated.as_deref()).emit(w); + wrapper_doc(ctx, f, go_name, &[note]).emit(w); - let (ret_sig, zero) = match &f.ret { + let (ret_sig, zero) = match ret { Some(r) => ( format!(" ({}, error)", go_type(r)), format!("{}, ", go_zero(r)), @@ -498,17 +568,20 @@ pub(crate) fn render_async( None => (" error".to_string(), String::new()), }; w.block(header(recv, go_name, &go_params(f), &ret_sig), "}", |w| { + if recv.is_none() { + w.line("wvLoaded()"); + } w.block("if err := ctx.Err(); err != nil {", "}", |w| { w.line(format!("return {zero}err")); }); let mut args = Vec::new(); if recv.is_some() { - emit_receiver(w, &mut args); + emit_prologue(w, recv, &mut args); } for p in &f.params { emit_param(w, ctx, &mut args, p, f); } - let token = if proto.cancellable { + let token = if ab.cancellable() { w.line(format!("token := C.{}_cancel_token_create()", ctx.prefix)); w.line(format!( "defer C.{}_cancel_token_destroy(token)", @@ -522,22 +595,14 @@ pub(crate) fn render_async( w.line(format!("call := wvNewAsyncCall[{val_ty}]()")); args.push(format!("C.{}(unsafe.Pointer(C.{tramp}))", ab.callback_type)); args.push("call.context()".into()); - w.line(format!("C.{}({})", ab.launch.symbol, args.join(", "))); - let res = if f.ret.is_some() { "res" } else { "_" }; - w.line(format!("{res}, fail, err := call.wait(ctx, {token})")); - w.block("if err != nil {", "}", |w| { - w.line(format!("return {zero}err")); - }); - w.block("if fail != nil {", "}", |w| { - if err.throws { - w.line(format!("return {zero}{}", err.map("*fail"))); - } else { - w.line("panic(fail.err())"); + w.line(format!("C.{}({})", f.abi.symbol, args.join(", "))); + let await_call = format!("wvAwait(ctx, call, {token}, {})", ctx.mapper(&f.error)); + match ret { + Some(_) => w.line(format!("return {await_call}")), + None => { + w.line(format!("_, err := {await_call}")); + w.line("return err") } - }); - match &f.ret { - Some(_) => w.line("return res, nil"), - None => w.line("return nil"), }; }); w.blank(); diff --git a/crates/weaveffi-cli/src/targets/go/codec.rs b/crates/weaveffi-cli/src/targets/go/codec.rs index 715b2b1e..180573db 100644 --- a/crates/weaveffi-cli/src/targets/go/codec.rs +++ b/crates/weaveffi-cli/src/targets/go/codec.rs @@ -1,19 +1,21 @@ -//! Value-buffer codecs: one `wvWrite{T}`/`wvRead{T}` pair per type that -//! crosses inside a buffer, against the `wvWriter`/`wvReader` pair and the -//! generic list, map, and optional helpers in `runtime/codec.go`. +//! Value-buffer codecs: one `wvWrite*`/`wvRead*` pair per type that crosses +//! inside a buffer, against the `wvWriter`/`wvReader` pair and the generic +//! list, map, and optional helpers in `runtime/codec.go`. //! -//! Every composite type the API uses (`[string]`, `{string:Store}`, -//! `Entry?`) gets exactly one named pair, built from the pairs of its parts, -//! so a call site, a record field, and a callback argument of the same type -//! share one encoder and one decoder. Primitives use the writer and reader -//! methods directly (`(*wvWriter).writeString`); records, rich enums, -//! C-style enums, and interfaces get their pairs from the entity renderers. - -use std::collections::{BTreeMap, BTreeSet}; - -use crate::codegen::CodeWriter; +//! Records, rich enums, C-style enums, and interfaces get their pairs from +//! the entity renderers, named after the Go type (`wvWriteItem`). Every +//! composite the API uses ([`codecs::composites`]) gets one pair named +//! after its shared stem (`wvWrite_list_string`, `wvRead_opt_Item`), so a +//! call site, a record field, and a callback argument of the same type +//! share one encoder and one decoder, and the stem matches every other +//! target's. Primitives use the writer and reader methods directly +//! (`(*wvWriter).writeString`). + +use std::collections::BTreeSet; + +use crate::codegen::{codecs, CodeWriter}; use weaveffi_model::model::Model; -use weaveffi_model::ty::{Family, Ty, WireType}; +use weaveffi_model::ty::Ty; use crate::targets::go::names::pascal; use crate::targets::go::types::{go_type, optional_derefs}; @@ -27,21 +29,12 @@ pub(crate) fn func(w: &mut CodeWriter, signature: &str, body: &str) { w.blank(); } -/// The stem naming a type's codec pair: `String`, `Entry`, `ListString`, -/// `MapStringStore`, `OptEntry`. Prefix notation with fixed arities, so -/// distinct types never share a stem. -fn stem(ty: &Ty) -> String { +/// The name fragment of a type's codec pair: a user type's Go name, or a +/// composite's shared stem after an underscore (`Item`, `_list_string`). +fn suffix(ty: &Ty) -> String { match ty { - Ty::Prim(p) => p.pascal().to_string(), - Ty::Optional(inner) => format!("Opt{}", stem(inner)), - Ty::List(inner) => format!("List{}", stem(inner)), - Ty::Map(k, v) => format!("Map{}{}", stem(k), stem(v)), - Ty::Iterator(inner) => format!("Iter{}", stem(inner)), - Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Enum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(n) => pascal(n), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => pascal(n), + _ => format!("_{}", codecs::stem(ty)), } } @@ -50,7 +43,7 @@ fn stem(ty: &Ty) -> String { pub(crate) fn write_fn(ty: &Ty) -> String { match ty { Ty::Prim(p) => format!("(*wvWriter).write{}", p.pascal()), - _ => format!("wvWrite{}", stem(ty)), + _ => format!("wvWrite{}", suffix(ty)), } } @@ -59,7 +52,7 @@ pub(crate) fn write_fn(ty: &Ty) -> String { pub(crate) fn read_fn(ty: &Ty) -> String { match ty { Ty::Prim(p) => format!("(*wvReader).read{}", p.pascal()), - _ => format!("wvRead{}", stem(ty)), + _ => format!("wvRead{}", suffix(ty)), } } @@ -67,7 +60,7 @@ pub(crate) fn read_fn(ty: &Ty) -> String { pub(crate) fn write_stmt(w: &str, expr: &str, ty: &Ty) -> String { match ty { Ty::Prim(p) => format!("{w}.write{}({expr})", p.pascal()), - _ => format!("wvWrite{}({w}, {expr})", stem(ty)), + _ => format!("{}({w}, {expr})", write_fn(ty)), } } @@ -75,96 +68,88 @@ pub(crate) fn write_stmt(w: &str, expr: &str, ty: &Ty) -> String { pub(crate) fn read_expr(r: &str, ty: &Ty) -> String { match ty { Ty::Prim(p) => format!("{r}.read{}()", p.pascal()), - _ => format!("wvRead{}({r})", stem(ty)), + _ => format!("{}({r})", read_fn(ty)), } } -/// Every type the API carries inside value buffers, by kind of codec it -/// needs. -#[derive(Default)] +/// The types the API carries inside value buffers that need codec pairs +/// beyond records and rich enums (which always get theirs). pub(crate) struct BufferTypes { - /// Optionals, lists, and maps, by stem. - composites: BTreeMap, + /// Optionals, lists, and maps, innermost first. + composites: Vec, /// C-style enums that appear inside a buffer. - pub(crate) enums: BTreeSet, + enums: BTreeSet, /// Interfaces that appear inside a buffer (as object tokens). - pub(crate) interfaces: BTreeSet, + interfaces: BTreeSet, } impl BufferTypes { - /// Walk every position of `model` where a value crosses in a buffer. + /// Every buffered shape of `model`: the shared composites, and the + /// C-style enums and interfaces inside them or inside a record, + /// variant, or error payload field. pub(crate) fn of(model: &Model) -> Self { - let mut t = Self::default(); - for m in &model.modules { - for s in &m.structs { - for f in &s.fields { - t.visit(&f.ty); - } - } - for e in &m.enums { - for v in &e.variants { - for f in &v.fields { - t.visit(&f.ty); - } - } - } - if let Some(e) = &m.errors { - for c in &e.codes { - for f in &c.fields { - t.visit(&f.ty); - } - } - } - let callables = m.callables().map(|f| (&f.params, f.ret.as_ref())); - let methods = m - .callback_interfaces + let composites = codecs::composites(model); + let mut t = Self { + composites: Vec::new(), + enums: BTreeSet::new(), + interfaces: BTreeSet::new(), + }; + let fields = model.modules.iter().flat_map(|m| { + let structs = m.structs.iter().flat_map(|s| &s.fields); + let variants = m + .enums .iter() - .flat_map(|cb| &cb.methods) - .map(|f| (&f.params, f.ret.as_ref())); - for (params, ret) in callables.chain(methods) { - for p in params { - t.visit_top(&p.ty); - } - if let Some(ret) = ret { - t.visit_top(ret.iterator_elem().unwrap_or(ret)); - } - } + .flat_map(|e| &e.variants) + .flat_map(|v| &v.fields); + let payloads = m + .errors + .iter() + .flat_map(|e| &e.codes) + .flat_map(|c| &c.fields); + structs.chain(variants).chain(payloads).map(|f| &f.ty) + }); + for ty in fields.chain(&composites) { + t.leaves(ty); } + t.composites = composites; t } - /// A parameter, return, or element: only a buffer-family value crosses - /// in a buffer. - fn visit_top(&mut self, ty: &Ty) { - if ty.family() == Family::Buffer { - self.visit(ty); - } - } - - fn visit(&mut self, ty: &Ty) { - match ty.wire() { - WireType::Prim(_) | WireType::User(_) => {} - WireType::Enum(n) => { - self.enums.insert(n.to_string()); - } - WireType::Object(n) => { - self.interfaces.insert(n.to_string()); + fn leaves(&mut self, ty: &Ty) { + match ty { + Ty::Enum(n) => { + self.enums.insert(n.clone()); } - WireType::Optional(inner) | WireType::List(inner) => { - self.composites.insert(stem(ty), ty.clone()); - self.visit(inner); + Ty::Interface(n) => { + self.interfaces.insert(n.clone()); } - WireType::Map(k, v) => { - self.composites.insert(stem(ty), ty.clone()); - self.visit(k); - self.visit(v); + Ty::Optional(inner) | Ty::List(inner) => self.leaves(inner), + Ty::Map(k, v) => { + self.leaves(k); + self.leaves(v); } + Ty::Prim(_) | Ty::Record(_) | Ty::RichEnum(_) => {} } } - /// Render the composite pairs, sorted by stem. + /// `true` when the C-style enum `name` crosses inside a buffer. + pub(crate) fn has_enum(&self, name: &str) -> bool { + self.enums.contains(name) + } + + /// `true` when the interface `name` crosses inside a buffer. + pub(crate) fn has_interface(&self, name: &str) -> bool { + self.interfaces.contains(name) + } + + /// `true` when any interface crosses inside a buffer. + pub(crate) fn has_interfaces(&self) -> bool { + !self.interfaces.is_empty() + } + + /// Render the composite pairs, innermost first. pub(crate) fn render_composites(&self, w: &mut CodeWriter) { - for (stem, ty) in &self.composites { + for ty in &self.composites { let go = go_type(ty); let (write, read) = match ty { Ty::List(inner) => ( @@ -187,12 +172,12 @@ impl BufferTypes { }; func( w, - &format!("func wvWrite{stem}(w *wvWriter, v {go})"), + &format!("func {}(w *wvWriter, v {go})", write_fn(ty)), &write, ); func( w, - &format!("func wvRead{stem}(r *wvReader) {go}"), + &format!("func {}(r *wvReader) {go}", read_fn(ty)), &format!("return {read}"), ); } @@ -205,24 +190,22 @@ mod tests { use weaveffi_model::ty::Prim; #[test] - fn stems_are_prefix_notation() { + fn composites_are_named_by_the_shared_stem() { let s = Ty::Prim(Prim::String); - let store = Ty::Interface("Store".into()); - assert_eq!(stem(&Ty::List(Box::new(s.clone()))), "ListString"); + let item = Ty::Record("Item".into()); assert_eq!( - stem(&Ty::Map(Box::new(s.clone()), Box::new(store))), - "MapStringStore" + write_fn(&Ty::List(Box::new(s.clone()))), + "wvWrite_list_string" ); assert_eq!( - stem(&Ty::List(Box::new(Ty::Optional(Box::new(Ty::Record( - "Entry".into() - )))))), - "ListOptEntry" + read_fn(&Ty::Optional(Box::new(item.clone()))), + "wvRead_opt_Item" ); assert_eq!(write_fn(&s), "(*wvWriter).writeString"); + assert_eq!(read_expr("r", &item), "wvReadItem(r)"); assert_eq!( - read_expr("r", &Ty::Record("Entry".into())), - "wvReadEntry(r)" + write_stmt("w", "v.ID", &Ty::Enum("user_kind".into())), + "wvWriteUserKind(w, v.ID)" ); } } diff --git a/crates/weaveffi-cli/src/targets/go/docs.rs b/crates/weaveffi-cli/src/targets/go/docs.rs index 4321b8d7..a48e3258 100644 --- a/crates/weaveffi-cli/src/targets/go/docs.rs +++ b/crates/weaveffi-cli/src/targets/go/docs.rs @@ -1,112 +1,146 @@ -//! Godoc comments: a declaration's doc starts with its name ("Path returns -//! the path the store was opened with."), followed by any further -//! paragraphs and a final `Deprecated:` paragraph. +//! Go doc comments, rendered from the shared [`Doc`] text. +//! +//! IDL docs are copied as written, with backticked API identifiers turned +//! into Go doc links (`` `new_op` `` is `[NewOp]`, a method `[Store.Get]`) +//! or their Go spelling (a field `ExpiresAt`, a parameter `ttlSeconds`). +//! Go convention starts the doc of an exported package-level declaration +//! with its name, so a summary that doesn't is prefixed with `Name: `; +//! fields, constants, and interface methods keep their text as is. A +//! deprecation becomes the final `Deprecated:` paragraph that Go tools +//! recognize. +use std::collections::BTreeMap; + +use crate::codegen::common::wrap; +use crate::codegen::docs::{map_code_spans, ApiNames, Doc, IdentKind}; use crate::codegen::CodeWriter; -use weaveffi_model::model::ParamBinding; +use weaveffi_model::model::Model; + +use crate::targets::go::names::{method_param, pascal, GoNames}; /// The width generated comment text wraps at (IDL docs keep their own line /// breaks). const WRAP: usize = 76; -/// What a doc comment describes, which decides how a summary that doesn't -/// start with the name is joined to it. -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum Kind { - /// A function or method with a result: "The path ..." reads "Path - /// returns the path ...". - Func, - /// A function or method without one: only a third-person verb or - /// "Whether" joins the name directly. - Proc, - /// A type or field: "The store's clock" reads "Clock is the store's - /// clock". - Value, - /// A variant, constant, or error code, whose IDL doc is often a clause - /// ("An entry was stored."): only a third-person verb joins the name - /// directly. - Clause, +/// Every API identifier a doc may name in backticks, with its Go spelling. +pub(crate) struct DocNames<'a> { + api: ApiNames, + go: &'a GoNames, + /// Enum variants (C-style constants and rich-enum structs) by C + /// constant. + variants: BTreeMap, } -impl Kind { - /// [`Kind::Func`] for a callable with a result, else [`Kind::Proc`]. - pub(crate) fn callable(returns: bool) -> Self { - if returns { - Kind::Func - } else { - Kind::Proc +impl<'a> DocNames<'a> { + /// Index the identifiers of `model`, spelled per `go`. + pub(crate) fn new(model: &Model, go: &'a GoNames) -> Self { + let variants = model + .modules + .iter() + .flat_map(|m| &m.enums) + .flat_map(|e| { + e.variants.iter().map(move |v| { + ( + v.c_const.clone(), + format!("{}{}", pascal(&e.name), pascal(&v.name)), + ) + }) + }) + .collect(); + Self { + api: ApiNames::new(model), + go, + variants, + } + } + + /// The Go replacement for a backticked identifier, backticks included: + /// a doc link for a package-level name or method, the plain Go spelling + /// for a field or parameter, and `None` (kept as written) for a module + /// or an identifier that names declarations of different kinds. + fn replace(&self, ident: &str) -> Option { + let link = |s: &str| Some(format!("[{s}]")); + match self.api.kind(ident)? { + IdentKind::Function | IdentKind::Member => { + link(self.go.callable(self.api.c_name(ident)?)?) + } + IdentKind::Type => link(&pascal(ident)), + IdentKind::ErrorDomain => link(&self.go.domain(ident).iface), + IdentKind::ErrorCode => link(self.go.code_by_const(self.api.c_name(ident)?)?), + IdentKind::Variant => link(self.variants.get(self.api.c_name(ident)?)?), + IdentKind::Field | IdentKind::CallbackMethod => Some(pascal(ident)), + IdentKind::Param => Some(method_param(ident)), + IdentKind::Module => None, } } + + /// `text` with its backticked identifiers in Go spelling. + pub(crate) fn render(&self, text: &str) -> String { + map_code_spans(text, |ident| self.replace(ident)) + } + + /// The doc text and deprecation message of a declaration, rendered. + pub(crate) fn of( + &self, + doc: &Option, + deprecated: &Option, + ) -> (Option, Option) { + let d = Doc::new(doc, deprecated); + let keep = |_: &str| None; + ( + d.text(keep).map(|t| self.render(&t)), + d.deprecation(keep).map(|t| self.render(&t)), + ) + } } -/// A godoc comment under construction: the summary paragraph naming the -/// declaration, then further paragraphs, then `Deprecated:`. +/// A Go doc comment under construction: paragraphs, then `Deprecated:`. +#[derive(Default)] pub(crate) struct GoDoc { - name: String, - paragraphs: Vec>, + paragraphs: Vec, deprecated: Option, } impl GoDoc { - /// A doc for `name` whose summary is the IDL doc `doc`, joined to the - /// name per `kind`, or `fallback` (which must start with the name) - /// when there's no IDL doc. - pub(crate) fn new(name: &str, doc: Option<&str>, kind: Kind, fallback: Option) -> Self { - let mut paragraphs = Vec::new(); - match doc.map(str::trim).filter(|d| !d.is_empty()) { - Some(d) => { - // Blank lines separate paragraphs. - let mut current = Vec::new(); - for (i, line) in d.lines().enumerate() { - let line = line.trim_end(); - if line.is_empty() { - if !current.is_empty() { - paragraphs.push(std::mem::take(&mut current)); - } - } else if i == 0 { - current.push(lead(name, line, kind)); - } else { - current.push(line.to_string()); - } - } - if !current.is_empty() { - paragraphs.push(current); - } + /// The doc of the exported package-level declaration `name`: `text`, + /// prefixed with `Name: ` unless its first word is already the name. + pub(crate) fn decl(name: &str, text: Option) -> Self { + let mut doc = Self::default(); + if let Some(text) = text { + let first = text.split_whitespace().next().unwrap_or_default(); + if first.trim_end_matches(['.', ',', ':', ';', '\'']) == name { + doc.paragraphs.push(text); + } else { + doc.paragraphs.push(format!("{name}: {text}")); } - None => paragraphs.extend(fallback.map(|f| wrap(&f))), } + doc + } + + /// A doc whose text needs no name (a field, constant, or interface + /// method). + pub(crate) fn plain(text: Option) -> Self { Self { - name: name.to_string(), - paragraphs, + paragraphs: text.into_iter().collect(), deprecated: None, } } - /// Append a paragraph of generated text, rewrapped. When it's the - /// first, it's joined to the name ("Wait blocks until ..."). + /// Append a paragraph of generated text, rewrapped. pub(crate) fn para(mut self, text: &str) -> Self { - let text = if self.paragraphs.is_empty() { - format!("{} {}", self.name, lower_first(text)) - } else { - text.to_string() - }; - self.paragraphs.push(wrap(&text)); + self.paragraphs.push(wrap(text, WRAP)); self } - /// Append a "Parameters:" list for the documented parameters, labelled - /// with their Go spellings (`name`). - pub(crate) fn params(mut self, params: &[ParamBinding], name: impl Fn(&str) -> String) -> Self { + /// Append a list of the documented parameters, labelled with their Go + /// spellings: `(name, doc)` pairs. + pub(crate) fn params(mut self, params: impl IntoIterator) -> Self { let mut list = Vec::new(); - for p in params { - let Some(doc) = p.doc.as_deref().map(str::trim).filter(|d| !d.is_empty()) else { - continue; - }; + for (name, doc) in params { let mut lines = doc.lines(); - let first = lines.next().unwrap_or_default(); - list.push(format!(" - {}: {first}", name(&p.name))); + list.push(format!(" - {name}: {}", lines.next().unwrap_or_default())); list.extend(lines.map(|l| { - if l.is_empty() { + if l.trim().is_empty() { String::new() } else { format!(" {l}") @@ -114,39 +148,30 @@ impl GoDoc { })); } if !list.is_empty() { - if self.paragraphs.is_empty() { - self.paragraphs - .push(vec![format!("{} takes these parameters:", self.name)]); - } else { - self.paragraphs.push(vec!["Parameters:".into()]); - } - self.paragraphs.push(list); + self.paragraphs.push("Parameters:".into()); + self.paragraphs.push(list.join("\n")); } self } /// Set the `Deprecated:` paragraph. - pub(crate) fn deprecated(mut self, msg: Option<&str>) -> Self { - self.deprecated = msg.map(str::to_string); + pub(crate) fn deprecated(mut self, msg: Option) -> Self { + self.deprecated = msg; self } - /// `true` when the comment has no paragraph yet. - pub(crate) fn is_empty(&self) -> bool { - self.paragraphs.is_empty() - } - /// Write the comment at the writer's indentation. pub(crate) fn emit(self, w: &mut CodeWriter) { let mut paragraphs = self.paragraphs; if let Some(msg) = self.deprecated { - paragraphs.push(vec![format!("Deprecated: {msg}")]); + paragraphs.push(wrap(&format!("Deprecated: {msg}"), WRAP)); } for (i, p) in paragraphs.iter().enumerate() { if i > 0 { w.line("//"); } - for line in p { + for line in p.lines() { + let line = line.trim_end(); if line.is_empty() { w.line("//"); } else { @@ -157,145 +182,10 @@ impl GoDoc { } } -/// The first line of an IDL summary, made to start with `name`: kept when -/// it already does; a third-person verb ("Returns ...") follows the name in -/// lowercase; "Whether ..." becomes "reports whether ..." for a callable or -/// field; "The ...", "A ...", or a wh-word becomes "returns ..." for a -/// callable with a result and "is ..." for a type or field; anything else -/// follows the name after a colon. -fn lead(name: &str, first: &str, kind: Kind) -> String { - let word = first.split_whitespace().next().unwrap_or_default(); - if word.trim_end_matches(['.', ',', ':', '\'']) == name { - return first.to_string(); - } - let lowered = lower_first(first); - let noun = matches!( - word, - "The" | "A" | "An" | "How" | "What" | "When" | "Where" | "Which" | "Who" | "Why" - ); - match kind { - _ if third_person(word) => format!("{name} {lowered}"), - Kind::Func | Kind::Proc | Kind::Value if word == "Whether" => { - format!("{name} reports {lowered}") - } - Kind::Func if noun => format!("{name} returns {lowered}"), - Kind::Value if noun => format!("{name} is {lowered}"), - _ => format!("{name}: {first}"), - } -} - -/// `true` for a capitalized third-person verb that opens doc summaries -/// ("Returns", "Creates", "Reports"). A word that can also be a plural -/// noun ("Stores", "Lists", "Statistics") isn't one. -fn third_person(word: &str) -> bool { - const VERBS: &[&str] = &[ - "Accepts", - "Adds", - "Advances", - "Allocates", - "Applies", - "Builds", - "Checks", - "Clears", - "Closes", - "Computes", - "Converts", - "Copies", - "Creates", - "Decodes", - "Deletes", - "Describes", - "Determines", - "Divides", - "Echoes", - "Emits", - "Encodes", - "Fetches", - "Finds", - "Fires", - "Formats", - "Frees", - "Gets", - "Gives", - "Greets", - "Identifies", - "Indicates", - "Installs", - "Loads", - "Makes", - "Merges", - "Notifies", - "Opens", - "Parses", - "Produces", - "Raises", - "Reads", - "Receives", - "Registers", - "Releases", - "Removes", - "Renders", - "Replaces", - "Reports", - "Represents", - "Resets", - "Resolves", - "Returns", - "Runs", - "Sends", - "Sets", - "Starts", - "Stops", - "Subscribes", - "Takes", - "Tells", - "Unsubscribes", - "Updates", - "Validates", - "Waits", - "Wraps", - "Writes", - "Yields", - ]; - VERBS.contains(&word) -} - -/// `s` with its first character lowercased, unless the first word is an -/// acronym or initialism (all capitals). -fn lower_first(s: &str) -> String { - let word = s.split_whitespace().next().unwrap_or_default(); - if word.len() > 1 && word.chars().all(|c| !c.is_lowercase()) { - return s.to_string(); - } - let mut chars = s.chars(); - match chars.next() { - Some(first) => first.to_lowercase().chain(chars).collect(), - None => String::new(), - } -} - -/// Generated text wrapped at [`WRAP`] columns. -fn wrap(text: &str) -> Vec { - let mut lines = Vec::new(); - let mut line = String::new(); - for word in text.split_whitespace() { - if !line.is_empty() && line.len() + 1 + word.len() > WRAP { - lines.push(std::mem::take(&mut line)); - } - if !line.is_empty() { - line.push(' '); - } - line.push_str(word); - } - if !line.is_empty() { - lines.push(line); - } - lines -} - #[cfg(test)] mod tests { use super::*; + use crate::codegen::test_model; fn render(doc: GoDoc) -> String { let mut w = CodeWriter::tabs(); @@ -304,58 +194,68 @@ mod tests { } #[test] - fn summaries_start_with_the_name() { - assert_eq!( - lead("Add", "Add two numbers.", Kind::Func), - "Add two numbers." - ); - assert_eq!( - lead("Sum", "Returns the sum.", Kind::Func), - "Sum returns the sum." - ); - assert_eq!( - lead("Path", "The path it was opened with.", Kind::Func), - "Path returns the path it was opened with." - ); + fn package_level_docs_start_with_the_name() { + let doc = |name: &str, text: &str| render(GoDoc::decl(name, Some(text.into()))); assert_eq!( - lead("Entry", "A stored value.", Kind::Value), - "Entry is a stored value." + doc("Sum", "Sum returns the sum."), + "// Sum returns the sum.\n" ); + assert_eq!(doc("Ping", "Trivial helper"), "// Ping: Trivial helper\n"); + assert_eq!(doc("Open", "Open a store."), "// Open a store.\n"); assert_eq!( - lead("Reason", "Why it failed.", Kind::Value), - "Reason is why it failed." - ); - assert_eq!( - lead("Accepts", "Whether to accept.", Kind::Func), - "Accepts reports whether to accept." - ); - assert_eq!( - lead("OnChange", "A change it accepted.", Kind::Proc), - "OnChange: A change it accepted." - ); - assert_eq!( - lead("ChangePut", "An entry was stored.", Kind::Clause), - "ChangePut: An entry was stored." - ); - assert_eq!(lead("Open", "Open a store.", Kind::Func), "Open a store."); - assert_eq!( - lead("Connect", "Open a store.", Kind::Func), - "Connect: Open a store." + render(GoDoc::plain(Some("A field.\n\nMore.".into()))), + "// A field.\n//\n// More.\n" ); + assert_eq!(render(GoDoc::decl("X", None)), ""); } #[test] - fn paragraphs_wrap_and_deprecation_comes_last() { - let doc = GoDoc::new("Old", Some("Returns it.\n\nMore."), Kind::Func, None) + fn deprecation_comes_last_and_long_text_wraps() { + let doc = GoDoc::decl("Old", Some("Old returns it.".into())) .para("Extra.") - .deprecated(Some("use New")); + .deprecated(Some("Use [New] instead.".into())); assert_eq!( render(doc), - "// Old returns it.\n//\n// More.\n//\n// Extra.\n//\n// Deprecated: use New\n" + "// Old returns it.\n//\n// Extra.\n//\n// Deprecated: Use [New] instead.\n" + ); + let long = render(GoDoc::plain(None).para(&"word ".repeat(30))); + assert!(long.lines().all(|l| l.len() <= WRAP + 3), "{long}"); + } + + #[test] + fn backticked_identifiers_become_links_or_go_spellings() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: kv + errors: + - name: KvErrors + codes: [{ name: not_found, code: 1, message: missing }] + enums: + - name: Mode + variants: [{ name: fast, value: 0 }] + structs: + - name: Entry + fields: [{ name: expires_at, type: i64 }] + interfaces: + - name: Store + constructors: [{ name: open, params: [] }] + methods: + - { name: get_all, params: [{ name: ttl_seconds, type: i64 }] } + functions: + - { name: new_op, params: [] } +"#, + ); + let go = GoNames::new(&model); + let names = DocNames::new(&model, &go); + assert_eq!( + names.render( + "Use `new_op`, `open`, `get_all`, `Entry`, `KvErrors`, `not_found`, \ + `fast`, `expires_at`, `ttl_seconds`, `kv`, or `x + 1`." + ), + "Use [NewOp], [OpenStore], [Store.GetAll], [Entry], [KvError], \ + [NotFoundError], [ModeFast], ExpiresAt, ttlSeconds, `kv`, or `x + 1`." ); - let bare = GoDoc::new("Wait", None, Kind::Func, None).para("Blocks until done."); - assert_eq!(render(bare), "// Wait blocks until done.\n"); - let long = "word ".repeat(30); - assert!(wrap(&long).iter().all(|l| l.len() <= WRAP)); } } diff --git a/crates/weaveffi-cli/src/targets/go/entities.rs b/crates/weaveffi-cli/src/targets/go/entities.rs index 341a4656..cda7859a 100644 --- a/crates/weaveffi-cli/src/targets/go/entities.rs +++ b/crates/weaveffi-cli/src/targets/go/entities.rs @@ -3,19 +3,19 @@ use crate::codegen::CodeWriter; use weaveffi_model::model::{ - CallShape, EnumBinding, ErrorBinding, FieldBinding, InterfaceBinding, ModuleBinding, - StructBinding, + EnumBinding, ErrorBinding, FieldBinding, InterfaceBinding, StructBinding, }; +use weaveffi_model::plan::CallbackRetPass; -use crate::targets::go::calls::{render_async, render_sync, Receiver}; -use crate::targets::go::codec::{func, read_expr, write_stmt, BufferTypes}; -use crate::targets::go::docs::{GoDoc, Kind}; -use crate::targets::go::names::{self, constructor, domain_type, pascal}; +use crate::targets::go::calls::{render_function, Receiver}; +use crate::targets::go::codec::{func, read_expr, write_stmt}; +use crate::targets::go::docs::GoDoc; +use crate::targets::go::names::{self, constructor, pascal}; use crate::targets::go::types::{go_str, go_type}; use crate::targets::go::Ctx; -/// One `name rest` line inside a struct or const block, with its optional -/// doc comment, for [`emit_aligned`]. +/// One `name rest` line inside a struct or const block, with its doc +/// comment, for [`emit_aligned`]. struct AlignedEntry { doc: Option, name: String, @@ -26,7 +26,7 @@ struct AlignedEntry { /// it out: gofmt aligns the second column of consecutive lines and starts a /// fresh column after a comment line, so each run of entries following a /// documented one (or the start) is padded to the widest name in that run. -fn emit_aligned(w: &mut CodeWriter, entries: Vec, kind: Kind) { +fn emit_aligned(w: &mut CodeWriter, entries: Vec) { let mut i = 0; while i < entries.len() { let mut j = i + 1; @@ -39,7 +39,7 @@ fn emit_aligned(w: &mut CodeWriter, entries: Vec, kind: Kind) { .max() .unwrap_or(0); for e in &entries[i..j] { - GoDoc::new(&e.name, e.doc.as_deref(), kind, None).emit(w); + GoDoc::plain(e.doc.clone()).emit(w); w.line(format!("{:, kind: Kind) { } /// The struct-body entries of `fields`, named by `name`. -fn field_entries(fields: &[FieldBinding], name: fn(&str) -> String) -> Vec { +fn field_entries( + ctx: &Ctx, + fields: &[FieldBinding], + name: fn(&str) -> String, +) -> Vec { fields .iter() .map(|f| AlignedEntry { - doc: f.doc.clone().filter(|d| !d.trim().is_empty()), + doc: ctx.docs.of(&f.doc, &None).0, name: name(&f.name), rest: go_type(&f.ty), }) @@ -60,11 +64,10 @@ fn field_entries(fields: &[FieldBinding], name: fn(&str) -> String) -> Vec GoDoc::decl(&vn, text), + None => GoDoc::plain(None).para(&format!("{vn} is the {} variant of {name}.", v.name)), + }; + doc.emit(w); if v.fields.is_empty() { w.line(format!("type {vn} struct{{}}")); } else { w.block(format!("type {vn} struct {{"), "}", |w| { - emit_aligned(w, field_entries(&v.fields, names::field), Kind::Value); + emit_aligned(w, field_entries(ctx, &v.fields, names::field)); }); } w.blank(); @@ -197,13 +201,12 @@ pub(crate) fn render_rich_enum(w: &mut CodeWriter, package: &str, e: &EnumBindin /// Render a record as a value struct with exported fields, plus its codec /// pair: the fields in declaration order. -pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { +pub(crate) fn render_struct(w: &mut CodeWriter, ctx: &Ctx, s: &StructBinding) { let name = pascal(&s.name); - GoDoc::new(&name, s.doc.as_deref(), Kind::Value, None) - .deprecated(s.deprecated.as_deref()) - .emit(w); + let (doc, deprecated) = ctx.docs.of(&s.doc, &s.deprecated); + GoDoc::decl(&name, doc).deprecated(deprecated).emit(w); w.block(format!("type {name} struct {{"), "}", |w| { - emit_aligned(w, field_entries(&s.fields, names::field), Kind::Value); + emit_aligned(w, field_entries(ctx, &s.fields, names::field)); }); w.blank(); w.block( @@ -235,50 +238,31 @@ pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { w.blank(); } -/// The name of the helper converting a failure in the domain `e` into its -/// error-code type (`wvKvError`). -pub(crate) fn domain_mapper(e: &ErrorBinding) -> String { - format!("wv{}", domain_type(e)) -} - -/// Render an error domain: the sealed `{Domain}` interface every code type -/// implements, one `*{Code}Error` struct per code (its payload fields, a -/// `Message`, `Error`, and `Code`), and the helper mapping a failure to the -/// code's type, with an *Error for any code outside the domain. -pub(crate) fn render_error(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding, e: &ErrorBinding) { +/// Render an error domain: the sealed interface every code type implements +/// (`KitchenError`), one `*{Code}Error` struct per code (its payload +/// fields, a `Message`, `Error`, and `Code`), the `*Unknown{Domain}` type of +/// a code these bindings don't declare, and the helper mapping a failure +/// onto the domain (an `*Error` for a runtime code). +pub(crate) fn render_error(w: &mut CodeWriter, ctx: &Ctx, e: &ErrorBinding) { let gn = ctx.names; - let domain = domain_type(e); + let d = gn.domain_of(e); + let domain = &d.iface; let marker = format!("is{domain}"); - let codes: Vec<&str> = e.codes.iter().map(|c| gn.code(c)).collect(); - let list = match codes.as_slice() { - [one] => format!("*{one}"), - [init @ .., last] => format!( - "{}, or *{last}", - init.iter() - .map(|c| format!("*{c}")) - .collect::>() - .join(", ") - ), - [] => unreachable!("an error domain declares at least one code"), - }; - GoDoc::new( - &domain, - None, - Kind::Value, - Some(format!( - "{domain} is an error the {} module reports: {list}.", - m.dot_path - )), - ) - .para(&if ctx.throwing_callbacks { - format!( - "Match one code with errors.As and its type, or any of them with a \ - {domain}. A throwing callback method returns one to report that code." - ) - } else { - format!("Match one code with errors.As and its type, or any of them with a {domain}.") - }) - .emit(w); + let codes: Vec = e.codes.iter().map(|c| format!("*{}", gn.code(c))).collect(); + GoDoc::plain(None) + .para(&format!( + "{domain} is an error of the {} domain: {}, or *{} for a code these \ + bindings don't declare.", + e.name, + codes.join(", "), + d.unknown + )) + .para(&format!( + "Match one code with errors.As and its type, or any code of the domain \ + with a {domain}. A callback method that declares the domain returns \ + one to report that code." + )) + .emit(w); w.block(format!("type {domain} interface {{"), "}", |w| { w.line("error"); w.line("// Code returns the error's numeric code."); @@ -289,25 +273,23 @@ pub(crate) fn render_error(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding, e: for c in &e.codes { let ty = gn.code(c); - let doc = c - .doc - .as_deref() - .filter(|d| d.trim() != c.message.trim()) - .map(str::to_string); - GoDoc::new( - ty, - doc.as_deref(), - Kind::Clause, - Some(format!( + let text = ctx + .docs + .of(&c.doc, &None) + .0 + .filter(|d| d.trim() != c.message.trim()); + let doc = match text { + Some(_) => GoDoc::decl(ty, text), + None => GoDoc::plain(None).para(&format!( "{ty} is the {domain} code {} ({}): {}.", c.name, c.value, c.message.trim_end_matches('.') )), - ) - .emit(w); + }; + doc.emit(w); w.block(format!("type {ty} struct {{"), "}", |w| { - emit_aligned(w, field_entries(&c.fields, names::error_field), Kind::Value); + emit_aligned(w, field_entries(ctx, &c.fields, names::error_field)); w.line("// Message describes the failure; when it's empty, Error returns the"); w.line(format!( "// code's default message, {}.", @@ -333,9 +315,7 @@ pub(crate) fn render_error(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding, e: w.line(format!("func (*{ty}) {marker}() {{}}")); w.blank(); if c.fields.is_empty() { - w.block(format!("func (*{ty}) wvPayload() []byte {{"), "}", |w| { - w.line("return nil"); - }); + func(w, &format!("func (*{ty}) wvPayload() []byte"), "return nil"); } else { w.block(format!("func (e *{ty}) wvPayload() []byte {{"), "}", |w| { w.line("w := &wvWriter{}"); @@ -345,12 +325,47 @@ pub(crate) fn render_error(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding, e: } w.line("return w.buf"); }); + w.blank(); } - w.blank(); } + let unknown = &d.unknown; + GoDoc::plain(None) + .para(&format!( + "{unknown} is a {domain} code these bindings don't declare, which a \ + newer library may report." + )) + .emit(w); + w.block(format!("type {unknown} struct {{"), "}", |w| { + w.line("code int32"); + w.line("// Message is the library's message."); + w.line("Message string"); + }); + w.blank(); + w.line("// Error returns the error's message."); + w.block(format!("func (e *{unknown}) Error() string {{"), "}", |w| { + w.block("if e.Message == \"\" {", "}", |w| { + w.line(format!("return wvCodeMessage({}, e.code)", go_str(&e.name))); + }); + w.line("return e.Message"); + }); + w.blank(); + w.line("// Code returns the code the library reported."); + func( + w, + &format!("func (e *{unknown}) Code() int32"), + "return e.code", + ); + w.line(format!("func (*{unknown}) {marker}() {{}}")); + w.blank(); + func( + w, + &format!("func (*{unknown}) wvPayload() []byte"), + "return nil", + ); + w.block( - format!("func {}(f wvFailure) error {{", domain_mapper(e)), + format!("func {}(f wvFailure) error {{", d.mapper), "}", |w| { w.line("switch f.code {"); @@ -378,50 +393,52 @@ pub(crate) fn render_error(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding, e: }); } w.line("}"); + w.block("if f.code > 0 {", "}", |w| { + w.line(format!( + "return &{unknown}{{code: f.code, Message: f.message}}" + )); + }); w.line("return f.err()"); }, ); w.blank(); } -/// Render an interface as a reference-counted object wrapper whose `wvRef` -/// owns one strong reference, released by `Close` (idempotent, and safe to -/// race with in-flight calls) or, as a backstop, by a finalizer; its codec -/// pair when it appears inside a buffer (an object token carrying a fresh -/// reference); and its members: constructors as factories (`OpenStore`), -/// methods on the wrapper, and statics prefixed with the type -/// (`StoreDefaultCapacity`). +/// Render an interface as a reference-counted object wrapper embedding a +/// `wvObject`, which owns one strong reference released by `Close` +/// (idempotent, and safe to race with in-flight calls) or by a cleanup once +/// the wrapper is unreachable; its codec pair when it appears inside a +/// buffer (an object token carrying a fresh reference); and its members: +/// constructors as factories (`OpenStore`), methods on the wrapper, and +/// statics prefixed with the type (`StoreDefaultCapacity`). pub(crate) fn render_interface(w: &mut CodeWriter, ctx: &Ctx, iface: &InterfaceBinding) { let package = ctx.package; let name = pascal(&iface.name); let c_tag = &iface.c_tag; - GoDoc::new( - &name, - iface.doc.as_deref(), - Kind::Value, - Some(format!( + let (doc, deprecated) = ctx.docs.of(&iface.doc, &iface.deprecated); + let doc = match doc { + Some(_) => GoDoc::decl(&name, doc), + None => GoDoc::plain(None).para(&format!( "{name} is a reference-counted object owned by the native library." )), + }; + doc.para( + "Each wrapper holds one strong reference to the object. Close releases \ + it; a wrapper that's never closed releases it some time after it becomes \ + unreachable.", ) - .para( - "Each wrapper holds one strong reference; Close releases it, and a \ - finalizer releases it if the wrapper is garbage collected first.", - ) - .deprecated(iface.deprecated.as_deref()) + .deprecated(deprecated) .emit(w); w.block(format!("type {name} struct {{"), "}", |w| { - w.line("ref wvRef"); + w.line("wvObject"); }); w.blank(); - w.block( - format!("func wvDestroy{name}(ptr unsafe.Pointer) {{"), - "}", - |w| { - w.line(format!("C.{}((*C.{c_tag})(ptr))", iface.destroy_symbol)); - }, + func( + w, + &format!("func wvDestroy{name}(ptr unsafe.Pointer)"), + &format!("C.{}((*C.{c_tag})(ptr))", iface.destroy_symbol), ); - w.blank(); w.line(format!( "// wvAdopt{name} wraps one owned strong reference. A null pointer adopts to nil." @@ -434,15 +451,14 @@ pub(crate) fn render_interface(w: &mut CodeWriter, ctx: &Ctx, iface: &InterfaceB w.line("return nil"); }); w.line(format!("s := &{name}{{}}")); - w.line(format!("s.ref.init(unsafe.Pointer(ptr), wvDestroy{name})")); - w.line(format!("runtime.SetFinalizer(s, (*{name}).Close)")); + w.line(format!("s.adopt(unsafe.Pointer(ptr), wvDestroy{name})")); w.line("return s"); }, ); w.blank(); w.line("// native borrows the wrapper's pointer for one call; pair it with a"); - w.line("// deferred s.ref.release()."); + w.line("// deferred s.release()."); w.block( format!("func (s *{name}) native() *C.{c_tag} {{"), "}", @@ -450,28 +466,36 @@ pub(crate) fn render_interface(w: &mut CodeWriter, ctx: &Ctx, iface: &InterfaceB w.block("if s == nil {", "}", |w| { w.line(format!("panic(\"{package}: nil *{name}\")")); }); - w.line(format!("return (*C.{c_tag})(s.ref.acquire(\"{name}\"))")); + w.line(format!("return (*C.{c_tag})(s.acquire(\"{name}\"))")); }, ); w.blank(); - w.line("// share returns a new strong reference to the wrapper's object for the"); - w.line("// native library to adopt (the wrapper keeps its own), or null for nil."); - w.block( - format!("func (s *{name}) share() *C.{c_tag} {{"), - "}", - |w| { - w.block("if s == nil {", "}", |w| { - w.line("return nil"); - }); - w.line("ptr := s.native()"); - w.line("defer s.ref.release()"); - w.line(format!("return C.{}(ptr)", iface.clone_symbol)); - }, - ); - w.blank(); + let in_buffers = ctx.codecs.has_interface(&iface.name); + let returned = ctx.model.callback_interfaces().any(|(_, cb)| { + cb.methods.iter().any(|m| { + matches!(&m.ret_pass, CallbackRetPass::Object { interface, .. } if *interface == iface.name) + }) + }); + if in_buffers || returned { + w.line("// share returns a new strong reference to the wrapper's object for the"); + w.line("// native library to adopt (the wrapper keeps its own), or null for nil."); + w.block( + format!("func (s *{name}) share() *C.{c_tag} {{"), + "}", + |w| { + w.block("if s == nil {", "}", |w| { + w.line("return nil"); + }); + w.line("ptr := s.native()"); + w.line("defer s.release()"); + w.line(format!("return C.{}(ptr)", iface.clone_symbol)); + }, + ); + w.blank(); + } - if ctx.codecs.interfaces.contains(&iface.name) { + if in_buffers { func( w, &format!("func wvWrite{name}(w *wvWriter, v *{name})"), @@ -493,36 +517,14 @@ pub(crate) fn render_interface(w: &mut CodeWriter, ctx: &Ctx, iface: &InterfaceB w.blank(); } - w.line("// Close releases the wrapper's strong reference and always returns nil."); - w.line("// It's idempotent and safe to call from any goroutine, even while a call"); - w.line("// on the wrapper is in flight: the reference is then released when that"); - w.line("// call returns. The object itself is dropped once its last reference, from"); - w.line("// any wrapper, record, or the native library, is gone."); - w.block(format!("func (s *{name}) Close() error {{"), "}", |w| { - w.line("runtime.SetFinalizer(s, nil)"); - w.line("s.ref.close()"); - w.line("return nil"); - }); - w.blank(); - for c in &iface.constructors { - render_sync(w, ctx, c, &constructor(&name, c), None); + render_function(w, ctx, c, &constructor(&name, c), None); } for f in &iface.methods { let go_name = names::method(&f.name); - let recv = Some(Receiver { ty: &name }); - if let CallShape::Async(ab) = &f.shape { - render_async(w, ctx, f, ab, &go_name, recv); - } else { - render_sync(w, ctx, f, &go_name, recv); - } + render_function(w, ctx, f, &go_name, Some(Receiver { ty: &name })); } for f in &iface.statics { - let go_name = format!("{name}{}", pascal(&f.name)); - if let CallShape::Async(ab) = &f.shape { - render_async(w, ctx, f, ab, &go_name, None); - } else { - render_sync(w, ctx, f, &go_name, None); - } + render_function(w, ctx, f, ctx.names.function(f), None); } } diff --git a/crates/weaveffi-cli/src/targets/go/mod.rs b/crates/weaveffi-cli/src/targets/go/mod.rs index 0ca29bff..a2e8a212 100644 --- a/crates/weaveffi-cli/src/targets/go/mod.rs +++ b/crates/weaveffi-cli/src/targets/go/mod.rs @@ -2,23 +2,29 @@ //! //! Emits a self-contained Go module under `go/`: `go.mod`, a copy of the C //! header, `bindings.go` (the API), `runtime.go` (load-time checks, errors, -//! strings, objects, callbacks, and async plumbing), and `codec.go` (the -//! value-buffer writer and reader). The module path defaults to the package -//! name and the Go package name is the C prefix; the bindings link -//! `-l{library}` at build time. Implements [`LanguageBackend`]. +//! strings, typed arrays, objects, callbacks, iterators, and async +//! plumbing), and `codec.go` (the value-buffer writer and reader). The +//! module path defaults to the package name and the Go package name to the +//! C prefix without underscores; the bindings link `-l{library}` at build +//! time. //! //! Records are value structs, rich enums sealed interfaces, and optionals, -//! lists, and maps Go pointers, slices, and maps; all of them cross the C -//! ABI as one value buffer, with one codec pair per type. Interfaces are -//! reference-counted objects: each wrapper holds one strong reference -//! released by `Close` or, as a backstop, a finalizer. Callback interfaces -//! are Go interfaces the consumer implements, crossing as a handle-table -//! context plus the address of one static vtable per interface, filled with -//! exported trampolines. A throwing call returns `error` values of its -//! domain's code types; any other failure panics with an `*Error`. Async +//! lists, and maps Go pointers, slices, and maps. Optional scalars cross as +//! a presence flag and a value, numeric lists as typed arrays (a Go slice's +//! own storage, on the way in), and everything else composite as one value +//! buffer with one codec pair per type. Interfaces are reference-counted +//! objects: each wrapper holds one strong reference released by `Close` or, +//! as a backstop, a `runtime.AddCleanup` cleanup. Callback interfaces are Go +//! interfaces the consumer implements, crossing as a handle-table context +//! plus the address of one static vtable per interface, filled with +//! exported trampolines. A function that declares an error domain returns +//! that domain's code types (an unknown code is `Unknown{Domain}`), one +//! that fails with any error returns an `*Error`, and any other panics on +//! failure; async functions always return an error instead. Async //! functions block on a `context.Context`, and a cancellable one cancels -//! its native token when the context is done. Loading checks the ABI -//! revision and every top-level module's contract table. +//! its native token when the context is done. The first call checks the +//! ABI revision and every top-level module's contract table, which `Check` +//! reports as an error. mod callbacks; mod calls; @@ -32,20 +38,21 @@ mod runtime; mod tests; mod types; -use crate::backend::{LanguageBackend, OutputFile}; -use crate::codegen::CodeWriter; -use crate::lang; +use crate::codegen::{contract, CodeWriter, OutputFile}; use crate::package::{Artifact, PackageContext}; use crate::targets::c::render_c_header_from_model; +use crate::targets::{Linkage, Target}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use camino::Utf8Path; +use camino::Utf8PathBuf; +use miette::Result; use serde::{Deserialize, Serialize}; -use weaveffi_model::contract::entries; -use weaveffi_model::model::{contract_symbol, CallShape, ErrorBinding, Model, ModuleBinding}; +use weaveffi_model::model::{Model, ModuleBinding}; +use weaveffi_model::plan::ErrorStrategy; use crate::targets::go::callbacks::{emit_preamble_decls, render_callback_interface}; -use crate::targets::go::calls::{render_async, render_sync}; +use crate::targets::go::calls::render_function; use crate::targets::go::codec::BufferTypes; +use crate::targets::go::docs::DocNames; use crate::targets::go::entities::{ render_enum, render_error, render_interface, render_rich_enum, render_struct, }; @@ -60,14 +67,21 @@ use crate::targets::go::types::go_str; pub struct GoConfig { /// Go module path written to `go.mod`. Defaults to the package name. pub name: Option, + /// Go package name. Defaults to the C prefix without underscores. + pub package: Option, } +/// The marker Go tools recognize on generated files (linters skip them and +/// editors warn before an edit). +const GENERATED: &str = "// Code generated by WeaveFFI. DO NOT EDIT.\n\n"; + /// Every identity-derived name the generated module uses. pub(crate) struct Names { /// The Go module path (`go.mod`): the configured `name`, else the /// package name. pub(crate) module_path: String, - /// The Go package name: the C prefix, escaped if it's a Go keyword. + /// The Go package name: the configured `package`, else the C prefix + /// without underscores, escaped if it's a Go keyword. pub(crate) package: String, /// The native library base name the bindings link (`-l{library}`). pub(crate) library: String, @@ -78,15 +92,17 @@ pub(crate) struct Names { impl Names { pub(crate) fn new(model: &Model, config: &GoConfig) -> Self { let id = &model.identity; - Self { - module_path: config - .name - .as_deref() + let configured = |v: &Option| { + v.as_deref() .map(str::trim) - .filter(|p| !p.is_empty()) - .unwrap_or(&id.name) - .to_string(), - package: lang::escape_ident(&id.prefix, lang::GO_KEYWORDS), + .filter(|s| !s.is_empty()) + .map(str::to_string) + }; + Self { + module_path: configured(&config.name).unwrap_or_else(|| id.name.clone()), + package: configured(&config.package) + .map(|p| crate::lang::escape_ident(&p, crate::lang::GO_KEYWORDS)) + .unwrap_or_else(|| names::package(&id.prefix)), library: id.library.clone(), header: crate::targets::c::header_name(model), } @@ -96,35 +112,69 @@ impl Names { /// What every renderer needs beyond the declaration it renders. #[derive(Clone, Copy)] pub(crate) struct Ctx<'a> { + /// The model being rendered. + pub(crate) model: &'a Model, /// The C symbol prefix. pub(crate) prefix: &'a str, /// The Go package name. pub(crate) package: &'a str, /// The Go name of every declaration. pub(crate) names: &'a GoNames, + /// Doc text in Go spelling. + pub(crate) docs: &'a DocNames<'a>, /// The types that cross inside value buffers. pub(crate) codecs: &'a BufferTypes, - /// The error domain in scope for the module being rendered. - pub(crate) domain: Option<&'a ErrorBinding>, - /// Whether any callback method is declared `throws` (so consumers - /// construct domain errors themselves). - pub(crate) throwing_callbacks: bool, +} + +impl Ctx<'_> { + /// The Go function (of type `func(wvFailure) error`) mapping a failure + /// of a callable with error strategy `e` onto its error: the domain's + /// mapper, or the generic `*Error`. + pub(crate) fn mapper(&self, e: &ErrorStrategy) -> String { + match e { + ErrorStrategy::Domain(d) => self.names.domain(d).mapper.clone(), + ErrorStrategy::Trap | ErrorStrategy::Untyped => "wvFailure.err".into(), + } + } + + /// The Go expression mapping the `wvFailure` value `failure` onto the + /// error of a callable with error strategy `e` (see [`Self::mapper`]). + pub(crate) fn map_failure(&self, e: &ErrorStrategy, failure: &str) -> String { + match e { + ErrorStrategy::Domain(d) => format!("{}({failure})", self.names.domain(d).mapper), + ErrorStrategy::Trap | ErrorStrategy::Untyped => format!("{failure}.err()"), + } + } } /// Go backend: emits a cgo module binding the C ABI exposed by the /// underlying cdylib. -pub struct GoGenerator; +pub struct GoGenerator { + config: GoConfig, +} -impl LanguageBackend for GoGenerator { - type Config = GoConfig; +impl From for GoGenerator { + fn from(config: GoConfig) -> Self { + Self { config } + } +} +impl Target for GoGenerator { fn name(&self) -> &'static str { "go" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { - let dir = out_dir.join("go"); - render_files(model, config) + fn linkage(&self) -> Linkage { + Linkage::Link + } + + fn fixed_files(&self) -> &'static [&'static str] { + &["README.md", "codec.go", "go.mod", "runtime.go"] + } + + fn render(&self, model: &Model) -> Vec { + let dir = Utf8PathBuf::new(); + render_files(model, &self.config) .into_iter() .map(|(name, contents)| OutputFile::new(dir.join(name), contents)) .collect() @@ -132,13 +182,8 @@ impl LanguageBackend for GoGenerator { /// The module directory at `go/{package}/` with each desktop library /// under `lib//` and a cgo preamble that links the right one. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { - Some(package_files(model, ctx, config)) + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + Ok(package_files(model, ctx, &self.config)) } } @@ -163,38 +208,42 @@ pub(crate) fn render_files(model: &Model, config: &GoConfig) -> Vec<(String, Str ] } +/// The head every Go file starts with: the WeaveFFI prelude and the Go +/// generated-code marker. +pub(crate) fn go_prelude() -> String { + format!("{}{GENERATED}", render_prelude(CommentStyle::DoubleSlash)) +} + /// Render `bindings.go`: the cgo preamble, the imports, the load-time /// checks, every module's declarations, and the composite codecs. pub(crate) fn render_bindings(model: &Model, names: &Names) -> String { let go_names = GoNames::new(model); + let doc_names = DocNames::new(model, &go_names); let codecs = BufferTypes::of(model); - let base = Ctx { + let ctx = Ctx { + model, prefix: model.prefix(), package: &names.package, names: &go_names, + docs: &doc_names, codecs: &codecs, - domain: None, - throwing_callbacks: model - .callback_interfaces() - .any(|(_, cb)| cb.methods.iter().any(|m| m.throws)), }; let mut w = CodeWriter::tabs(); - w.raw(render_prelude(CommentStyle::DoubleSlash)); + w.raw(go_prelude()); w.line(format!( "// Package {} binds the {} native library.", names.package, names.library )); + w.line("//"); + w.line("// Call [Check] at startup to find out whether the linked library is the"); + w.line("// one these bindings were generated for."); w.line(format!("package {}", names.package)); w.blank(); render_preamble(&mut w, model, names, &codecs); render_imports(&mut w, model); - render_init(&mut w, model); + render_checks(&mut w, model); for m in &model.modules { - let ctx = Ctx { - domain: model.error_domain(m), - ..base - }; render_module(&mut w, &ctx, m); } codecs.render_composites(&mut w); @@ -218,7 +267,7 @@ fn render_preamble(w: &mut CodeWriter, model: &Model, names: &Names, codecs: &Bu w.line("#cgo CFLAGS: -Wno-deprecated-declarations"); } w.line(format!("#include \"{}\"", names.header)); - if !codecs.interfaces.is_empty() { + if codecs.has_interfaces() { // Widening an object token to a pointer in C keeps `go vet` from // flagging a "possible misuse of unsafe.Pointer". Each preamble // helper is compiled into every cgo translation unit, some of which @@ -236,14 +285,14 @@ fn render_preamble(w: &mut CodeWriter, model: &Model, names: &Names, codecs: &Bu /// Write the standard-library imports `bindings.go` uses; everything else /// lives in `runtime.go` and `codec.go`, whose imports are fixed. fn render_imports(w: &mut CodeWriter, model: &Model) { - let objects = model.has_interfaces(); - let callbacks = model.has_callback_interfaces(); let has_async = model.has_async(); let packages = [ (has_async, "context"), (model.has_iterators(), "iter"), - (objects, "runtime"), - (objects || callbacks || has_async, "unsafe"), + ( + model.has_interfaces() || model.has_callback_interfaces() || has_async, + "unsafe", + ), ]; let used: Vec<&str> = packages .iter() @@ -261,54 +310,64 @@ fn render_imports(w: &mut CodeWriter, model: &Model) { w.blank(); } -/// Write the load-time checks: the ABI revision, then every top-level -/// module's contract table against the entries these bindings were -/// generated with. -fn render_init(w: &mut CodeWriter, model: &Model) { - w.block("func init() {", "}", |w| { - w.line("wvCheckABI()"); - let mut first = true; - for root in model.roots() { - if first { +/// Write `wvCheckLibrary`, the load-time checks the Go `Check` runs: the ABI +/// revision, then every top-level module's contract table against the rows +/// these bindings were generated with. +fn render_checks(w: &mut CodeWriter, model: &Model) { + w.line("// wvCheckLibrary checks the linked library's ABI revision and every"); + w.line("// top-level module's contract table."); + w.block("func wvCheckLibrary() error {", "}", |w| { + w.block("if err := wvCheckABI(); err != nil {", "}", |w| { + w.line("return err"); + }); + for (i, table) in contract::tables(model).into_iter().enumerate() { + // The table is fetched before `n` is read: Go leaves the order + // of a call and a plain operand in one argument list open. + if i == 0 { w.line("var n C.size_t"); + w.line(format!("table := C.{}(&n)", table.symbol)); + } else { + w.line(format!("table = C.{}(&n)", table.symbol)); } - let assign = if first { ":=" } else { "=" }; - first = false; - w.line(format!( - "table {assign} C.{}(&n)", - contract_symbol(model.prefix(), &root.name) - )); - w.block("wvCheckContract(table, n, []wvContractEntry{", "})", |w| { - for e in entries(model, root) { - w.line(format!( - "{{{:#018x}, {:#018x}, {}}},", - e.id, - e.hash, - go_str(&e.path) - )); - } - }); + w.block( + "if err := wvCheckContract(table, n, []wvContractEntry{", + "}); err != nil {", + |w| { + for row in &table.rows { + w.line(format!( + "{{{}, {}, {}}},", + contract::hex(row.id), + contract::hex(row.hash), + go_str(&row.path) + )); + } + }, + ); + w.indent(); + w.line("return err"); + w.dedent(); + w.line("}"); } + w.line("return nil"); }); w.blank(); } -/// Render every declaration of `m`: its error domain (in the declaring -/// module; submodules use the ancestor's), enums, records, callback -/// interfaces, interfaces, and functions. +/// Render every declaration of `m`: its error domains, enums, records, +/// callback interfaces, interfaces, and functions. fn render_module(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding) { - if let Some(e) = &m.errors { - render_error(w, ctx, m, e); + for e in &m.errors { + render_error(w, ctx, e); } for e in &m.enums { if e.is_rich() { - render_rich_enum(w, ctx.package, e); + render_rich_enum(w, ctx, e); } else { - render_enum(w, e, ctx.codecs); + render_enum(w, ctx, e); } } for s in &m.structs { - render_struct(w, s); + render_struct(w, ctx, s); } for cb in &m.callback_interfaces { render_callback_interface(w, ctx, cb); @@ -317,11 +376,6 @@ fn render_module(w: &mut CodeWriter, ctx: &Ctx, m: &ModuleBinding) { render_interface(w, ctx, i); } for f in &m.functions { - let go_name = ctx.names.function(f); - if let CallShape::Async(ab) = &f.shape { - render_async(w, ctx, f, ab, go_name, None); - } else { - render_sync(w, ctx, f, go_name, None); - } + render_function(w, ctx, f, ctx.names.function(f), None); } } diff --git a/crates/weaveffi-cli/src/targets/go/names.rs b/crates/weaveffi-cli/src/targets/go/names.rs index 00431b45..4cd37ce7 100644 --- a/crates/weaveffi-cli/src/targets/go/names.rs +++ b/crates/weaveffi-cli/src/targets/go/names.rs @@ -5,11 +5,13 @@ //! name is computed here once ([`GoNames`]) and looked up by the renderers: //! types keep their IDL name (`Store`), constructors become factories //! (`OpenStore`), statics are prefixed with their type (`StoreOpenMany`), -//! error codes become `*{Code}Error` types, and a free function whose name -//! would clash with any of those keeps its module path (`KvOpenStore`). +//! error domains and codes take the shared error type names (`KvError`, +//! `*KeyNotFoundError`), and a free function whose name would clash with +//! any of those keeps its module path (`KvOpenStore`). use heck::ToSnakeCase; use std::collections::{BTreeMap, BTreeSet}; +use weaveffi_model::errors::type_name; use weaveffi_model::model::{ErrorBinding, ErrorCodeBinding, FnBinding, Model}; use crate::lang; @@ -22,12 +24,15 @@ const INITIALISMS: &[&str] = &[ "UDP", "UI", "UID", "URI", "URL", "UTF8", "UUID", "VM", "XML", "XMPP", "XSRF", "XSS", ]; +/// Exported names the runtime files declare. +const RUNTIME_NAMES: &[&str] = &["Check", "DebugLive", "Error"]; + /// Names a wrapper or trampoline body uses besides its parameters: the /// packages it calls into, the receiver, and its locals. A parameter or /// slot spelled like one of them gains a trailing underscore. const RESERVED: &[&str] = &[ - "C", "cErr", "cIter", "cItem", "cItemLen", "cIterErr", "cRet", "cRetLen", "cSelf", "call", - "context", "err", "fail", "iter", "more", "res", "ret", "runtime", "token", "unsafe", "yield", + "C", "cErr", "cOut", "cRet", "cRetLen", "cSelf", "call", "context", "err", "iter", "it", "ret", + "runtime", "token", "unsafe", ]; /// The words of an identifier in any IDL spelling (`snake_case`, @@ -63,9 +68,8 @@ pub(crate) fn pascal(name: &str) -> String { /// The unexported (lowerCamelCase) Go spelling of an IDL name, with Go's /// initialisms after the first word: `user_id` is `userID`, `url` is `url`. pub(crate) fn camel(name: &str) -> String { - let words = words(name); let mut out = String::new(); - for (i, w) in words.iter().enumerate() { + for (i, w) in words(name).iter().enumerate() { if i == 0 { out.push_str(w); } else { @@ -75,6 +79,18 @@ pub(crate) fn camel(name: &str) -> String { out } +/// The default Go package name for a C prefix: the prefix without its +/// underscores (`kitchen_sink` is `kitchensink`), lowercased, and escaped +/// if it's a Go keyword. +pub(crate) fn package(prefix: &str) -> String { + let name: String = prefix + .chars() + .filter(|c| *c != '_') + .flat_map(char::to_lowercase) + .collect(); + lang::escape_ident(&name, lang::GO_KEYWORDS) +} + /// Methods every object wrapper declares: `Close` releases the wrapper's /// reference (its unexported helpers can't collide with an exported name). const OBJECT_METHODS: &[&str] = &["Close"]; @@ -95,7 +111,7 @@ pub(crate) fn method(name: &str) -> String { pub(crate) fn param(name: &str, f: &FnBinding) -> String { let ident = lang::escape_ident(&camel(name), lang::GO_KEYWORDS); let taken = RESERVED.contains(&ident.as_str()) - || (f.has_self && ident == "s") + || (f.has_self() && ident == "s") || (f.is_async() && ident == "ctx"); if taken { format!("{ident}_") @@ -123,10 +139,7 @@ pub(crate) fn slot(name: &str) -> String { } } -/// The exported Go name of a struct field or variant field. `Error`, -/// `Code`, and `Message` are taken on error-code types, so a field spelled -/// like one of those gains a trailing underscore there (see -/// [`error_field`]). +/// The exported Go name of a struct field or variant field. pub(crate) fn field(name: &str) -> String { pascal(name) } @@ -143,11 +156,29 @@ pub(crate) fn error_field(name: &str) -> String { } } +/// The Go names of one error domain. +#[derive(Debug, Clone)] +pub(crate) struct DomainNames { + /// The sealed interface every code type implements (`KvError`): the + /// shared [`type_name`] in Go's casing. + pub(crate) iface: String, + /// The concrete type of a code these bindings don't declare + /// (`UnknownKvError`). + pub(crate) unknown: String, + /// The unexported helper mapping a failure onto the domain + /// (`wvKvError`). + pub(crate) mapper: String, +} + /// Every exported name the generated package declares for the API, keyed by /// the declaration it names. pub(crate) struct GoNames { - /// Free functions by C base symbol. - functions: BTreeMap, + /// Every callable's Go name by C symbol: a free function's package + /// function, a constructor's factory, a static's prefixed function, and + /// a method's `Type.Method` (the spelling a doc link uses). + callables: BTreeMap, + /// Error domains by IDL name. + domains: BTreeMap, /// Error-code types by C constant. codes: BTreeMap, } @@ -157,11 +188,21 @@ impl GoNames { /// package (see the module docs). pub(crate) fn new(model: &Model) -> Self { // Names the runtime declares, then every type-level name. - let mut taken: BTreeSet = ["Error", "DebugLive"].map(String::from).into(); + let mut taken: BTreeSet = RUNTIME_NAMES.iter().map(|s| s.to_string()).collect(); + let mut domains = BTreeMap::new(); + for (_, e) in model.error_domains() { + let iface = pascal(&type_name(&e.name, "Error")); + let names = DomainNames { + unknown: format!("Unknown{iface}"), + mapper: format!("wv{iface}"), + iface, + }; + taken.insert(names.iface.clone()); + taken.insert(names.unknown.clone()); + domains.insert(e.name.clone(), names); + } + let mut callables = BTreeMap::new(); for m in &model.modules { - if let Some(e) = &m.errors { - taken.insert(domain_type(e)); - } for e in &m.enums { let name = pascal(&e.name); for v in &e.variants { @@ -178,10 +219,17 @@ impl GoNames { for i in &m.interfaces { let name = pascal(&i.name); for c in &i.constructors { - taken.insert(constructor(&name, c)); + let factory = constructor(&name, c); + taken.insert(factory.clone()); + callables.insert(c.abi.symbol.clone(), factory); } for f in &i.statics { - taken.insert(format!("{name}{}", pascal(&f.name))); + let go = format!("{name}{}", pascal(&f.name)); + taken.insert(go.clone()); + callables.insert(f.abi.symbol.clone(), go); + } + for f in &i.methods { + callables.insert(f.abi.symbol.clone(), format!("{name}.{}", method(&f.name))); } taken.insert(name); } @@ -189,15 +237,14 @@ impl GoNames { // Error codes: `{Code}Error`, or `{Domain}{Code}Error` past a clash. let mut codes = BTreeMap::new(); - for m in &model.modules { - let Some(e) = &m.errors else { continue }; + for (_, e) in model.error_domains() { + let domain = &domains[&e.name].iface; for c in &e.codes { - let plain = lang_error_name(&pascal(&c.name)); + let plain = pascal(&type_name(&c.name, "Error")); let name = if taken.contains(&plain) { - lang_error_name(&format!( - "{}{}", - domain_type(e).trim_end_matches("Error"), - pascal(&c.name) + pascal(&type_name( + &format!("{}{}", domain.trim_end_matches("Error"), pascal(&c.name)), + "Error", )) } else { plain @@ -213,46 +260,54 @@ impl GoNames { for (_, f) in model.functions() { *seen.entry(pascal(&f.name)).or_default() += 1; } - let functions = model - .functions() - .map(|(m, f)| { - let bare = pascal(&f.name); - let name = if taken.contains(&bare) || seen[&bare] > 1 { - pascal(&format!("{}_{}", m.path, f.name)) - } else { - bare - }; - (f.c_base.clone(), name) - }) - .collect(); - Self { functions, codes } + for (m, f) in model.functions() { + let bare = pascal(&f.name); + let name = if taken.contains(&bare) || seen[&bare] > 1 { + pascal(&format!("{}_{}", m.path, f.name)) + } else { + bare + }; + callables.insert(f.abi.symbol.clone(), name); + } + Self { + callables, + domains, + codes, + } } - /// The Go name of the free function `f`. + /// The Go name of the free function, constructor, or static `f`. pub(crate) fn function(&self, f: &FnBinding) -> &str { - &self.functions[&f.c_base] + &self.callables[&f.abi.symbol] + } + + /// The doc-link spelling of the callable whose C symbol is `symbol` + /// (`NewOp`, `NewGadget`, `Gadget.Describe`). + pub(crate) fn callable(&self, symbol: &str) -> Option<&str> { + self.callables.get(symbol).map(String::as_str) + } + + /// The Go names of the error domain `name`. + pub(crate) fn domain(&self, name: &str) -> &DomainNames { + &self.domains[name] + } + + /// The Go names of the error domain `e`. + pub(crate) fn domain_of(&self, e: &ErrorBinding) -> &DomainNames { + self.domain(&e.name) } /// The Go type name of the error code `c`. pub(crate) fn code(&self, c: &ErrorCodeBinding) -> &str { &self.codes[&c.c_const] } -} -/// `name` with exactly one `Error` suffix. -fn lang_error_name(name: &str) -> String { - if name.ends_with("Error") { - name.to_string() - } else { - format!("{name}Error") + /// The Go type name of the error code whose C constant is `c_const`. + pub(crate) fn code_by_const(&self, c_const: &str) -> Option<&str> { + self.codes.get(c_const).map(String::as_str) } } -/// The Go name of an error domain's interface type (`KvError`). -pub(crate) fn domain_type(e: &ErrorBinding) -> String { - pascal(&e.type_name) -} - /// The factory name of the constructor `c` on the interface `iface` (Go /// spelling): `new` on `Store` is `NewStore`, `open` is `OpenStore`. pub(crate) fn constructor(iface: &str, c: &FnBinding) -> String { @@ -262,6 +317,7 @@ pub(crate) fn constructor(iface: &str, c: &FnBinding) -> String { #[cfg(test)] mod tests { use super::*; + use crate::codegen::test_model; #[test] fn initialisms_keep_one_case() { @@ -278,23 +334,31 @@ mod tests { assert_eq!(camel("ttl_seconds"), "ttlSeconds"); } + #[test] + fn the_default_package_drops_underscores() { + assert_eq!(package("kitchen_sink"), "kitchensink"); + assert_eq!(package("kv"), "kv"); + assert_eq!(package("my_Lib"), "mylib"); + assert_eq!(package("go_"), "go_"); + assert_eq!(package("ra_nge"), "range_"); + } + #[test] fn parameters_avoid_keywords_and_body_names() { - use weaveffi_model::ir::Api; - let yaml = r#" -version: "0.11.0" + let model = test_model( + r#" +version: "0.12.0" modules: - name: m interfaces: - name: Box methods: - - { name: put, params: [{ name: s, type: i32 }, { name: type, type: i32 }] } + - { name: put, params: [{ name: s, type: i32 }, { name: range, type: i32 }] } functions: - { name: plain, params: [{ name: s, type: i32 }, { name: ctx, type: i32 }] } - { name: later, params: [{ name: ctx, type: i32 }], return: i32, async: true } -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let model = Model::assume_valid(&api, weaveffi_model::pkg::Identity::named("p")); +"#, + ); let m = &model.modules[0]; let (put, plain, later) = ( &m.interfaces[0].methods[0], @@ -302,12 +366,12 @@ modules: &m.functions[1], ); assert_eq!(param("s", put), "s_"); - assert_eq!(param("type", put), "type_"); + assert_eq!(param("range", put), "range_"); assert_eq!(param("s", plain), "s"); assert_eq!(param("ctx", plain), "ctx"); assert_eq!(param("ctx", later), "ctx_"); assert_eq!(param("context", plain), "context_"); - assert_eq!(method_param("type"), "type_"); + assert_eq!(method_param("chan"), "chan_"); assert_eq!(method_param("item"), "item"); assert_eq!(slot("err"), "err_"); assert_eq!(error_field("message"), "Message_"); @@ -321,4 +385,41 @@ modules: assert_eq!(method("close_all"), "CloseAll"); assert_eq!(method("dispose"), "Dispose"); } + + #[test] + fn clashing_names_gain_a_qualifier() { + let model = test_model( + r#" +version: "0.12.0" +modules: + - name: kv + errors: + - name: KvErrors + codes: + - { name: Store, code: 1, message: store } + - { name: user_id_missing, code: 2, message: missing } + interfaces: + - name: Store + constructors: [{ name: open, params: [] }] + methods: [{ name: close, params: [] }] + functions: + - { name: open_store, params: [], return: Store } + - { name: check, params: [] } +"#, + ); + let names = GoNames::new(&model); + let d = names.domain("KvErrors"); + assert_eq!( + (d.iface.as_str(), d.unknown.as_str(), d.mapper.as_str()), + ("KvError", "UnknownKvError", "wvKvError") + ); + let e = &model.modules[0].errors[0]; + assert_eq!(names.code(&e.codes[0]), "StoreError"); + assert_eq!(names.code(&e.codes[1]), "UserIDMissingError"); + let f = &model.modules[0].functions; + assert_eq!(names.function(&f[0]), "KvOpenStore"); + assert_eq!(names.function(&f[1]), "KvCheck"); + assert_eq!(names.callable("kv_kv_Store_close"), Some("Store.Close_")); + assert_eq!(names.callable("kv_kv_Store_open"), Some("OpenStore")); + } } diff --git a/crates/weaveffi-cli/src/targets/go/package.rs b/crates/weaveffi-cli/src/targets/go/package.rs index 4ee6bf82..60cd3923 100644 --- a/crates/weaveffi-cli/src/targets/go/package.rs +++ b/crates/weaveffi-cli/src/targets/go/package.rs @@ -42,9 +42,10 @@ fn bundled<'a>( pub(crate) fn render_go_mod(module_path: &str) -> String { let prelude = render_prelude(CommentStyle::DoubleSlash); let trailer = render_trailer(CommentStyle::DoubleSlash, "go.mod"); - // Go 1.23 is required for the standard `iter` package the lazy - // `iter` wrappers return. - format!("{prelude}module {module_path}\n\ngo 1.23\n\n{trailer}") + // Go 1.24 is required for `runtime.AddCleanup`, which releases an + // object wrapper nobody closed (1.23 brought the `iter` package the lazy + // `iter` wrappers return). + format!("{prelude}module {module_path}\n\ngo 1.24\n\n{trailer}") } /// README for the generate-mode output. @@ -89,26 +90,30 @@ local checkout, a `replace`) of `{module_path}`, then import it: import "{module_path}" ``` -Go 1.23 or newer is required. +Go 1.24 or newer is required. ## Usage notes +- Call `Check()` at startup: it returns an error when the linked library + isn't the one these bindings were generated for (a different ABI revision, + or a declaration that's missing or changed). The first call into the + package runs the same checks, and every call after a failed check panics + with that error. - Records are plain structs and rich enums are sealed interfaces; both cross - the boundary by value. + the boundary by value. Optional scalars are pointers, and numeric lists + pass the slice's own storage. - Interfaces are reference-counted objects. Call `Close` when you're done - with one (a finalizer releases it otherwise); it's safe to call twice or - while a call is in flight. -- A call that declares errors returns `error` values you match with - `errors.As`; any other call panics with an `*Error` when the library - reports a failure, since that's a bug. + with one (an unreachable wrapper is released eventually otherwise); it's + safe to call twice or while a call is in flight. +- A function that declares an error domain returns that domain's code types, + which you match with `errors.As`; one that fails with any error returns an + `*Error`. Any other call panics with an `*Error` when the library reports + a failure, since that's a bug. Async functions always return the error. - Async functions take a `context.Context` first. Cancellable ones cancel the native call when the context is done. - Callback interfaces are Go interfaces you implement. The library may call them from any thread; a panic is reported to the native caller as a callback failure instead of crashing. -- Importing the package checks the library's ABI revision and every - module's contract table, and panics naming the first declaration that's - missing from the library or changed since the bindings were generated. {trailer}"# ) diff --git a/crates/weaveffi-cli/src/targets/go/runtime.rs b/crates/weaveffi-cli/src/targets/go/runtime.rs index ce69849c..36a9a03c 100644 --- a/crates/weaveffi-cli/src/targets/go/runtime.rs +++ b/crates/weaveffi-cli/src/targets/go/runtime.rs @@ -3,14 +3,16 @@ //! The sources live in `runtime/` as ordinary Go files and are spliced into //! the output with `{{PLACEHOLDER}}` substitution: //! -//! * `runtime.go`: the load-time ABI and contract checks, the generic -//! `Error` type, error-slot, callback, string, and byte-run helpers, the -//! object reference guard (`wvRef`), the async completion bridge, and -//! `DebugLive`. +//! * `runtime.go`: `Check` and the load-time ABI and contract checks, the +//! generic `Error` type, error-slot, callback, string, byte-run, +//! optional-scalar, and typed-array helpers, the object wrapper core +//! (`wvObject`), the generic iterator sequences, the async completion +//! bridge, and `DebugLive`. //! * `codec.go`: the value-buffer writer and reader and the generic list, //! map, and optional codecs the generated pairs are built from. -use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use crate::targets::go::go_prelude; +use crate::utils::{render_trailer, CommentStyle}; use weaveffi_model::model::ABI_VERSION; const RUNTIME_GO: &str = include_str!("runtime/runtime.go"); @@ -34,7 +36,7 @@ fn splice(template: &str, names: &RuntimeNames, file: &str) -> String { .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()); format!( "{}{body}\n{}", - render_prelude(CommentStyle::DoubleSlash), + go_prelude(), render_trailer(CommentStyle::DoubleSlash, file) ) } diff --git a/crates/weaveffi-cli/src/targets/go/runtime/runtime.go b/crates/weaveffi-cli/src/targets/go/runtime/runtime.go index 6a532ce5..199582c8 100644 --- a/crates/weaveffi-cli/src/targets/go/runtime/runtime.go +++ b/crates/weaveffi-cli/src/targets/go/runtime/runtime.go @@ -1,7 +1,6 @@ package {{PACKAGE}} /* -#include #include "{{HEADER}}" static void* wvRuntimeHandlePtr(uintptr_t h) { return (void*)h; } @@ -13,8 +12,12 @@ import ( "context" "errors" "fmt" + "iter" + "reflect" + "runtime" "runtime/cgo" "slices" + "sync" "sync/atomic" "unsafe" ) @@ -24,15 +27,18 @@ const wvABIVersion uint32 = {{ABI_VERSION}} // The runtime error codes this package produces or interprets itself. const ( + wvCodeGeneric int32 = -1 + wvCodeMarshal int32 = -3 wvCodeForeign int32 = -4 wvCodeCancelled int32 = -5 ) -// Error is a failure no error domain claims: a generic failure (code -1), a -// panic in the native library (-2), an argument or result that couldn't be -// marshalled (-3), a callback implementation that failed (-4), or a code -// outside the domain. A call that doesn't declare errors panics with an -// *Error, since its failure is a bug; a call that does returns one. +// Error is a failure no error domain claims: a generic failure (code -1), +// including every failure of a function that fails with any error; a panic +// in the native library (-2); an argument or result that couldn't be +// marshalled (-3); or a callback implementation that failed (-4). A +// function that declares no errors panics with an *Error, since its failure +// is a bug; any other returns one. type Error struct { // Code is the runtime code. Code int32 @@ -55,12 +61,37 @@ func DebugLive(kind int32) uint64 { // ── Load-time checks ── -// wvCheckABI panics when the linked library implements a different C ABI +// wvLoad runs the load-time checks once, on first use. +var wvLoad = sync.OnceValue(wvCheckLibrary) + +// Check reports whether the linked native library is the one these bindings +// were generated for: it must implement the same C ABI revision, and its +// contract tables must hold every declaration the bindings use, unchanged. +// Declarations only the library has are fine. +// +// The checks run once, on the first call to Check or to any function of the +// package, and Check returns their result from then on. Call it at startup +// to handle a mismatched library as an error: a function called after a +// failed check panics with the error Check returns. +func Check() error { + return wvLoad() +} + +// wvLoaded runs the load-time checks if they haven't run yet, and panics +// with their error when they failed. +func wvLoaded() { + if err := wvLoad(); err != nil { + panic(err) + } +} + +// wvCheckABI fails when the linked library implements a different C ABI // revision than these bindings were generated for. -func wvCheckABI() { +func wvCheckABI() error { if found := uint32(C.{{PREFIX}}_abi_version()); found != wvABIVersion { - panic(fmt.Errorf("{{PACKAGE}}: the linked library implements C ABI revision %d, but these bindings were generated for revision %d", found, wvABIVersion)) + return fmt.Errorf("{{PACKAGE}}: the linked library implements C ABI revision %d, but these bindings were generated for revision %d", found, wvABIVersion) } + return nil } // wvContractEntry is one declaration these bindings were generated with: @@ -70,22 +101,23 @@ type wvContractEntry struct { path string } -// wvCheckContract panics unless every entry in want is in the library's +// wvCheckContract fails unless every entry in want is in the library's // contract table for one top-level module (n entries sorted by id) with an -// equal signature hash. Entries only the library has are fine. -func wvCheckContract(table *C.{{PREFIX}}_contract_entry, n C.size_t, want []wvContractEntry) { +// equal signature hash. +func wvCheckContract(table *C.{{PREFIX}}_contract_entry, n C.size_t, want []wvContractEntry) error { have := unsafe.Slice(table, int(n)) for _, w := range want { i, found := slices.BinarySearchFunc(have, w.id, func(e C.{{PREFIX}}_contract_entry, id uint64) int { return cmp.Compare(uint64(e.id), id) }) if !found { - panic(fmt.Errorf("{{PACKAGE}}: %s is missing from the library", w.path)) + return fmt.Errorf("{{PACKAGE}}: %s is missing from the library", w.path) } if uint64(have[i].hash) != w.hash { - panic(fmt.Errorf("{{PACKAGE}}: %s changed since these bindings were generated", w.path)) + return fmt.Errorf("{{PACKAGE}}: %s changed since these bindings were generated", w.path) } } + return nil } // ── Errors ── @@ -103,9 +135,9 @@ func (f wvFailure) err() error { } func wvCopyFailure(cErr *C.{{PREFIX}}_error) wvFailure { - f := wvFailure{code: int32(cErr.code)} - if cErr.message != nil { - f.message = C.GoString(cErr.message) + f := wvFailure{ + code: int32(cErr.code), + message: wvBorrowString(cErr.message_ptr, cErr.message_len), } if cErr.payload_ptr != nil { f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) @@ -138,6 +170,12 @@ func wvTrap(cErr *C.{{PREFIX}}_error) { } } +// wvCodeMessage is the message of a failure in the error domain named +// domain that the library reported without one. +func wvCodeMessage(domain string, code int32) string { + return fmt.Sprintf("%s code %d", domain, code) +} + // wvDomainError is implemented by every error-code type: its code and its // fields encoded as a value buffer (nil when it has none). type wvDomainError interface { @@ -149,18 +187,23 @@ type wvDomainError interface { // wvSetError reports a failure through a callback's error slot. The native // library copies the message and payload, so both are only borrowed. func wvSetError(outErr *C.{{PREFIX}}_error, code int32, message string, payload []byte) { - msg := C.CString(message) - defer C.free(unsafe.Pointer(msg)) - C.{{PREFIX}}_error_set(outErr, C.int32_t(code), msg) + ptr, n := wvStr(message) + C.{{PREFIX}}_error_set(outErr, C.int32_t(code), ptr, n) if len(payload) > 0 { ptr, n := wvBytes(payload) C.{{PREFIX}}_error_set_payload(outErr, ptr, n) } } -// wvCallbackFailed reports the error a throwing callback method returned: -// an error of the domain D in scope for the method reports its code and -// payload, and anything else is a callback failure (code -4). +// wvCallbackError reports an error a callback method returned as a generic +// failure (code -1) with the error's text. +func wvCallbackError(outErr *C.{{PREFIX}}_error, err error) { + wvSetError(outErr, wvCodeGeneric, err.Error(), nil) +} + +// wvCallbackFailed reports the error a callback method of the domain D +// returned: an error of D reports its code, message, and payload, and +// anything else is a generic failure (see wvCallbackError). func wvCallbackFailed[D error](outErr *C.{{PREFIX}}_error, err error) { var domain D if errors.As(err, &domain) { @@ -169,7 +212,7 @@ func wvCallbackFailed[D error](outErr *C.{{PREFIX}}_error, err error) { return } } - wvSetError(outErr, wvCodeForeign, err.Error(), nil) + wvCallbackError(outErr, err) } // wvRecoverCallback, deferred by every callback trampoline, reports a panic @@ -187,17 +230,43 @@ func wvRecoverCallback(outErr *C.{{PREFIX}}_error) { // ── Callback contexts ── +// wvIsNil reports whether a callback implementation is nil: a nil interface, +// or one holding a nil pointer, map, slice, func, or channel (a typed nil, +// which would otherwise reach the native library as an implementation whose +// every call fails). +func wvIsNil(impl any) bool { + if impl == nil { + return true + } + switch v := reflect.ValueOf(impl); v.Kind() { + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan, reflect.Interface: + return v.IsNil() + } + return false +} + // wvNewCallback keeps impl alive in a handle table for as long as the // native library holds it and returns the handle as the callback's context // pointer; the vtable's free entry deletes it (see wvFreeCallback). A nil -// implementation of a required callback panics. -func wvNewCallback(impl any) unsafe.Pointer { - if impl == nil { - panic("{{PACKAGE}}: nil callback implementation") +// implementation (see wvIsNil) of the required callback interface iface +// panics. +func wvNewCallback(impl any, iface string) unsafe.Pointer { + if wvIsNil(impl) { + panic("{{PACKAGE}}: nil " + iface + " implementation") } return C.wvRuntimeHandlePtr(C.uintptr_t(cgo.NewHandle(impl))) } +// wvOptionalCallback is wvNewCallback for an optional callback parameter: +// a nil implementation (including a typed nil) passes no callback, a null +// context and vtable. +func wvOptionalCallback[V any](impl any, vtable *V) (unsafe.Pointer, *V) { + if wvIsNil(impl) { + return nil, nil + } + return C.wvRuntimeHandlePtr(C.uintptr_t(cgo.NewHandle(impl))), vtable +} + // wvCallback recovers the implementation behind a callback context. func wvCallback[T any](ctx unsafe.Pointer) T { return cgo.Handle(uintptr(ctx)).Value().(T) @@ -223,9 +292,10 @@ func wvBytes(b []byte) (*C.uint8_t, C.size_t) { return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) } -// wvBorrowString copies a borrowed UTF-8 run into a Go string. +// wvBorrowString copies a borrowed UTF-8 run into a Go string. A run of +// length 0 is never read. func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { - if ptr == nil { + if n == 0 { return "" } return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) @@ -239,13 +309,10 @@ func wvTakeString(ptr *C.uint8_t, n C.size_t) string { return s } -// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer -// copies to an empty slice. +// wvBorrowBytes copies a borrowed byte run into Go memory. A run of length +// 0 copies to an empty slice. func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { - if ptr == nil { - return []byte{} - } - return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) + return wvBorrowSlice[byte](ptr, n) } // wvTakeBytes copies a returned byte run into Go memory and releases the @@ -260,31 +327,85 @@ func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { // allocator and writes it to a callback's return slots; the library adopts // the run. func wvHandOverString(s string, outPtr **C.uint8_t, outLen *C.size_t) { - ptr, n := wvStr(s) - wvHandOver(ptr, n, outPtr, outLen) + wvHandOverSlice(unsafe.Slice(unsafe.StringData(s), len(s)), outPtr, outLen) } // wvHandOverBytes is wvHandOverString for a byte slice (raw bytes or an // encoded value buffer). func wvHandOverBytes(b []byte, outPtr **C.uint8_t, outLen *C.size_t) { - ptr, n := wvBytes(b) - wvHandOver(ptr, n, outPtr, outLen) + wvHandOverSlice(b, outPtr, outLen) +} + +// ── Optional scalars and typed arrays ── + +// wvPresent splits an optional scalar into the presence flag and value a +// call passes: (false, zero) for nil. +func wvPresent[T any](v *T) (bool, T) { + if v == nil { + var zero T + return false, zero + } + return true, *v +} + +// wvOptional joins a presence flag and value the library passed into an +// optional scalar: nil when absent. +func wvOptional[T any](present bool, v T) *T { + if !present { + return nil + } + return &v } -func wvHandOver(ptr *C.uint8_t, n C.size_t, outPtr **C.uint8_t, outLen *C.size_t) { - run := C.{{PREFIX}}_alloc(n) +// wvSliceIn returns a borrowed (pointer, count) view of s as an array of +// the C element type E, which has T's size and layout, for the duration of +// one call. The elements hold no Go pointers, and cgo keeps the memory in +// place until the call returns. +func wvSliceIn[E, T any](s []T) (*E, C.size_t) { + return (*E)(unsafe.Pointer(unsafe.SliceData(s))), C.size_t(len(s)) +} + +// wvBorrowSlice copies a borrowed array of n elements into a new slice. An +// array of length 0 is never read. +func wvBorrowSlice[T, E any](ptr *E, n C.size_t) []T { + out := make([]T, int(n)) if n > 0 { - copy(unsafe.Slice((*byte)(unsafe.Pointer(run)), n), unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) + copy(out, unsafe.Slice((*T)(unsafe.Pointer(ptr)), int(n))) + } + return out +} + +// wvTakeSlice copies a returned array of n elements into a new slice and +// releases the native allocation (n times the element size). +func wvTakeSlice[T, E any](ptr *E, n C.size_t) []T { + out := wvBorrowSlice[T](ptr, n) + var elem E + C.{{PREFIX}}_free_bytes((*C.uint8_t)(unsafe.Pointer(ptr)), n*C.size_t(unsafe.Sizeof(elem))) + return out +} + +// wvHandOverSlice copies v into a run allocated with the native library's +// allocator and writes it and its element count to a callback's return +// slots; the library adopts the run. +func wvHandOverSlice[T, E any](v []T, outPtr **E, outLen *C.size_t) { + var elem E + size := C.size_t(len(v)) * C.size_t(unsafe.Sizeof(elem)) + run := C.{{PREFIX}}_alloc(size) + if size > 0 { + if run == nil { + panic("{{PACKAGE}}: the native library couldn't allocate a callback result") + } + copy(unsafe.Slice((*T)(unsafe.Pointer(run)), len(v)), v) } - *outPtr, *outLen = run, n + *outPtr, *outLen = (*E)(unsafe.Pointer(run)), C.size_t(len(v)) } // ── Objects ── -// wvRef owns one strong reference to a native object on behalf of a wrapper. -// Calls borrow the pointer between acquire and release. Close may race them: -// the reference is destroyed exactly once, by close when no call is in -// flight, or else by the last in-flight call to finish. +// wvRef owns one strong reference to a native object. Calls borrow the +// pointer between acquire and release. Close may race them: the reference +// is destroyed exactly once, by close when no call is in flight, or else by +// the last in-flight call to finish. type wvRef struct { ptr unsafe.Pointer destroy func(unsafe.Pointer) @@ -293,11 +414,6 @@ type wvRef struct { state atomic.Uint64 } -func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { - r.ptr = ptr - r.destroy = destroy -} - // acquire borrows the pointer for one call, panicking when the wrapper was // already closed. func (r *wvRef) acquire(typeName string) unsafe.Pointer { @@ -319,8 +435,8 @@ func (r *wvRef) release() { } } -// close releases the wrapper's reference once no call is in flight. Calling -// it again is a no-op. +// close releases the reference once no call is in flight. Calling it again +// is a no-op. func (r *wvRef) close() { for { s := r.state.Load() @@ -336,6 +452,102 @@ func (r *wvRef) close() { } } +// wvObject is embedded in every object wrapper: it owns the wrapper's strong +// reference, and a cleanup releases the reference when the wrapper becomes +// unreachable without being closed. +type wvObject struct { + ref *wvRef + cleanup runtime.Cleanup +} + +// adopt takes over one owned strong reference to ptr, released by destroy. +func (o *wvObject) adopt(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + o.ref = &wvRef{ptr: ptr, destroy: destroy} + o.cleanup = runtime.AddCleanup(o, (*wvRef).close, o.ref) +} + +// acquire borrows the object's pointer for one call; pair it with a deferred +// release. +func (o *wvObject) acquire(typeName string) unsafe.Pointer { + if o.ref == nil { + panic("{{PACKAGE}}: " + typeName + " used before it was created by this package") + } + return o.ref.acquire(typeName) +} + +// release ends one call's borrow. +func (o *wvObject) release() { + o.ref.release() +} + +// Close releases the wrapper's strong reference and always returns nil. It's +// idempotent and safe to call from any goroutine, even while a call on the +// wrapper is in flight: the reference is then released when that call +// returns. The object itself is dropped once its last reference, from any +// wrapper, record, or the native library, is gone. A wrapper that's never +// closed is released some time after it becomes unreachable. +func (o *wvObject) Close() error { + if o.ref != nil { + o.cleanup.Stop() + o.ref.close() + } + return nil +} + +// ── Iterators ── + +// wvCursor is one launched native iterator: next pulls the next element, +// reporting false at the end, and destroy releases the iterator. +type wvCursor[T any] struct { + next func(cErr *C.{{PREFIX}}_error) (T, bool) + destroy func() +} + +// wvSeq is the sequence over a native iterator of a function that declares +// no errors: each range launches the iterator, pulls one element per step, +// and destroys it when the range ends, early or not. A failure panics with +// an *Error. +func wvSeq[T any](launch func(cErr *C.{{PREFIX}}_error) wvCursor[T]) iter.Seq[T] { + return func(yield func(T) bool) { + var cErr C.{{PREFIX}}_error + c := launch(&cErr) + wvTrap(&cErr) + defer c.destroy() + for { + v, more := c.next(&cErr) + wvTrap(&cErr) + if !more || !yield(v) { + return + } + } + } +} + +// wvSeq2 is wvSeq for a function that declares errors: a failure, mapped by +// mapErr, is yielded as a final (zero value, error) pair. +func wvSeq2[T any](launch func(cErr *C.{{PREFIX}}_error) wvCursor[T], mapErr func(wvFailure) error) iter.Seq2[T, error] { + return func(yield func(T, error) bool) { + var zero T + var cErr C.{{PREFIX}}_error + c := launch(&cErr) + if cErr.code != 0 { + yield(zero, mapErr(wvTakeError(&cErr))) + return + } + defer c.destroy() + for { + v, more := c.next(&cErr) + if cErr.code != 0 { + yield(zero, mapErr(wvTakeError(&cErr))) + return + } + if !more || !yield(v, nil) { + return + } + } + } +} + // ── Async ── // wvOutcome is one async completion: a converted result, or the failure the @@ -382,36 +594,40 @@ func wvSettle[T any](cErr *C.{{PREFIX}}_error, convert func() T) (o wvOutcome[T] } defer func() { if r := recover(); r != nil { - o = wvOutcome[T]{fail: &wvFailure{code: -3, message: fmt.Sprint(r)}} + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} } }() o.val = convert() return o } -// wait blocks until the call completes or ctx is done. With a cancel token, -// ctx's cancellation cancels the native call and waits for its completion, -// which the native library delivers promptly; without one, the call is -// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) -// returns ctx.Err(), or context.Canceled when ctx itself isn't done. -func (c *wvAsyncCall[T]) wait(ctx context.Context, token *C.{{PREFIX}}_cancel_token) (T, *wvFailure, error) { +// wvAwait blocks until call completes or ctx is done, and never panics. With +// a cancel token, ctx's cancellation cancels the native call and waits for +// its completion, which the native library delivers promptly; without one, +// the call is abandoned and ctx.Err() returned at once. A cancelled +// completion (code -5) returns ctx.Err(), or context.Canceled when ctx +// itself isn't done; any other failure is mapped by mapErr. +func wvAwait[T any](ctx context.Context, call *wvAsyncCall[T], token *C.{{PREFIX}}_cancel_token, mapErr func(wvFailure) error) (T, error) { var o wvOutcome[T] select { - case o = <-c.done: + case o = <-call.done: case <-ctx.Done(): if token == nil { var zero T - return zero, nil, ctx.Err() + return zero, ctx.Err() } C.{{PREFIX}}_cancel_token_cancel(token) - o = <-c.done + o = <-call.done } - if o.fail != nil && o.fail.code == wvCodeCancelled { - var zero T + if o.fail == nil { + return o.val, nil + } + var zero T + if o.fail.code == wvCodeCancelled { if err := ctx.Err(); err != nil { - return zero, nil, err + return zero, err } - return zero, nil, context.Canceled + return zero, context.Canceled } - return o.val, o.fail, nil + return zero, mapErr(*o.fail) } diff --git a/crates/weaveffi-cli/src/targets/go/tests.rs b/crates/weaveffi-cli/src/targets/go/tests.rs index 004231e8..20124c04 100644 --- a/crates/weaveffi-cli/src/targets/go/tests.rs +++ b/crates/weaveffi-cli/src/targets/go/tests.rs @@ -1,96 +1,49 @@ -//! Unit tests: render a small API that exercises every C ABI revision 4 -//! shape the Go bindings distinguish and assert the key pieces of each -//! contract. +//! Target-specific tests: names derived from the identity and the config, +//! escaping, and the generated-file markers. The `kitchen_sink` snapshot pins +//! the rendered bindings themselves. -use weaveffi_model::contract::entries; -use weaveffi_model::ir::Api; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; use weaveffi_model::validate::validate; use super::{render_files, GoConfig}; -/// One module with an error domain carrying a field; a C-style enum; a -/// record carrying objects; an interface with a `new` constructor, a -/// method, and a throwing static; nullable objects; an iterator of objects; -/// a cancellable async function; a composite map; and a callback interface -/// whose methods take every argument family and return a direct value, an -/// enum, a string, an optional record, an object, and an optional object, -/// one of them `throws`, passed both required and optional. const FIXTURE: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: shop errors: - name: ShopError - codes: - - { name: OutOfStock, code: 1, message: "out of stock", fields: [{ name: sku, type: string }] } - - { name: Closed, code: 2, message: "closed" } - enums: - - name: Mood - variants: [{ name: Happy, value: 0 }, { name: Grumpy, value: 1 }] - structs: - - name: Order - fields: - - { name: order_id, type: u64 } - - { name: cart, type: Cart } - - { name: history, type: "[Cart]" } - - { name: note, type: "string?" } + - name: ShopErrors + codes: + - { name: out_of_stock, code: 1, message: "out of stock", fields: [{ name: message, type: string }] } interfaces: - name: Cart - constructors: - - { name: new, params: [{ name: owner, type: string }] } + constructors: [{ name: new, params: [{ name: range, type: string }] }] methods: - - { name: add, params: [{ name: sku, type: string }], return: bool } - statics: - - { name: restore, params: [{ name: id, type: i64 }], return: Cart, throws: true } - callback_interfaces: - - name: Watcher - methods: - - name: on_event - params: - - { name: name, type: string } - - { name: count, type: i32 } - - { name: order, type: Order } - - { name: cart, type: Cart } - return: bool - - { name: mood, return: Mood } - - { name: label, return: string, throws: true } - - { name: latest, return: "Order?" } - - { name: favorite, return: Cart } - - { name: maybe, return: "Cart?" } - - { name: on_done } + - { name: close, params: [] } + - { name: add, params: [{ name: s, type: i32 }] } functions: - - { name: find_cart, params: [{ name: current, type: "Cart?" }], return: "Cart?" } - - { name: all_carts, return: "iter" } - - { name: watch, params: [{ name: watcher, type: Watcher }] } - - { name: maybe_watch, params: [{ name: watcher, type: "Watcher?" }] } - - { name: echo, params: [{ name: text, type: string }], return: string } - - { name: tally, params: [{ name: counts, type: "{string:[i32?]}" }], return: "[Order]", throws: true } - - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } + - { name: open_cart, params: [], return: Cart } + - { name: check, params: [{ name: context, type: i32 }], throws: ShopErrors } "#; fn model() -> Model { - let api: Api = serde_yaml::from_str(FIXTURE).unwrap(); + let api = weaveffi_model::parse::parse_api_str(FIXTURE, "yaml").unwrap(); validate(&api, &Identity::named("shop_kit"), None).unwrap() } -fn files() -> Vec<(String, String)> { - render_files(&model(), &GoConfig::default()) +fn render(config: GoConfig) -> Vec<(String, String)> { + render_files(&model(), &config) } -fn file(name: &str) -> String { - files() - .into_iter() +fn file<'a>(files: &'a [(String, String)], name: &str) -> &'a str { + files + .iter() .find(|(n, _)| n == name) - .map(|(_, c)| c) + .map(|(_, c)| c.as_str()) .unwrap_or_else(|| panic!("no {name}")) } -fn bindings() -> String { - file("bindings.go") -} - #[track_caller] fn assert_has(out: &str, needle: &str) { assert!(out.contains(needle), "missing {needle:?} in:\n{out}"); @@ -98,7 +51,7 @@ fn assert_has(out: &str, needle: &str) { #[test] fn names_come_from_the_identity() { - let files = files(); + let files = render(GoConfig::default()); let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect(); assert_eq!( names, @@ -111,264 +64,80 @@ fn names_come_from_the_identity() { "codec.go" ] ); - assert_has(&files[0].1, "module shop_kit\n"); - let src = &files[3].1; + assert_has(file(&files, "go.mod"), "module shop_kit\n\ngo 1.24\n"); + let src = file(&files, "bindings.go"); assert_has( src, - "// Package shop_kit binds the shop_kit native library.\npackage shop_kit\n", + "// Package shopkit binds the shop_kit native library.\n", ); + assert_has(src, "\npackage shopkit\n"); assert_has(src, "#cgo LDFLAGS: -lshop_kit\n"); assert_has(src, "#include \"shop_kit.h\"\n"); - for (name, contents) in &files { - if name.ends_with(".go") || name == "go.mod" { - let body: String = contents.lines().skip(3).collect::>().join("\n"); - assert!( - !body.to_lowercase().contains("weaveffi"), - "{name} is branded:\n{body}" - ); - assert!(!body.contains("{{"), "{name} has a placeholder:\n{body}"); - } - } - - let custom = render_files( - &model(), - &GoConfig { - name: Some("example.com/shop".into()), - }, - ); - assert_has(&custom[0].1, "module example.com/shop\n"); + assert_has(file(&files, "runtime.go"), "\npackage shopkit\n"); + assert_has(file(&files, "codec.go"), "\npackage shopkit\n"); } #[test] -fn load_checks_embed_every_contract_entry() { - let model = model(); - let src = bindings(); - assert_has( - &src, - "func init() {\n\twvCheckABI()\n\tvar n C.size_t\n\ttable := C.shop_kit_shop_contract(&n)\n\twvCheckContract(table, n, []wvContractEntry{\n", - ); - let root = model.roots().next().unwrap(); - for e in entries(&model, root) { - assert_has( - &src, - &format!( - "\t\t{{{:#018x}, {:#018x}, \"{}\"}},\n", - e.id, e.hash, e.path - ), - ); - } - let rt = file("runtime.go"); - assert_has(&rt, "const wvABIVersion uint32 = 4\n"); - assert_has(&rt, "%s is missing from the library"); - assert_has(&rt, "%s changed since these bindings were generated"); - for f in files() { - assert!(!f.1.contains("checksum"), "{} mentions a checksum", f.0); - assert!(!f.1.contains("_dealloc"), "{} mentions dealloc", f.0); - } +fn the_config_names_the_module_and_the_package() { + let files = render(GoConfig { + name: Some("example.com/shop".into()), + package: Some("shop".into()), + }); + assert_has(file(&files, "go.mod"), "module example.com/shop\n"); + assert_has(file(&files, "bindings.go"), "\npackage shop\n"); + assert_has(file(&files, "README.md"), "import \"example.com/shop\""); + + // A keyword is escaped, and blank values fall back to the defaults. + let files = render(GoConfig { + name: Some(" ".into()), + package: Some("select".into()), + }); + assert_has(file(&files, "go.mod"), "module shop_kit\n"); + assert_has(file(&files, "bindings.go"), "\npackage select_\n"); } #[test] -fn vtables_carry_the_header_and_every_method() { - let src = bindings(); - assert_has( - &src, - "static const shop_kit_shop_Watcher_vtable wvVtable_shop_kit_shop_Watcher_vtable = {\n\t.size = sizeof(shop_kit_shop_Watcher_vtable),\n\t.flags = 0,\n\t.free = goWv_shop_kit_shop_Watcher_free,\n", - ); - assert_has( - &src, - "\t.on_event = (bool (*)(void*, const uint8_t*, size_t, int32_t, const uint8_t*, size_t, shop_kit_shop_Cart*, shop_kit_error*))goWv_shop_kit_shop_Watcher_on_event,\n", - ); - assert_has(&src, "\t.label = goWv_shop_kit_shop_Watcher_label,\n"); - assert_has( - &src, - "extern void goWv_shop_kit_shop_Watcher_label(void* ctx, uint8_t** out_ptr, size_t* out_len, shop_kit_error* out_err);", - ); - assert_has( - &src, - "extern shop_kit_shop_Cart* goWv_shop_kit_shop_Watcher_favorite(void* ctx, shop_kit_error* out_err);", - ); -} - -#[test] -fn callback_methods_return_every_family() { - let src = bindings(); - assert_has( - &src, - "\tOnEvent(name string, count int32, order Order, cart *Cart) bool\n", - ); - assert_has(&src, "\tMood() Mood\n"); - assert_has(&src, "\tLabel() (string, error)\n"); - assert_has(&src, "\tLatest() *Order\n"); - assert_has(&src, "\tFavorite() *Cart\n"); - assert_has(&src, "\tMaybe() *Cart\n"); - assert_has(&src, "\tOnDone()\n"); - - // Arguments: borrowed runs are copied or decoded; an object is adopted. - assert_has( - &src, - "ret := wvCallback[Watcher](ctx).OnEvent(wvBorrowString(name_ptr, name_len), int32(count), wvDecodeBorrowed(order_ptr, order_len, wvReadOrder), wvAdoptCart(cart))\n\treturn C._Bool(ret)\n", - ); - // Direct values return by value, objects as a fresh reference, and runs - // through the out slots as a library allocation. - assert_has(&src, "(cRet C.shop_kit_shop_Mood) {"); - assert_has(&src, "return C.shop_kit_shop_Mood(ret)"); - assert_has(&src, "wvHandOverString(ret, out_ptr, out_len)"); - assert_has( - &src, - "wvHandOverBytes(wvEncode(ret, wvWriteOptOrder), out_ptr, out_len)", - ); - assert_has(&src, "(cRet *C.shop_kit_shop_Cart) {"); - assert_has(&src, "return ret.share()"); - let rt = file("runtime.go"); - assert_has(&rt, "run := C.shop_kit_alloc(n)"); - assert_has(&src, "defer wvRecoverCallback(out_err)"); -} - -#[test] -fn throwing_callbacks_report_domain_codes_and_payloads() { - let src = bindings(); - assert_has( - &src, - "ret, err := wvCallback[Watcher](ctx).Label()\n\tif err != nil {\n\t\twvCallbackFailed[ShopError](out_err, err)\n\t\treturn\n\t}\n", - ); - assert_has( - &src, - "func (e *OutOfStockError) wvPayload() []byte {\n\tw := &wvWriter{}\n\tw.writeString(e.Sku)\n\treturn w.buf\n}", - ); - let rt = file("runtime.go"); - assert_has(&rt, "C.shop_kit_error_set_payload(outErr, ptr, n)"); - assert_has(&rt, "wvSetError(outErr, wvCodeForeign, err.Error(), nil)"); -} - -#[test] -fn callback_parameters_pass_a_handle_and_the_static_vtable() { - let src = bindings(); - assert_has( - &src, - "C.shop_kit_shop_watch(wvNewCallback(watcher), C.wvVtablePtr_shop_kit_shop_Watcher_vtable(), &cErr)", - ); - // An optional callback passes a null vtable for nil. - assert_has( - &src, - "\tvar cWatcherCtx unsafe.Pointer\n\tvar cWatcherVtable *C.shop_kit_shop_Watcher_vtable\n\tif watcher != nil {\n\t\tcWatcherCtx, cWatcherVtable = wvNewCallback(watcher), C.wvVtablePtr_shop_kit_shop_Watcher_vtable()\n\t}\n", - ); - assert_has( - &src, - "func goWv_shop_kit_shop_Watcher_free(ctx unsafe.Pointer) {\n\twvFreeCallback(ctx)\n}", - ); -} - -#[test] -fn errors_map_to_code_types() { - let src = bindings(); - assert_has( - &src, - "type ShopError interface {\n\terror\n\t// Code returns the error's numeric code.\n\tCode() int32\n\tisShopError()\n}", - ); - assert_has(&src, "type OutOfStockError struct {\n\tSku string\n"); - assert_has(&src, "func (*ClosedError) Code() int32 {\n\treturn 2\n}"); - assert_has(&src, "func (*ClosedError) isShopError() {}"); - assert_has(&src, "func wvShopError(f wvFailure) error {"); - assert_has(&src, "\t\te.Sku = r.readString()\n"); - // A throwing call returns the mapped error; any other call traps. - assert_has(&src, "func CartRestore(id int64) (*Cart, error) {"); - assert_has(&src, "return nil, wvShopError(wvTakeError(&cErr))"); - assert_has(&src, "func Echo(text string) string {"); - assert_has( - &src, - "\twvTrap(&cErr)\n\treturn wvTakeString(cRet, cRetLen)\n", - ); -} - -#[test] -fn composite_codecs_are_rendered_once_per_type() { - let src = bindings(); - for stem in [ - "MapStringListOptI32", - "ListOptI32", - "OptI32", - "ListOrder", - "ListCart", - "OptString", - "OptOrder", - ] { - let def = format!("func wvWrite{stem}(w *wvWriter"); - assert_eq!(src.matches(&def).count(), 1, "{def}"); - assert_eq!( - src.matches(&format!("func wvRead{stem}(r *wvReader)")) - .count(), - 1, - "{stem}" +fn every_go_file_is_marked_generated() { + for (name, contents) in render(GoConfig::default()) { + if !name.ends_with(".go") { + continue; + } + let marker = contents + .lines() + .position(|l| l == "// Code generated by WeaveFFI. DO NOT EDIT.") + .unwrap_or_else(|| panic!("{name} has no generated-code marker")); + let package = contents + .lines() + .position(|l| l.starts_with("package ")) + .unwrap(); + assert!( + marker < package, + "{name}: the marker must precede the package clause" ); + assert!(!contents.contains("{{"), "{name} has a placeholder"); } - assert_has( - &src, - "func wvWriteMapStringListOptI32(w *wvWriter, v map[string][]*int32) {\n\twvWriteMap(w, v, (*wvWriter).writeString, wvWriteListOptI32)\n}", - ); - assert_has( - &src, - "func wvReadOptOrder(r *wvReader) *Order {\n\treturn wvReadOptional(r, wvReadOrder)\n}", - ); - assert_has( - &src, - "cCountsPtr, cCountsLen := wvBytes(wvEncode(counts, wvWriteMapStringListOptI32))", - ); - assert_has(&src, "return wvDecode(cRet, cRetLen, wvReadListOrder), nil"); - // Records read and write their fields through the shared pairs. - assert_has(&src, "\tOrderID uint64\n"); - assert_has( - &src, - "\twvWriteListCart(w, v.History)\n\twvWriteOptString(w, v.Note)\n", - ); -} - -#[test] -fn objects_cross_as_borrowed_pointers_and_fresh_tokens() { - let src = bindings(); - assert_has( - &src, - "\tvar cCurrent *C.shop_kit_shop_Cart\n\tif current != nil {\n\t\tcCurrent = current.native()\n\t\tdefer current.ref.release()\n\t}\n", - ); - assert_has(&src, "return wvAdoptCart(cRet)"); - assert_has( - &src, - "func wvWriteCart(w *wvWriter, v *Cart) {\n\tw.writeU64(uint64(uintptr(unsafe.Pointer(v.share()))))\n}", - ); - assert_has(&src, "return C.shop_kit_shop_Cart_clone(ptr)"); - assert_has(&src, "runtime.SetFinalizer(s, (*Cart).Close)"); -} - -#[test] -fn async_calls_block_on_a_context() { - let src = bindings(); - assert_has( - &src, - "func Wait(ctx context.Context, ms int64) (int64, error) {", - ); - assert_has(&src, "token := C.shop_kit_cancel_token_create()"); - assert_has(&src, "defer C.shop_kit_cancel_token_destroy(token)"); - assert_has(&src, "call := wvNewAsyncCall[int64]()"); - assert_has(&src, "res, fail, err := call.wait(ctx, token)"); - assert_has( - &src, - "wvComplete(context_, err_, func() int64 {\n\t\treturn int64(result)\n\t})", - ); - // A non-throwing async failure is a bug: it panics. - assert_has(&src, "\t\tpanic(fail.err())\n"); } #[test] -fn iterators_are_lazy_sequences() { - let src = bindings(); - assert_has(&src, "func AllCarts() iter.Seq[*Cart] {"); - assert_has( - &src, - "defer C.shop_kit_shop_AllCartsIterator_destroy(cIter)", - ); - assert_has(&src, "if !yield(wvAdoptCart(cItem)) {"); +fn identifiers_avoid_keywords_and_generated_names() { + let files = render(GoConfig::default()); + let src = file(&files, "bindings.go"); + // A method named like the wrapper's Close, parameters named like Go + // keywords or body locals, and a function named like the runtime's + // Check. + assert_has(src, "func (s *Cart) Close_() {"); + assert_has(src, "func (s *Cart) Add(s_ int32) {"); + assert_has(src, "func NewCart(range_ string) *Cart {"); + assert_has(src, "func ShopCheck(context_ int32) error {"); + // A payload field named like the error's Message field. + assert_has(src, "\tMessage_ string\n"); + // The domain takes the shared name; its unknown codes have their own type. + assert_has(src, "type ShopError interface {"); + assert_has(src, "type UnknownShopError struct {"); + assert_has(src, "type OutOfStockError struct {"); } #[test] fn output_is_deterministic() { - assert_eq!(files(), files()); + assert_eq!(render(GoConfig::default()), render(GoConfig::default())); } diff --git a/crates/weaveffi-cli/src/targets/go/types.rs b/crates/weaveffi-cli/src/targets/go/types.rs index c12fa6c4..b3b1d2b4 100644 --- a/crates/weaveffi-cli/src/targets/go/types.rs +++ b/crates/weaveffi-cli/src/targets/go/types.rs @@ -1,34 +1,50 @@ -//! Go type mapping: how resolved types, zero values, scalar conversions, -//! and C ABI slot types are spelled in the generated package. +//! Go type mapping: how value types, parameter and return types, zero +//! values, and C ABI slot types are spelled in the generated package. use weaveffi_model::abi::{CType, ConstPos}; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty}; use crate::targets::go::names::pascal; -/// The Go type spelling of a resolved type. +/// The Go type spelling of a value type. /// -/// Records are value structs, rich enums sealed interfaces, interfaces -/// wrapper pointers (one Go value owns each strong reference), and callback -/// interfaces Go interfaces. `T?` is `*T` unless `T` is already nil-able -/// (see [`optional_derefs`]). +/// Records are value structs, rich enums sealed interfaces, and interfaces +/// wrapper pointers (one Go value owns each strong reference). `T?` is `*T` +/// unless `T` is already nil-able (see [`optional_derefs`]); that holds for +/// optional scalars too, whichever way they cross. pub(crate) fn go_type(ty: &Ty) -> String { match ty { Ty::Prim(p) => prim_type(*p).into(), - Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::CallbackInterface(n) => pascal(n), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) => pascal(n), Ty::Interface(n) => format!("*{}", pascal(n)), Ty::Optional(inner) if optional_derefs(inner) => format!("*{}", go_type(inner)), Ty::Optional(inner) => go_type(inner), Ty::List(inner) => format!("[]{}", go_type(inner)), Ty::Map(k, v) => format!("map[{}]{}", go_type(k), go_type(v)), - // A throwing iterator wrapper spells `iter.Seq2[T, error]` at its - // signature site instead. - Ty::Iterator(inner) => format!("iter.Seq[{}]", go_type(inner)), + } +} + +/// The Go type of a parameter: a value type, or the Go interface a +/// callback implementation satisfies (nil for an absent optional one). +pub(crate) fn param_type(ty: &ParamTy) -> String { + match ty { + ParamTy::Value(t) => go_type(t), + ParamTy::Callback { name, .. } => pascal(name), + } +} + +/// The Go type a wrapper returns for `ret` (before any `error`): a value +/// type, or a lazy sequence of the element type. +pub(crate) fn ret_type(ret: &RetTy, throws: bool) -> String { + match ret { + RetTy::Value(t) => go_type(t), + RetTy::Iterator(t) if throws => format!("iter.Seq2[{}, error]", go_type(t)), + RetTy::Iterator(t) => format!("iter.Seq[{}]", go_type(t)), } } /// The Go type of a primitive. -fn prim_type(p: Prim) -> &'static str { +pub(crate) fn prim_type(p: Prim) -> &'static str { match p { Prim::I8 => "int8", Prim::I16 => "int16", @@ -47,17 +63,12 @@ fn prim_type(p: Prim) -> &'static str { } /// `true` when `T?` surfaces as `*T` in Go. Types that are already -/// nil-able (rich enums, slices, maps, byte slices, wrapper pointers, and -/// callback interfaces) use nil as the none marker instead. +/// nil-able (rich enums, slices, maps, byte slices, and wrapper pointers) +/// use nil as the none marker instead. pub(crate) fn optional_derefs(inner: &Ty) -> bool { !matches!( inner, - Ty::RichEnum(_) - | Ty::List(_) - | Ty::Map(_, _) - | Ty::Prim(Prim::Bytes) - | Ty::Interface(_) - | Ty::CallbackInterface(_) + Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) | Ty::Prim(Prim::Bytes) | Ty::Interface(_) ) } @@ -73,18 +84,6 @@ pub(crate) fn go_zero(ty: &Ty) -> String { } } -/// The Go expression converting the Go value `expr` into the by-value C -/// slot of type `slot` (a scalar, `bool`, or C-style enum). -pub(crate) fn to_c_direct(expr: &str, slot: &CType, prefix: &str) -> String { - format!("{}({expr})", cgo_type(slot, prefix)) -} - -/// The Go expression converting the by-value C slot `expr` back into the -/// Go value of type `ty`. -pub(crate) fn from_c_direct(expr: &str, ty: &Ty) -> String { - format!("{}({expr})", go_type(ty)) -} - /// The cgo spelling of one C ABI type: `C.int32_t`, `*C.uint8_t`, /// `**C.uint8_t`, `*C.kv_kv_Store`, and `unsafe.Pointer` for `void*`. pub(crate) fn cgo_type(ct: &CType, prefix: &str) -> String { @@ -109,6 +108,14 @@ pub(crate) fn cgo_type(ct: &CType, prefix: &str) -> String { } } +/// The cgo type a pointer slot points at (`C.int32_t` for `int32_t*`). +pub(crate) fn cgo_pointee(ct: &CType, prefix: &str) -> String { + match ct { + CType::Ptr { pointee, .. } => cgo_type(&strip_const(pointee), prefix), + other => unreachable!("{other:?} is not a pointer slot"), + } +} + /// `ct` with every `const` qualifier dropped, matching the const-free /// prototypes cgo writes into `_cgo_export.h` for exported Go functions. pub(crate) fn strip_const(ct: &CType) -> CType { diff --git a/crates/weaveffi-cli/src/targets/js/api.rs b/crates/weaveffi-cli/src/targets/js/api.rs index 142e4c7c..ecc47527 100644 --- a/crates/weaveffi-cli/src/targets/js/api.rs +++ b/crates/weaveffi-cli/src/targets/js/api.rs @@ -5,28 +5,35 @@ //! provides: the N-API addon on Node.js, linear-memory glue on WebAssembly. //! The raw calling convention, which both transports implement, is: //! -//! * **Arguments**, in ABI order (an instance method's handle first): a -//! direct value as a `number`, `bigint`, or `boolean`; a string as a -//! `string`; bytes and value buffers as a `Uint8Array`; an object as its -//! handle (`null` for an absent `Interface?`); a callback interface as an -//! adapter object (`null` for an absent `Cb?`); and, last, a cancellable -//! call's cancel token handle (or `null`). -//! * **Results** come back the same way: a direct value, a `string`, a -//! `Uint8Array`, or an object handle (one strong reference). An async -//! launcher returns a `Promise` of its result; an iterator launcher -//! returns a handle, and the iterator's `next` returns an element or -//! `undefined` when the iterator is exhausted. A module's contract -//! function (`{prefix}_{module}_contract`) returns its table as a -//! `BigUint64Array` of `id, hash` pairs. +//! * **Arguments**, in ABI order (an instance method's handle first), one +//! per parameter: a direct value as a `number`, `bigint`, or `boolean`; an +//! optional scalar (OptDirect) as its value, or `null` (or `undefined`) +//! for none; a typed array (Slice) as the matching typed array +//! (`Float64Array` for `[f64]`); a string as a `string`; bytes and value +//! buffers as a `Uint8Array`; an object as its handle (`null` for an +//! absent `Interface?`); a callback interface as an adapter object +//! (`null` for an absent `Cb?`); and, last, a cancellable call's cancel +//! token handle (or `null`). The transport range-checks direct and +//! optional integers (`RangeError`) and type-checks everything. +//! * **Results** come back the same way: a direct value, an optional +//! scalar's value or `null`, a typed array (a copy the caller owns), a +//! `string`, a `Uint8Array`, or an object handle (one strong reference, +//! or `null`). An async launcher returns a `Promise` of its result; an +//! iterator launcher returns a handle, and the iterator's `next` returns +//! an element or `undefined` when the iterator is exhausted. A module's +//! contract function (`{prefix}_{module}_contract`) returns its table as +//! a `BigUint64Array` of `id, hash` pairs. //! * **Failures** throw (or reject with) a runtime `$Fault`. //! * **Callback adapters** have one method per callback method, named as in -//! the IDL, taking raw arguments (objects as adopted handles) and -//! returning the raw value of the method's return: a direct value, a -//! `string`, a `Uint8Array` (bytes or an encoded buffer, which the -//! transport copies into a run from `{prefix}_alloc`), or an object handle -//! that is a fresh strong reference (`null` for none). An adapter method -//! that throws a `$Fault` reports its code, message, and payload; any -//! other exception is reported as code -4 with its message. +//! the IDL, taking raw arguments (objects as adopted handles, optional +//! scalars as a value or `null`, typed arrays as copies) and returning +//! the raw value of the method's return: a direct value, an optional +//! scalar's value or `null`, a typed array, a `string`, a `Uint8Array` +//! (bytes or an encoded buffer, which the transport copies into a run +//! from `{prefix}_alloc`), or an object handle that is a fresh strong +//! reference (`null` for none). An adapter method that throws a `$Fault` +//! reports its code, message, and payload; any other exception is +//! reported as code -4 with its message. //! //! The transport also defines `$token`, which turns an object token read //! from a value buffer into a handle. @@ -34,17 +41,20 @@ use std::collections::BTreeSet; use weaveffi_model::model::{ - CallShape, CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding, FnBinding, - InterfaceBinding, Model, ModuleBinding, ParamBinding, + CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, InterfaceBinding, Model, + ModuleBinding, ParamBinding, }; -use weaveffi_model::plan::{ArgPass, RetPass}; -use weaveffi_model::ty::Ty; +use weaveffi_model::plan::{ + ArgPass, CallbackRetPass, ErrorStrategy, ItemPass, ResultPass, RetPass, +}; +use weaveffi_model::ty::{RetTy, Ty}; +use crate::codegen::errors::{self, ErrorTable}; use crate::codegen::CodeWriter; use crate::targets::js::codec::{emit_codecs, read_expr, reader_fn, write_expr, writer_fn}; use crate::targets::js::names::{ - callback_method_name, code_class, decl, direct_prim, error_mapper, error_owner, fn_name, - helper, js_string, member_name, module_name, param_name, type_decl, + callback_method_name, decl, error_mapper, fn_name, helper, js_string, member_name, module_name, + param_name, scalar_kind, type_decl, }; /// Render the shared API section of `index.js`: everything after the @@ -52,39 +62,31 @@ use crate::targets::js::names::{ pub(crate) fn render_api(w: &mut CodeWriter, model: &Model) { let prefix = model.prefix(); - if model.callables().any(|(_, f)| f.cancellable) { + if model.callables().any(|(_, f)| f.cancellable()) { w.line("// The native cancel tokens behind `AbortSignal`s."); w.block("const $tokens = {", "};", |w| { - w.line(format!( - "create: () => $raw.{prefix}_cancel_token_create()," - )); - w.line(format!( - "cancel: (t) => $raw.{prefix}_cancel_token_cancel(t)," - )); - w.line(format!( - "destroy: (t) => $raw.{prefix}_cancel_token_destroy(t)," - )); + for op in ["create", "cancel", "destroy"] { + let arg = if op == "create" { "" } else { "t" }; + w.line(format!( + "{op}: ({arg}) => $raw.{prefix}_cancel_token_{op}({arg})," + )); + } }); w.blank(); } - w.line("/**"); - w.line(" * The native library's live-resource counters (0 objects, 1 callbacks,"); - w.line(" * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is"); - w.line(" * `1n` when the library counts at all (its `leak-check` feature), else"); - w.line(" * every kind is `0n`."); - w.line(" */"); - w.block("export function __debugLive(kind) {", "}", |w| { - w.line(format!("return $raw.{prefix}_debug_live(kind);")); - }); + w.line("// The leak counters behind the package's `./debug` export."); + w.line(format!( + "$setLive((kind) => $raw.{prefix}_debug_live(kind));" + )); w.blank(); let raised = raised_domains(model); + for table in errors::tables(model, "Error") { + emit_error_domain(w, model, &table, raised.contains(&table.domain.name)); + } for m in &model.modules { - if let Some(eb) = m.errors.as_ref() { - emit_error_domain(w, model, m, eb, &raised); - } - for e in m.enums.iter().filter(|e| !e.rich) { + for e in m.enums.iter().filter(|e| !e.is_rich()) { w.block( format!("const {} = Object.freeze({{", decl(&m.segments, &e.name)), "});", @@ -115,92 +117,82 @@ pub(crate) fn render_api(w: &mut CodeWriter, model: &Model) { for m in &model.modules { for cb in &m.callback_interfaces { - emit_adapter(w, model, m, cb); + emit_adapter(w, model, cb); } for i in &m.interfaces { emit_class(w, model, m, i); } for f in &m.functions { - emit_iterator_spec(w, model, m, f, &decl(&m.segments, &f.name)); - let is_async = if f.is_async() { "async " } else { "" }; - let mut params = js_params(f); - if f.cancellable { - params.push("$options".into()); - } + let key = decl(&m.segments, &f.name); + emit_iterator_spec(w, model, f, &key); let name = fn_name(&f.name); w.block( format!( - "const {} = {is_async}function {name}({}) {{", + "const {} = {}function {name}({}) {{", decl(&m.segments, &name), - params.join(", ") + if f.is_async() { "async " } else { "" }, + js_params(f).join(", ") ), "};", - |w| { - emit_body( - w, - model, - m, - f, - None, - &decl(&m.segments, &f.name), - Finish::Return, - ); - }, + |w| emit_body(w, model, f, None, &key, Finish::Return), ); w.blank(); } } for m in model.roots() { - emit_namespace(w, model, m, &format!("export const {} = ", module_name(m))); + emit_namespace( + w, + model, + m, + &format!("export const {} = ", module_name(&m.name)), + ); w.blank(); } } -/// The JavaScript parameter names of a callable. +/// The JavaScript parameter names of a callable, with the trailing +/// `$options` of a cancellable one. fn js_params(f: &FnBinding) -> Vec { - f.params.iter().map(|p| param_name(&p.name)).collect() + let mut out: Vec = f.params.iter().map(|p| param_name(&p.name)).collect(); + if f.cancellable() { + out.push("$options".into()); + } + out } -/// The declaration keys (`kv$KvError`) of the error domains some callback -/// method may raise, which need a `$raise$...` mapper. +/// The error domains some callback method throws, which need a +/// `$raise$...` mapper. fn raised_domains(model: &Model) -> BTreeSet { model .callback_interfaces() - .filter(|(_, cb)| cb.methods.iter().any(|m| m.throws)) - .filter_map(|(m, _)| model.error_domain(m)) - .map(|eb| { - let owner = error_owner(&model.modules, eb); - decl(&owner.segments, &eb.name) - }) + .flat_map(|(_, cb)| &cb.methods) + .filter_map(|m| m.error.domain().map(str::to_string)) .collect() } /// Emit one error domain: the domain class, one class per code (whose /// constructor takes the code's fields first, when it has any), and the /// mapper (`$from$kv$KvError`) its throwing callables route faults through. -/// When a callback method may raise the domain, also its fields' writers -/// and the mapper (`$raise$kv$KvError`) that turns a raised error into the -/// fault the transport reports. -fn emit_error_domain( - w: &mut CodeWriter, - model: &Model, - m: &ModuleBinding, - eb: &ErrorBinding, - raised: &BTreeSet, -) { - let domain = decl(&m.segments, &eb.type_name); - let key = decl(&m.segments, &eb.name); +/// When a callback method may raise the domain (`raised`), also its fields' +/// writers and the mapper (`$raise$kv$KvError`) that turns a raised error +/// into the fault the transport reports. +fn emit_error_domain(w: &mut CodeWriter, model: &Model, table: &ErrorTable<'_>, raised: bool) { + let segments = &table.module.segments; + let eb = table.domain; + let domain = decl(segments, &table.type_name); + let key = type_decl(model, &eb.name); w.line(format!( "const {domain} = class {} extends $Error {{}};", - eb.type_name + table.type_name )); - for c in &eb.codes { - let class = code_class(&c.name); + for row in &table.codes { + let c = row.code; + let class = &row.type_name; w.block( format!( "const {} = class {class} extends {domain} {{", - decl(&m.segments, &class) + decl(segments, class) ), "};", |w| { @@ -225,36 +217,40 @@ fn emit_error_domain( }, ); } - let codes: Vec = eb + let codes: Vec = table .codes .iter() - .map(|c| format!("[{}, {}]", c.value, decl(&m.segments, &code_class(&c.name)))) + .map(|row| format!("[{}, {}]", row.code.value, decl(segments, &row.type_name))) .collect(); emit_map(w, &format!("$codes${key}"), &codes); - let fielded = || eb.codes.iter().filter(|c| !c.fields.is_empty()); + let fielded = || table.codes.iter().filter(|row| !row.code.fields.is_empty()); let payloads: Vec = fielded() - .map(|c| { - let fields: Vec = c + .map(|row| { + let fields: Vec = row + .code .fields .iter() .map(|f| format!("{}: {}", f.name, read_expr(model, &f.ty))) .collect(); - format!("[{}, (r) => ({{ {} }})]", c.value, fields.join(", ")) + format!("[{}, (r) => ({{ {} }})]", row.code.value, fields.join(", ")) }) .collect(); emit_map(w, &format!("$payloads${key}"), &payloads); w.block(format!("function $from${key}(e) {{"), "}", |w| { - w.line(format!("return $domain(e, $codes${key}, $payloads${key});")); + w.line(format!( + "return $domain(e, {domain}, $codes${key}, $payloads${key});" + )); }); - if raised.contains(&key) { + if raised { let writers: Vec = fielded() - .map(|c| { - let fields: Vec = c + .map(|row| { + let fields: Vec = row + .code .fields .iter() .map(|f| format!("{};", write_expr(model, &f.ty, &format!("e.{}", f.name)))) .collect(); - format!("[{}, (w, e) => {{ {} }}]", c.value, fields.join(" ")) + format!("[{}, (w, e) => {{ {} }}]", row.code.value, fields.join(" ")) }) .collect(); emit_map(w, &format!("$fields${key}"), &writers); @@ -281,65 +277,169 @@ fn emit_map(w: &mut CodeWriter, name: &str, entries: &[String]) { }); } +/// How a raw value received from the native side becomes its idiomatic +/// value. +enum Lift<'a> { + /// Unchanged: a direct value, an optional scalar, a string, or bytes. + Plain, + /// A typed array that surfaces as a plain array. + List, + /// A value buffer decoded as this type. + Decode(&'a Ty), + /// An object handle adopted into a new wrapper. + Adopt { interface: &'a str, nullable: bool }, +} + +impl<'a> Lift<'a> { + fn of_ret(pass: &'a RetPass, ty: Option<&'a Ty>) -> Self { + match pass { + RetPass::Void + | RetPass::Direct + | RetPass::OptDirect { .. } + | RetPass::String { .. } + | RetPass::Bytes { .. } + | RetPass::Iterator(_) => Lift::Plain, + RetPass::Slice { .. } => Lift::List, + RetPass::Buffer { .. } => Lift::Decode(ty.expect("a buffered return has a type")), + RetPass::Object { + interface, + nullable, + .. + } => Lift::Adopt { + interface, + nullable: *nullable, + }, + } + } + + fn of_result(pass: &'a ResultPass, ty: Option<&'a Ty>) -> Self { + match pass { + ResultPass::Void + | ResultPass::Direct { .. } + | ResultPass::OptDirect { .. } + | ResultPass::String { .. } + | ResultPass::Bytes { .. } => Lift::Plain, + ResultPass::Slice { .. } => Lift::List, + ResultPass::Buffer { .. } => Lift::Decode(ty.expect("a buffered result has a type")), + ResultPass::Object { + interface, + nullable, + .. + } => Lift::Adopt { + interface, + nullable: *nullable, + }, + } + } + + fn of_item(pass: &'a ItemPass, ty: &'a Ty) -> Self { + match pass { + ItemPass::Direct { .. } + | ItemPass::OptDirect { .. } + | ItemPass::String { .. } + | ItemPass::Bytes { .. } => Lift::Plain, + ItemPass::Slice { .. } => Lift::List, + ItemPass::Buffer { .. } => Lift::Decode(ty), + ItemPass::Object { + interface, + nullable, + .. + } => Lift::Adopt { + interface, + nullable: *nullable, + }, + } + } + + /// A callback method's parameter, as the producer passes it. + fn of_arg(pass: &'a ArgPass, ty: &'a Ty) -> Self { + match pass { + ArgPass::Direct { .. } + | ArgPass::OptDirect { .. } + | ArgPass::String { .. } + | ArgPass::Bytes { .. } => Lift::Plain, + ArgPass::Slice { .. } => Lift::List, + ArgPass::Buffer { .. } => Lift::Decode(ty), + ArgPass::Object { + interface, + nullable, + .. + } => Lift::Adopt { + interface, + nullable: *nullable, + }, + ArgPass::Callback { .. } => unreachable!("callback methods take value types only"), + } + } + + /// The idiomatic value of `raw`. + fn apply(&self, model: &Model, raw: &str) -> String { + match self { + Lift::Plain => raw.to_string(), + Lift::List => format!("Array.from({raw})"), + Lift::Decode(ty) => format!("$decode({raw}, {})", reader_fn(model, ty)), + Lift::Adopt { + interface, + nullable, + } => { + let adopt = if *nullable { "$adoptOpt" } else { "$adopt" }; + format!("{adopt}({}, {raw})", type_decl(model, interface)) + } + } + } +} + /// Emit the adapter for one callback interface: it checks the consumer's /// implementation and returns the raw object the transport calls, whose /// methods (named as in the IDL) convert raw arguments into idiomatic values -/// (decoding buffers, adopting objects), call the implementation, and turn -/// the return value into its raw form (checking direct values, strings, and -/// bytes, encoding buffers, cloning objects). An exception reaches the -/// transport, which reports it to the producer as a code -4 failure, except -/// that a method declared `throws` first maps its module's domain errors -/// onto the `$Fault` that carries their code and fields. -fn emit_adapter( - w: &mut CodeWriter, - model: &Model, - m: &ModuleBinding, - cb: &CallbackInterfaceBinding, -) { - let protocol = cb.protocol(); - let raise = model.error_domain(m).map(|eb| { - let owner = error_owner(&model.modules, eb); - format!("$raise${}", decl(&owner.segments, &eb.name)) - }); +/// (decoding buffers, adopting objects, turning typed arrays into arrays), +/// call the implementation, and turn the return value into its raw form +/// (checking and range-checking direct values, strings, bytes, and typed +/// arrays, encoding buffers, cloning objects). How a failure reaches the +/// producer follows the method's error strategy: a method that doesn't +/// throw lets the exception reach the transport, which reports code -4; a +/// `throws: any` method reports code -1 with the message; a method that +/// throws a domain reports that domain's errors with their code and +/// fields, and anything else as code -1. +fn emit_adapter(w: &mut CodeWriter, model: &Model, cb: &CallbackInterfaceBinding) { w.block( format!("function {}(impl) {{", helper(model, "adapt", &cb.name)), "}", |w| { w.line(format!("$impl(impl, {});", js_string(&cb.name))); w.block("return {", "};", |w| { - for ((method, passes), ret) in cb - .methods - .iter() - .zip(&protocol.method_args) - .zip(&protocol.method_returns) - { + for method in &cb.methods { let raw: Vec = (0..method.params.len()).map(|i| format!("$a{i}")).collect(); let args: Vec = method .params .iter() - .zip(passes) .zip(&raw) - .map(|((p, pass), a)| receive(model, &p.ty, pass, a)) + .map(|(p, a)| Lift::of_arg(&p.pass, &p.ty).apply(model, a)) .collect(); let call = format!( "impl.{}({})", callback_method_name(&method.name), args.join(", ") ); - let stmt = callback_return(model, cb, method, ret, &call); + let stmt = callback_return(model, cb, method, &call); + let map = match &method.error { + ErrorStrategy::Trap => None, + ErrorStrategy::Untyped => Some("$untyped".to_string()), + ErrorStrategy::Domain(d) => Some(helper(model, "raise", d)), + }; w.block( format!("{}({}) {{", method.name, raw.join(", ")), "},", - |w| match raise.as_deref().filter(|_| method.throws) { - Some(raise) => { + |w| match &map { + Some(map) => { w.line("try {"); w.scope(|w| { w.line(&stmt); }); w.line("} catch ($e) {"); w.scope(|w| { - w.line(format!("throw {raise}($e);")); + w.line(format!("throw {map}($e);")); }); w.line("}"); } @@ -362,83 +462,65 @@ fn callback_return( model: &Model, cb: &CallbackInterfaceBinding, method: &CallbackMethodBinding, - pass: &RetPass, call: &str, ) -> String { - let where_ = || { + let what = || { js_string(&format!( - "{}.{}", + "the return value of {}.{}", cb.name, callback_method_name(&method.name) )) }; - let Some(ty) = &method.ret else { - return format!("{call};"); + let ty = || { + method + .ret + .as_ref() + .expect("a method with a return returns a type") }; - let value = match pass { - RetPass::Void => unreachable!("a method with a return type returns a value"), - RetPass::Direct => format!("$ret.{}({call}, {})", direct_prim(ty).pascal(), where_()), - RetPass::String => format!("$ret.String({call}, {})", where_()), - RetPass::Bytes => format!("$ret.Bytes({call}, {})", where_()), - RetPass::Buffer => format!("$encode({call}, {})", writer_fn(model, ty)), - RetPass::Object { nullable } => { - let cls = type_decl( - model, - ty.interface_name().expect("object type names an interface"), - ); + let value = match &method.ret_pass { + CallbackRetPass::Void => return format!("{call};"), + CallbackRetPass::Direct => { + format!("$check.{}({call}, {})", scalar_kind(ty()), what()) + } + CallbackRetPass::OptDirect { .. } => { + let Ty::Optional(inner) = ty() else { + unreachable!("an OptDirect return is optional") + }; + format!("$opt({call}, $check.{}, {})", scalar_kind(inner), what()) + } + CallbackRetPass::Slice { elem, .. } => { + format!("$slice.{}({call}, {})", elem.pascal(), what()) + } + CallbackRetPass::String { .. } => format!("$check.String({call}, {})", what()), + CallbackRetPass::Bytes { .. } => format!("$check.Bytes({call}, {})", what()), + CallbackRetPass::Buffer { .. } => format!("$encode({call}, {})", writer_fn(model, ty())), + CallbackRetPass::Object { + nullable, + interface, + .. + } => { let clone = if *nullable { "$cloneOpt" } else { "$clone" }; - format!("{clone}({call}, {cls})") + format!("{clone}({call}, {})", type_decl(model, interface)) } }; format!("return {value};") } -/// The idiomatic value of a raw value `raw` of type `ty` received from the -/// native side per `pass`: decoded when buffered, adopted when an object, -/// unchanged otherwise. -fn receive(model: &Model, ty: &Ty, pass: &RetPass, raw: &str) -> String { - match pass { - RetPass::Buffer => format!("$decode({raw}, {})", reader_fn(model, ty)), - RetPass::Object { nullable } => { - let cls = type_decl( - model, - ty.interface_name().expect("object type names an interface"), - ); - if *nullable { - format!("$adoptOpt({cls}, {raw})") - } else { - format!("$adopt({cls}, {raw})") - } - } - RetPass::Void | RetPass::Direct | RetPass::String | RetPass::Bytes => raw.to_string(), - } -} - /// Emit the iterator spec of an iterator-returning callable (`key` names /// the callable), the shared `next`/`destroy`/error mapping/element /// conversion its `$Iterator`s use. -fn emit_iterator_spec( - w: &mut CodeWriter, - model: &Model, - m: &ModuleBinding, - f: &FnBinding, - key: &str, -) { - let CallShape::Iterator(it) = &f.shape else { +fn emit_iterator_spec(w: &mut CodeWriter, model: &Model, f: &FnBinding, key: &str) { + let Some(it) = f.iterator() else { return; }; - let protocol = it.protocol(f); - let convert = match receive(model, &it.elem, &protocol.elem, "v") { - v if v == "v" => "null".to_string(), - other => format!("(v) => {other}"), + let convert = match Lift::of_item(&it.item, &it.elem) { + Lift::Plain => "null".to_string(), + lift => format!("(v) => {}", lift.apply(model, "v")), }; w.block(format!("const $it${key} = {{"), "};", |w| { w.line(format!("next: (h) => $raw.{}(h),", it.next.symbol)); w.line(format!("destroy: (h) => $raw.{}(h),", it.destroy_symbol)); - w.line(format!( - "map: {},", - error_mapper(&model.modules, f, model.error_domain(m)) - )); + w.line(format!("map: {},", error_mapper(model, &f.error))); w.line(format!("convert: {convert},")); }); } @@ -450,12 +532,9 @@ fn emit_iterator_spec( /// `$destroy` and `$clone`. fn emit_class(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, i: &InterfaceBinding) { let cls = decl(&m.segments, &i.name); - let canonical = i - .constructors - .iter() - .find(|c| c.name == "new" && !c.is_async()); - for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { - emit_iterator_spec(w, model, m, f, &format!("{cls}${}", f.name)); + let canonical = canonical_constructor(i); + for f in i.members() { + emit_iterator_spec(w, model, f, &format!("{cls}${}", f.name)); } w.block( format!("const {cls} = class {} extends $Object {{", i.name), @@ -468,7 +547,7 @@ fn emit_class(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, i: &Interfac "}", |w| { w.line("super();"); - emit_body(w, model, m, c, None, &cls, Finish::Own); + emit_body(w, model, c, None, &cls, Finish::Own); }, ); } @@ -499,21 +578,17 @@ fn emit_class(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, i: &Interfac .chain(i.methods.iter().map(|f| (f, false))) .chain(i.statics.iter().map(|f| (f, true))); for (f, is_static) in members { - let mut params = js_params(f); - if f.cancellable { - params.push("$options".into()); - } let head = format!( "{}{}{}({}) {{", if is_static { "static " } else { "" }, if f.is_async() { "async " } else { "" }, member_name(&f.name, is_static), - params.join(", ") + js_params(f).join(", ") ); let recv = (!is_static).then_some(cls.as_str()); let key = format!("{cls}${}", f.name); w.block(head, "}", |w| { - emit_body(w, model, m, f, recv, &key, Finish::Return); + emit_body(w, model, f, recv, &key, Finish::Return); }); } }, @@ -521,6 +596,14 @@ fn emit_class(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, i: &Interfac w.blank(); } +/// The constructor that becomes an interface's JavaScript constructor: the +/// synchronous one named `new`. +pub(crate) fn canonical_constructor(i: &InterfaceBinding) -> Option<&FnBinding> { + i.constructors + .iter() + .find(|c| c.name == "new" && !c.is_async()) +} + /// What a callable's body does with the converted result. #[derive(Clone, Copy, PartialEq, Eq)] enum Finish { @@ -544,7 +627,6 @@ struct Loan { fn emit_body( w: &mut CodeWriter, model: &Model, - m: &ModuleBinding, f: &FnBinding, recv: Option<&str>, key: &str, @@ -564,12 +646,8 @@ fn emit_body( for p in &f.params { args.push(arg_expr(model, p, &mut loans)); } - let symbol = match &f.shape { - CallShape::Sync(abi) => &abi.symbol, - CallShape::Async(a) => &a.launch.symbol, - CallShape::Iterator(it) => &it.launch.symbol, - }; - let call = if f.cancellable { + let symbol = &f.abi.symbol; + let call = if f.cancellable() { args.push("$t".into()); format!( "await $cancellable($options?.signal, $tokens, ($t) => $raw.{symbol}({}))", @@ -580,19 +658,20 @@ fn emit_body( } else { format!("$raw.{symbol}({})", args.join(", ")) }; - let stmt = match (&f.shape, finish) { - (_, Finish::Own) => format!("$own(this, {call});"), - (CallShape::Iterator(_), _) => format!("return new $Iterator({call}, $it${key});"), - _ => match RetPass::of(f.ret.as_ref()) { + let ty = f.ret.as_ref().map(RetTy::elem); + let stmt = match (finish, f.async_binding()) { + (Finish::Own, _) => format!("$own(this, {call});"), + (Finish::Return, Some(a)) => match &a.result { + ResultPass::Void => format!("{call};"), + pass => format!("return {};", Lift::of_result(pass, ty).apply(model, &call)), + }, + (Finish::Return, None) => match &f.ret_pass { RetPass::Void => format!("{call};"), - pass => format!( - "return {};", - receive(model, f.ret.as_ref().expect("non-void"), &pass, &call) - ), + RetPass::Iterator(_) => format!("return new $Iterator({call}, $it${key});"), + pass => format!("return {};", Lift::of_ret(pass, ty).apply(model, &call)), }, }; - let map = error_mapper(&model.modules, f, model.error_domain(m)); - emit_loans(w, &loans, &stmt, &map); + emit_loans(w, &loans, &stmt, &error_mapper(model, &f.error)); } /// Emit the loans (each `$lend` paired with an `$unlend` in a `finally`) @@ -642,30 +721,39 @@ fn emit_catch(w: &mut CodeWriter, map: &str) { /// lends. fn arg_expr(model: &Model, p: &ParamBinding, loans: &mut Vec) -> String { let name = param_name(&p.name); - match p.arg_pass() { - ArgPass::Direct { .. } | ArgPass::String { .. } | ArgPass::Bytes { .. } => name, - ArgPass::Buffer { .. } => format!("$encode({name}, {})", writer_fn(model, &p.ty)), - ArgPass::Object { nullable, .. } => { - let cls = type_decl( - model, - p.ty.interface_name() - .expect("object parameter names an interface"), - ); + match &p.pass { + ArgPass::Direct { .. } + | ArgPass::OptDirect { .. } + | ArgPass::String { .. } + | ArgPass::Bytes { .. } => name, + ArgPass::Slice { elem, .. } => { + format!("$slice.{}({name}, {})", elem.pascal(), js_string(&name)) + } + ArgPass::Buffer { .. } => { + let ty = p.ty.value().expect("a buffered parameter is a value"); + format!("$encode({name}, {})", writer_fn(model, ty)) + } + ArgPass::Object { + nullable, + interface, + .. + } => { let local = format!("$o{}", loans.len()); loans.push(Loan { value: name, local: local.clone(), - cls, - nullable, + cls: type_decl(model, interface), + nullable: *nullable, }); local } - ArgPass::Callback { nullable, .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback parameter names a callback interface"); - let adapt = format!("{}({name})", helper(model, "adapt", cb)); - if nullable { + ArgPass::Callback { + nullable, + interface, + .. + } => { + let adapt = format!("{}({name})", helper(model, "adapt", interface)); + if *nullable { format!("{name} == null ? null : {adapt}") } else { adapt @@ -680,18 +768,24 @@ fn arg_expr(model: &Model, p: &ParamBinding, loans: &mut Vec) -> String { fn emit_namespace(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, lead: &str) { w.line(format!("{lead}Object.freeze({{")); w.scope(|w| { - if let Some(eb) = m.errors.as_ref() { + for table in errors::tables(model, "Error") + .iter() + .filter(|t| t.module.index == m.index) + { w.line(format!( "{}: {},", - eb.type_name, - decl(&m.segments, &eb.type_name) + table.type_name, + decl(&m.segments, &table.type_name) )); - for c in &eb.codes { - let class = code_class(&c.name); - w.line(format!("{class}: {},", decl(&m.segments, &class))); + for row in &table.codes { + w.line(format!( + "{}: {},", + row.type_name, + decl(&m.segments, &row.type_name) + )); } } - for e in m.enums.iter().filter(|e| !e.rich) { + for e in m.enums.iter().filter(|e| !e.is_rich()) { w.line(format!("{}: {},", e.name, decl(&m.segments, &e.name))); } for i in &m.interfaces { @@ -702,8 +796,8 @@ fn emit_namespace(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, lead: &s w.line(format!("{name}: {},", decl(&m.segments, &name))); } for child in model.children(m) { - emit_namespace(w, model, child, &format!("{}: ", module_name(child))); + emit_namespace(w, model, child, &format!("{}: ", module_name(&child.name))); } }); - w.line(if m.segments.len() == 1 { "});" } else { "})," }); + w.line(if m.parent.is_none() { "});" } else { "})," }); } diff --git a/crates/weaveffi-cli/src/targets/js/codec.rs b/crates/weaveffi-cli/src/targets/js/codec.rs index 48bac80b..bd99aecd 100644 --- a/crates/weaveffi-cli/src/targets/js/codec.rs +++ b/crates/weaveffi-cli/src/targets/js/codec.rs @@ -1,10 +1,11 @@ //! Value-buffer codec emitters. //! -//! Records, rich enums, optionals, lists, maps, and error payloads cross the -//! ABI as value buffers. The runtime's `$Writer` and `$Reader` implement the -//! wire primitives (`writeI32`, `readString`, ...) and the optional, list, -//! and map combinators. This module emits one writer and one reader -//! function per type a buffer can hold beyond the primitives: +//! Records, rich enums, optionals, lists, and maps that cross inside a value +//! buffer, and error payloads, use the wire format. The runtime's `$Writer` +//! and `$Reader` implement the wire primitives (`writeI32`, `readString`, +//! ...) and the optional, list, and map combinators. This module emits one +//! writer and one reader function per type a buffer can hold beyond the +//! primitives: //! //! * each record and rich enum (`$w$kv$Entry`, `$r$kv$Entry`), named by its //! declaring module's path; @@ -13,34 +14,21 @@ //! (`$clone`), and reading one adopts the reference into a new wrapper //! (the transport defines `$token`, which turns the `u64` token into its //! handle representation); -//! * each distinct optional, list, and map type anywhere in the API -//! (`$w_list_opt_Entry` writes `[Entry?]`), named after the type's shape -//! with bare type names, which are unique across the API. +//! * each composite (`$w_list_opt_Entry` writes `[Entry?]`), from the shared +//! [`codecs::composites`] and named by its [`codecs::stem`]. //! //! Call sites then name these functions instead of building closures. Every //! dispatch goes through [`Ty::wire`], and primitives collapse onto one arm -//! through [`Prim::pascal`]. +//! through [`Prim::pascal`](weaveffi_model::ty::Prim::pascal). -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; -use weaveffi_model::model::{CallShape, Model}; -use weaveffi_model::ty::{Family, Ty, WireType}; +use weaveffi_model::model::Model; +use weaveffi_model::ty::{Ty, WireType}; -use crate::codegen::CodeWriter; +use crate::codegen::{codecs, CodeWriter}; use crate::targets::js::names::{helper, js_string, type_decl}; -/// The name stem of a composite (optional, list, or map) type's codec -/// functions: `opt_Entry`, `list_opt_Entry`, `map_string_Store`. -fn mangle(ty: &Ty) -> String { - match ty.wire() { - WireType::Prim(p) => p.snake().to_string(), - WireType::Enum(n) | WireType::User(n) | WireType::Object(n) => n.to_string(), - WireType::Optional(inner) => format!("opt_{}", mangle(inner)), - WireType::List(inner) => format!("list_{}", mangle(inner)), - WireType::Map(k, v) => format!("map_{}_{}", mangle(k), mangle(v)), - } -} - /// A function expression `(w, v) => ...` writing one value of `ty`. pub(crate) fn writer_fn(model: &Model, ty: &Ty) -> String { match ty.wire() { @@ -48,7 +36,7 @@ pub(crate) fn writer_fn(model: &Model, ty: &Ty) -> String { WireType::Enum(_) => "$W.I32".into(), WireType::User(n) | WireType::Object(n) => helper(model, "w", n), WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("$w_{}", mangle(ty)) + format!("$w_{}", codecs::stem(ty)) } } } @@ -60,7 +48,7 @@ pub(crate) fn reader_fn(model: &Model, ty: &Ty) -> String { WireType::Enum(_) => "$R.I32".into(), WireType::User(n) | WireType::Object(n) => helper(model, "r", n), WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("$r_{}", mangle(ty)) + format!("$r_{}", codecs::stem(ty)) } } } @@ -92,94 +80,58 @@ fn key_writer_fn(ty: &Ty) -> String { } } -/// Every type a value buffer in `model` can hold that needs codec functions -/// of its own beyond records and rich enums: the composites by name stem, -/// and the interfaces carried as object tokens. -#[derive(Default)] -struct Shapes<'a> { - composites: BTreeMap, - objects: BTreeSet<&'a str>, -} - -impl<'a> Shapes<'a> { - fn of(model: &'a Model) -> Self { - let mut shapes = Shapes::default(); - for m in &model.modules { - let fields = m - .structs - .iter() - .flat_map(|s| &s.fields) - .chain( - m.enums - .iter() - .flat_map(|e| e.variants.iter()) - .flat_map(|v| &v.fields), - ) - .chain( - m.errors - .iter() - .flat_map(|e| &e.codes) - .flat_map(|c| &c.fields), - ); - for f in fields { - shapes.visit(&f.ty); - } - for f in m.callables() { - for p in &f.params { - shapes.visit_slot(&p.ty); - } - match &f.shape { - CallShape::Iterator(it) => shapes.visit_slot(&it.elem), - _ => { - if let Some(ty) = &f.ret { - shapes.visit_slot(ty); - } - } - } - } - for method in m.callback_interfaces.iter().flat_map(|cb| &cb.methods) { - for p in &method.params { - shapes.visit_slot(&p.ty); - } - if let Some(ty) = &method.ret { - shapes.visit_slot(ty); - } - } +/// Every interface a value buffer in `model` can carry as an object token: +/// those named by a record, variant, or payload field, or inside a +/// composite. +fn tokens(model: &Model, composites: &[Ty]) -> BTreeSet { + let mut out = BTreeSet::new(); + let mut visit = |ty: &Ty| collect_interfaces(ty, &mut out); + for m in &model.modules { + let fields = m + .structs + .iter() + .flat_map(|s| &s.fields) + .chain( + m.enums + .iter() + .flat_map(|e| &e.variants) + .flat_map(|v| &v.fields), + ) + .chain( + m.errors + .iter() + .flat_map(|e| &e.codes) + .flat_map(|c| &c.fields), + ); + for f in fields { + visit(&f.ty); } - shapes } - - /// A parameter, return, or element type: only buffers hold codecs. - fn visit_slot(&mut self, ty: &'a Ty) { - if ty.family() == Family::Buffer { - self.visit(ty); - } + for ty in composites { + visit(ty); } + out +} - /// A type inside a value buffer. - fn visit(&mut self, ty: &'a Ty) { - match ty.wire() { - WireType::Prim(_) | WireType::Enum(_) | WireType::User(_) => {} - WireType::Object(n) => { - self.objects.insert(n); - } - WireType::Optional(inner) | WireType::List(inner) => { - self.composites.insert(mangle(ty), ty); - self.visit(inner); - } - WireType::Map(k, v) => { - self.composites.insert(mangle(ty), ty); - self.visit(k); - self.visit(v); - } +/// Add every interface `ty` names, at any depth, to `out`. +fn collect_interfaces(ty: &Ty, out: &mut BTreeSet) { + match ty { + Ty::Interface(n) => { + out.insert(n.clone()); + } + Ty::Optional(inner) | Ty::List(inner) => collect_interfaces(inner, out), + Ty::Map(k, v) => { + collect_interfaces(k, out); + collect_interfaces(v, out); } + Ty::Prim(_) | Ty::Record(_) | Ty::RichEnum(_) | Ty::Enum(_) => {} } } /// Emit the writer and reader of every record, rich enum, object token, and /// composite type in the model. pub(crate) fn emit_codecs(w: &mut CodeWriter, model: &Model) { - let shapes = Shapes::of(model); + let composites = codecs::composites(model); for m in &model.modules { for s in &m.structs { let name = &s.name; @@ -211,7 +163,7 @@ pub(crate) fn emit_codecs(w: &mut CodeWriter, model: &Model) { }, ); } - for e in m.enums.iter().filter(|e| e.rich) { + for e in m.enums.iter().filter(|e| e.is_rich()) { let name = &e.name; let what = js_string(&format!("unknown {} tag: ", e.name)); w.block( @@ -262,24 +214,24 @@ pub(crate) fn emit_codecs(w: &mut CodeWriter, model: &Model) { ); } } - for name in &shapes.objects { - let cls = type_decl(model, name); + for name in tokens(model, &composites) { + let cls = type_decl(model, &name); w.block( - format!("function {}(w, v) {{", helper(model, "w", name)), + format!("function {}(w, v) {{", helper(model, "w", &name)), "}", |w| { w.line(format!("w.writeU64(BigInt($clone(v, {cls})));")); }, ); w.block( - format!("function {}(r) {{", helper(model, "r", name)), + format!("function {}(r) {{", helper(model, "r", &name)), "}", |w| { w.line(format!("return $adopt({cls}, $token(r.readU64()));")); }, ); } - for ty in shapes.composites.values() { + for ty in &composites { let (write, read) = match ty.wire() { WireType::Optional(inner) => ( format!("w.writeOpt(v, {});", writer_fn(model, inner)), @@ -340,7 +292,7 @@ mod tests { } #[test] - fn composites_name_one_function_per_shape() { + fn composites_are_named_by_the_shared_stem() { let m = model(); let ty = Ty::Optional(Box::new(Ty::List(Box::new(Ty::Record("Entry".into()))))); assert_eq!(write_expr(&m, &ty, "v.x"), "$w_opt_list_Entry(w, v.x)"); @@ -352,45 +304,4 @@ mod tests { assert_eq!(writer_fn(&m, &map), "$w_map_u64_Store"); assert_eq!(reader_fn(&m, &Ty::Interface("Store".into())), "$r$kv$Store"); } - - #[test] - fn codecs_cover_every_buffered_shape_once() { - let yaml = r#" -version: "0.11.0" -modules: - - name: kv - structs: - - name: Entry - fields: - - { name: tags, type: "[string]" } - - { name: by_kind, type: "{Kind:u32}" } - - { name: owner, type: "Store?" } - enums: [{ name: Kind, variants: [{ name: A, value: 0 }] }] - interfaces: [{ name: Store, methods: [{ name: get }] }] - functions: - - { name: all, params: [{ name: tags, type: "[string]" }], return: "[Entry?]" } -"#; - let api = weaveffi_model::parse::parse_api_str(yaml, "yaml").unwrap(); - let m = weaveffi_model::validate::validate( - &api, - &weaveffi_model::pkg::Identity::named("t"), - None, - ) - .unwrap(); - let mut w = CodeWriter::two_space(); - emit_codecs(&mut w, &m); - let js = w.finish(); - for needle in [ - "function $w_list_string(w, v) {\n w.writeList(v, $W.String);\n}", - "function $r_map_Kind_u32(r) {\n return r.readMap($R.I32, $R.U32);\n}", - "function $w_map_Kind_u32(w, v) {\n w.writeMap(v, $WK.I32, $W.U32);\n}", - "function $w_list_opt_Entry(w, v) {\n w.writeList(v, $w_opt_Entry);\n}", - "function $r_opt_Store(r) {\n return r.readOpt($r$kv$Store);\n}", - "function $w$kv$Store(w, v) {\n w.writeU64(BigInt($clone(v, kv$Store)));\n}", - " $w_opt_Store(w, v.owner);\n", - ] { - assert!(js.contains(needle), "missing `{needle}` in:\n{js}"); - } - assert_eq!(js.matches("function $w_list_string(").count(), 1); - } } diff --git a/crates/weaveffi-cli/src/targets/js/dts.rs b/crates/weaveffi-cli/src/targets/js/dts.rs index cb2cb8f6..c8a45a5c 100644 --- a/crates/weaveffi-cli/src/targets/js/dts.rs +++ b/crates/weaveffi-cli/src/targets/js/dts.rs @@ -2,23 +2,35 @@ //! //! Each top-level IDL module is an exported namespace (`kv`), nested //! modules are nested namespaces, and user types are referenced by their -//! full path from the root (`kv.Entry`). +//! full path from the root (`kv.Entry`). Doc and deprecation text name API +//! declarations in their JavaScript spelling. use weaveffi_model::model::{ - CallShape, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - Model, ModuleBinding, ParamBinding, StructBinding, + CallbackInterfaceBinding, EnumBinding, FnBinding, InterfaceBinding, Model, ModuleBinding, + StructBinding, }; -use weaveffi_model::ty::Ty; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; +use weaveffi_model::ty::{RetTy, Ty}; +use crate::codegen::docs::{ApiNames, Doc}; +use crate::codegen::errors::{self, ErrorTable}; use crate::codegen::CodeWriter; +use crate::targets::js::api::canonical_constructor; use crate::targets::js::names::{ - callback_method_name, code_class, error_owner, fn_name, js_string, member_name, module_name, - param_name, ts_path_in, ts_type, + callback_method_name, doc_spelling, fn_name, js_string, member_name, module_name, param_name, + rewrite_doc, ts_domain, ts_path_in, ts_slice_input, ts_type, }; +/// What the declarations render with: the model and its identifier index. +struct Decls<'a> { + model: &'a Model, + names: ApiNames, + root_error: &'a str, +} + /// Render the declarations body: the root error classes, `extra` (the -/// transport's own exports, such as WebAssembly's `init`), the leak-counter -/// hook, and one namespace per top-level module. `root_error` is the root +/// transport's own exports, such as WebAssembly's `init`), the iterator +/// type, and one namespace per top-level module. `root_error` is the root /// error class name. pub(crate) fn render_declarations( w: &mut CodeWriter, @@ -26,13 +38,18 @@ pub(crate) fn render_declarations( root_error: &str, extra: &str, ) { + let d = Decls { + model, + names: ApiNames::new(model), + root_error, + }; w.line("/// "); w.blank(); w.line("/**"); w.line(" * The root of every error these bindings throw. `code` is the ABI error"); - w.line(" * code: positive for a declared domain error, negative for a runtime trap"); - w.line(" * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback"); - w.line(" * implementation failed, -5 cancelled)."); + w.line(" * code: positive for a domain error, negative for a runtime failure (-1"); + w.line(" * an untyped failure, -2 producer panic, -3 marshalling failure, -4 a"); + w.line(" * callback implementation failed, -5 cancelled)."); w.line(" */"); w.block( format!("export declare class {root_error} extends Error {{"), @@ -52,326 +69,380 @@ pub(crate) fn render_declarations( ); w.line("/** The root error class, under a name no namespace member can shadow. */"); w.line(format!("declare const $Error: typeof {root_error};")); - w.raw(extra); w.line("/**"); - w.line(" * The native library's live-resource counters (0 objects, 1 callbacks,"); - w.line(" * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is"); - w.line(" * `1n` when the library counts at all."); - w.line(" * @internal"); + w.line(" * A lazy iterator over a native `iter`: each `next()` makes one native"); + w.line(" * call. The native iterator is released when it is exhausted or fails, by"); + w.line(" * `return()` (which `for...of` calls on `break`), by `close()` or a `using`"); + w.line(" * declaration, or when the iterator is garbage collected."); w.line(" */"); - w.line("export declare function __debugLive(kind: number): bigint;"); + w.block( + "export interface NativeIterator extends IterableIterator {", + "}", + |w| { + w.line("/** Release the native iterator now. Safe to call more than once. */"); + w.line("close(): void;"); + w.line("/** Alias of `close()` for `using` declarations. */"); + w.line("[Symbol.dispose](): void;"); + }, + ); + w.raw(extra); + let tables = errors::tables(model, "Error"); for m in model.roots() { w.blank(); - doc(w, m.doc.as_deref(), &[]); + d.doc(w, &m.doc, &None, &[]); w.block( - format!("export declare namespace {} {{", module_name(m)), + format!("export declare namespace {} {{", module_name(&m.name)), "}", - |w| namespace_body(w, model, m), + |w| d.namespace_body(w, m, &tables), ); } } -fn namespace_body(w: &mut CodeWriter, model: &Model, m: &ModuleBinding) { - if let Some(eb) = m.errors.as_ref() { - error_domain(w, model, m, eb); - } - for e in &m.enums { - enumeration(w, model, e); - } - for s in &m.structs { - record(w, model, s); - } - for cb in &m.callback_interfaces { - callback_interface(w, model, m, cb); - } - for i in &m.interfaces { - class(w, model, m, i); - } - for f in &m.functions { - doc(w, f.doc.as_deref(), &fn_tags(model, m, f)); - w.line(format!( - "export function {}({}): {};", - fn_name(&f.name), - params(model, f), - ret(model, f) - )); - } - for child in model.children(m) { - doc(w, child.doc.as_deref(), &[]); - w.block( - format!("export namespace {} {{", module_name(child)), - "}", - |w| namespace_body(w, model, child), - ); +impl Decls<'_> { + fn namespace_body(&self, w: &mut CodeWriter, m: &ModuleBinding, tables: &[ErrorTable<'_>]) { + for table in tables.iter().filter(|t| t.module.index == m.index) { + self.error_domain(w, table); + } + for e in &m.enums { + self.enumeration(w, e); + } + for s in &m.structs { + self.record(w, s); + } + for cb in &m.callback_interfaces { + self.callback_interface(w, cb); + } + for i in &m.interfaces { + self.class(w, i); + } + for f in &m.functions { + self.doc(w, &f.doc, &f.deprecated, &self.fn_tags(f)); + w.line(format!( + "export function {}({}): {};", + fn_name(&f.name), + self.params(f), + self.ret(f) + )); + } + for child in self.model.children(m) { + self.doc(w, &child.doc, &None, &[]); + w.block( + format!("export namespace {} {{", module_name(&child.name)), + "}", + |w| self.namespace_body(w, child, tables), + ); + } } -} -/// Emit a JSDoc block: the doc text, then `tags` (each a full tag line such -/// as `@deprecated use x`). Nothing when both are empty. -fn doc(w: &mut CodeWriter, text: Option<&str>, tags: &[String]) { - let text = text.map(str::trim).filter(|t| !t.is_empty()); - let mut lines: Vec = text - .map(|t| t.lines().map(str::to_string).collect()) - .unwrap_or_default(); - lines.extend(tags.iter().cloned()); - match lines.as_slice() { - [] => {} - [one] => { - w.line(format!("/** {one} */")); + /// Emit a JSDoc block: the doc text, then `tags` (each a full tag line + /// such as `@param x the count`), then `@deprecated` with the + /// deprecation message. Nothing when all are empty. + fn doc( + &self, + w: &mut CodeWriter, + doc: &Option, + deprecated: &Option, + tags: &[String], + ) { + let spell = doc_spelling(&self.names); + let doc = Doc::new(doc, deprecated); + let mut lines: Vec = doc + .text(&spell) + .map(|t| t.lines().map(str::to_string).collect()) + .unwrap_or_default(); + lines.extend(tags.iter().cloned()); + if let Some(msg) = doc.deprecation(&spell) { + lines.push(format!("@deprecated {msg}")); } - _ => { - w.line("/**"); - for l in &lines { - if l.is_empty() { - w.line(" *"); - } else { - w.line(format!(" * {l}")); + match lines.as_slice() { + [] => {} + [one] => { + w.line(format!("/** {one} */")); + } + _ => { + w.line("/**"); + for l in &lines { + if l.is_empty() { + w.line(" *"); + } else { + w.line(format!(" * {l}")); + } } + w.line(" */"); } - w.line(" */"); } } -} - -fn deprecated(msg: Option<&String>) -> Vec { - msg.map(|m| vec![format!("@deprecated {m}")]) - .unwrap_or_default() -} -/// The JSDoc tags of a callable: documented parameters, the domain it -/// throws, and any deprecation. -fn fn_tags(model: &Model, m: &ModuleBinding, f: &FnBinding) -> Vec { - let mut tags = param_tags(&f.params); - if let (true, Some(eb)) = (f.throws, model.error_domain(m)) { - let owner = error_owner(&model.modules, eb); - tags.push(format!( - "@throws {{{}}}", - ts_path_in(&owner.segments, &eb.type_name) - )); + /// The `@param` tags of documented parameters. + fn param_tags<'p>( + &self, + params: impl Iterator)>, + ) -> Vec { + params + .filter_map(|(name, doc)| { + let d = doc.as_deref()?.trim(); + (!d.is_empty()).then(|| { + format!( + "@param {} {}", + param_name(name), + rewrite_doc(&self.names, &d.replace('\n', " ")) + ) + }) + }) + .collect() } - tags.extend(deprecated(f.deprecated.as_ref())); - tags -} -fn param_tags(params: &[ParamBinding]) -> Vec { - params - .iter() - .filter_map(|p| { - let d = p.doc.as_deref()?.trim(); - (!d.is_empty()) - .then(|| format!("@param {} {}", param_name(&p.name), d.replace('\n', " "))) - }) - .collect() -} + /// The JSDoc tags of a callable: documented parameters and what it + /// throws. + fn fn_tags(&self, f: &FnBinding) -> Vec { + let mut tags = self.param_tags(f.params.iter().map(|p| (p.name.as_str(), &p.doc))); + match &f.error { + ErrorStrategy::Domain(domain) => tags.push(format!( + "@throws {{{}}} when the call fails with one of its codes", + ts_domain(self.model, domain) + )), + ErrorStrategy::Untyped => { + tags.push(format!( + "@throws {{{}}} (code -1) when the call fails", + self.root_error + )); + } + ErrorStrategy::Trap => {} + } + tags + } -fn params(model: &Model, f: &FnBinding) -> String { - let mut out: Vec = f - .params - .iter() - .map(|p| format!("{}: {}", param_name(&p.name), ts_type(model, &p.ty))) - .collect(); - if f.cancellable { - out.push("options?: { signal?: AbortSignal }".into()); + fn params(&self, f: &FnBinding) -> String { + let mut out: Vec = f + .params + .iter() + .map(|p| { + let ty = match &p.pass { + ArgPass::Slice { elem, .. } => ts_slice_input(*elem), + ArgPass::Callback { + interface, + nullable, + .. + } => { + let path = ts_type(self.model, &Ty::Interface(interface.clone())); + if *nullable { + format!("{path} | null") + } else { + path + } + } + _ => ts_type(self.model, p.ty.value().expect("a value parameter")), + }; + format!("{}: {ty}", param_name(&p.name)) + }) + .collect(); + if f.cancellable() { + out.push("options?: { signal?: AbortSignal }".into()); + } + out.join(", ") } - out.join(", ") -} -fn ret(model: &Model, f: &FnBinding) -> String { - let base = match (&f.shape, &f.ret) { - (CallShape::Iterator(it), _) => format!("IterableIterator<{}>", ts_type(model, &it.elem)), - (_, Some(ty)) => ts_type(model, ty), - (_, None) => "void".into(), - }; - if f.is_async() { - format!("Promise<{base}>") - } else { - base + fn ret(&self, f: &FnBinding) -> String { + let base = match &f.ret { + Some(RetTy::Iterator(elem)) => { + format!("NativeIterator<{}>", ts_type(self.model, elem)) + } + Some(RetTy::Value(ty)) => ts_type(self.model, ty), + None => "void".into(), + }; + if f.is_async() { + format!("Promise<{base}>") + } else { + base + } } -} -fn error_domain(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, eb: &ErrorBinding) { - let domain_ts = ts_path_in(&m.segments, &eb.type_name); - w.line(format!( - "/** The errors of the `{}` error domain. */", - m.dot_path - )); - w.block( - format!("export class {} extends $Error {{", eb.type_name), - "}", - |w| { - w.line("constructor(code: number, message?: string);"); - }, - ); - for c in &eb.codes { - doc(w, Some(c.doc.as_deref().unwrap_or(&c.message)), &[]); + fn error_domain(&self, w: &mut CodeWriter, table: &ErrorTable<'_>) { + let domain_ts = ts_path_in(&table.module.segments, &table.type_name); + w.line(format!( + "/** The errors of the `{}` domain (an unknown code is this class itself). */", + table.domain.name + )); w.block( - format!( - "export class {} extends {domain_ts} {{", - code_class(&c.name) - ), + format!("export class {} extends $Error {{", table.type_name), "}", |w| { - w.line(format!("static readonly CODE: {};", c.value)); - for f in &c.fields { - doc(w, f.doc.as_deref(), &[]); - w.line(format!("readonly {}: {};", f.name, ts_type(model, &f.ty))); - } - if c.fields.is_empty() { - w.line("constructor(message?: string);"); - } else { - let fields: Vec = c + w.line("constructor(code: number, message?: string);"); + }, + ); + for row in &table.codes { + let c = row.code; + let doc = Some(c.doc.clone().unwrap_or_else(|| c.message.clone())); + self.doc(w, &doc, &None, &[]); + w.block( + format!("export class {} extends {domain_ts} {{", row.type_name), + "}", + |w| { + w.line(format!("static readonly CODE: {};", c.value)); + for f in &c.fields { + self.doc(w, &f.doc, &None, &[]); + w.line(format!( + "readonly {}: {};", + f.name, + ts_type(self.model, &f.ty) + )); + } + if c.fields.is_empty() { + w.line("constructor(message?: string);"); + } else { + let fields: Vec = c + .fields + .iter() + .map(|f| format!("{}: {}", f.name, ts_type(self.model, &f.ty))) + .collect(); + w.line(format!( + "constructor(fields: {{ {} }}, message?: string);", + fields.join("; ") + )); + } + }, + ); + } + } + + fn enumeration(&self, w: &mut CodeWriter, e: &EnumBinding) { + self.doc(w, &e.doc, &e.deprecated, &[]); + if e.is_rich() { + w.line(format!("export type {} =", e.name)); + w.scope(|w| { + let last = e.variants.len().saturating_sub(1); + for (i, v) in e.variants.iter().enumerate() { + self.doc(w, &v.doc, &None, &[]); + let fields: String = v .fields .iter() - .map(|f| format!("{}: {}", f.name, ts_type(model, &f.ty))) + .map(|f| format!("; {}: {}", f.name, ts_type(self.model, &f.ty))) .collect(); - w.line(format!( - "constructor(fields: {{ {} }}, message?: string);", - fields.join("; ") - )); + let end = if i == last { ";" } else { "" }; + w.line(format!("| {{ tag: {}{fields} }}{end}", js_string(&v.name))); } - }, - ); + }); + return; + } + w.block(format!("export enum {} {{", e.name), "}", |w| { + for v in &e.variants { + self.doc(w, &v.doc, &None, &[]); + w.line(format!("{} = {},", v.name, v.value)); + } + }); } -} -fn enumeration(w: &mut CodeWriter, model: &Model, e: &EnumBinding) { - doc(w, e.doc.as_deref(), &deprecated(e.deprecated.as_ref())); - if e.rich { - w.line(format!("export type {} =", e.name)); - w.scope(|w| { - let last = e.variants.len().saturating_sub(1); - for (i, v) in e.variants.iter().enumerate() { - doc(w, v.doc.as_deref(), &[]); - let fields: String = v - .fields + fn record(&self, w: &mut CodeWriter, s: &StructBinding) { + self.doc(w, &s.doc, &s.deprecated, &[]); + w.block(format!("export interface {} {{", s.name), "}", |w| { + for f in &s.fields { + self.doc(w, &f.doc, &None, &[]); + w.line(format!("{}: {};", f.name, ts_type(self.model, &f.ty))); + } + }); + } + + /// A callback interface: any object with these methods implements it. + /// A method that throws a domain may raise that domain's errors, which + /// reach the library with their codes and fields. + fn callback_interface(&self, w: &mut CodeWriter, cb: &CallbackInterfaceBinding) { + self.doc(w, &cb.doc, &cb.deprecated, &[]); + w.block(format!("export interface {} {{", cb.name), "}", |w| { + for m in &cb.methods { + let mut tags = self.param_tags(m.params.iter().map(|p| (p.name.as_str(), &p.doc))); + match &m.error { + ErrorStrategy::Domain(domain) => tags.push(format!( + "@throws {{{}}} to report one of its codes, with its fields, to the library", + ts_domain(self.model, domain) + )), + ErrorStrategy::Untyped => { + tags.push("@throws to report a failure, with its message, to the library".into()); + } + ErrorStrategy::Trap => {} + } + self.doc(w, &m.doc, &m.deprecated, &tags); + let ps: Vec = m + .params .iter() - .map(|f| format!("; {}: {}", f.name, ts_type(model, &f.ty))) + .map(|p| { + format!( + "{}: {}", + param_name(&p.name), + ts_type(self.model, &p.ty) + ) + }) .collect(); - let end = if i == last { ";" } else { "" }; - w.line(format!("| {{ tag: {}{fields} }}{end}", js_string(&v.name))); + let r = match (&m.ret_pass, &m.ret) { + (CallbackRetPass::Slice { elem, .. }, _) => ts_slice_input(*elem), + (_, Some(ty)) => ts_type(self.model, ty), + (_, None) => "void".to_string(), + }; + w.line(format!( + "{}({}): {r};", + callback_method_name(&m.name), + ps.join(", ") + )); } }); - return; } - w.block(format!("export enum {} {{", e.name), "}", |w| { - for v in &e.variants { - doc(w, v.doc.as_deref(), &[]); - w.line(format!("{} = {},", v.name, v.value)); - } - }); -} -fn record(w: &mut CodeWriter, model: &Model, s: &StructBinding) { - doc(w, s.doc.as_deref(), &deprecated(s.deprecated.as_ref())); - w.block(format!("export interface {} {{", s.name), "}", |w| { - for f in &s.fields { - doc(w, f.doc.as_deref(), &[]); - w.line(format!("{}: {};", f.name, ts_type(model, &f.ty))); - } - }); -} - -/// A callback interface: any object with these methods implements it. A -/// method that throws may raise its module's domain errors, which reach the -/// library with their codes and fields. -fn callback_interface( - w: &mut CodeWriter, - model: &Model, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, -) { - let domain = model.error_domain(module).map(|eb| { - let owner = error_owner(&model.modules, eb); - ts_path_in(&owner.segments, &eb.type_name) - }); - doc(w, cb.doc.as_deref(), &deprecated(cb.deprecated.as_ref())); - w.block(format!("export interface {} {{", cb.name), "}", |w| { - for m in &cb.methods { - let mut tags = param_tags(&m.params); - if let (true, Some(domain)) = (m.throws, &domain) { - tags.push(format!( - "@throws {{{domain}}} to report that error, with its fields, to the library" - )); + fn class(&self, w: &mut CodeWriter, i: &InterfaceBinding) { + let self_ty = ts_type(self.model, &Ty::Interface(i.name.clone())); + self.doc(w, &i.doc, &i.deprecated, &[]); + w.block(format!("export class {} {{", i.name), "}", |w| { + let canonical = canonical_constructor(i); + match canonical { + Some(c) => { + self.doc(w, &c.doc, &c.deprecated, &self.fn_tags(c)); + w.line(format!("constructor({});", self.params(c))); + } + None => { + w.line("private constructor();"); + } } - tags.extend(deprecated(m.deprecated.as_ref())); - doc(w, m.doc.as_deref(), &tags); - let ps: Vec = m - .params + for c in i + .constructors .iter() - .map(|p| format!("{}: {}", param_name(&p.name), ts_type(model, &p.ty))) - .collect(); - let r = m - .ret - .as_ref() - .map_or_else(|| "void".to_string(), |t| ts_type(model, t)); - w.line(format!( - "{}({}): {r};", - callback_method_name(&m.name), - ps.join(", ") - )); - } - }); -} - -fn class(w: &mut CodeWriter, model: &Model, m: &ModuleBinding, i: &InterfaceBinding) { - let self_ty = ts_type(model, &Ty::Interface(i.name.clone())); - doc(w, i.doc.as_deref(), &deprecated(i.deprecated.as_ref())); - w.block(format!("export class {} {{", i.name), "}", |w| { - let canonical = i - .constructors - .iter() - .find(|c| c.name == "new" && !c.is_async()); - match canonical { - Some(c) => { - doc(w, c.doc.as_deref(), &fn_tags(model, m, c)); - w.line(format!("constructor({});", params(model, c))); + .filter(|c| canonical.is_none_or(|k| !std::ptr::eq(k, *c))) + { + self.doc(w, &c.doc, &c.deprecated, &self.fn_tags(c)); + let r = if c.is_async() { + format!("Promise<{self_ty}>") + } else { + self_ty.clone() + }; + w.line(format!( + "static {}({}): {r};", + member_name(&c.name, true), + self.params(c) + )); } - None => { - w.line("private constructor();"); + for f in &i.methods { + self.doc(w, &f.doc, &f.deprecated, &self.fn_tags(f)); + w.line(format!( + "{}({}): {};", + member_name(&f.name, false), + self.params(f), + self.ret(f) + )); } - } - for c in i - .constructors - .iter() - .filter(|c| canonical.is_none_or(|k| !std::ptr::eq(k, *c))) - { - doc(w, c.doc.as_deref(), &fn_tags(model, m, c)); - let r = if c.is_async() { - format!("Promise<{self_ty}>") - } else { - self_ty.clone() - }; - w.line(format!( - "static {}({}): {r};", - member_name(&c.name, true), - params(model, c) - )); - } - for f in &i.methods { - doc(w, f.doc.as_deref(), &fn_tags(model, m, f)); - w.line(format!( - "{}({}): {};", - member_name(&f.name, false), - params(model, f), - ret(model, f) - )); - } - for f in &i.statics { - doc(w, f.doc.as_deref(), &fn_tags(model, m, f)); - w.line(format!( - "static {}({}): {};", - member_name(&f.name, true), - params(model, f), - ret(model, f) - )); - } - w.line("/**"); - w.line(" * Release this wrapper's reference to the native object. Safe to call"); - w.line(" * more than once; a wrapper that is never closed is released when it is"); - w.line(" * garbage collected. Using the wrapper after `close()` throws."); - w.line(" */"); - w.line("close(): void;"); - w.line("/** Alias of `close()` for `using` declarations. */"); - w.line("[Symbol.dispose](): void;"); - }); + for f in &i.statics { + self.doc(w, &f.doc, &f.deprecated, &self.fn_tags(f)); + w.line(format!( + "static {}({}): {};", + member_name(&f.name, true), + self.params(f), + self.ret(f) + )); + } + w.line("/**"); + w.line(" * Release this wrapper's reference to the native object. Safe to call"); + w.line(" * more than once; a wrapper that is never closed is released when it is"); + w.line(" * garbage collected. Using the wrapper after `close()` throws."); + w.line(" */"); + w.line("close(): void;"); + w.line("/** Alias of `close()` for `using` declarations. */"); + w.line("[Symbol.dispose](): void;"); + }); + } } diff --git a/crates/weaveffi-cli/src/targets/js/mod.rs b/crates/weaveffi-cli/src/targets/js/mod.rs index 017e3b1c..8f3dd031 100644 --- a/crates/weaveffi-cli/src/targets/js/mod.rs +++ b/crates/weaveffi-cli/src/targets/js/mod.rs @@ -4,28 +4,31 @@ //! Both targets generate the same ES module: one exported namespace object //! per top-level IDL module (`export const kv = { Store, get, ... }`, with //! nested modules as nested namespaces), the same error classes (a root -//! `{Package}Error`, one `{Domain}Error` per error domain, one `{Code}Error` -//! per code, and `CancelledError`), records as plain objects, rich enums as -//! tagged unions, interfaces as classes with `close()`, `[Symbol.dispose]`, -//! and a `FinalizationRegistry` backstop, lazy iterators, `Promise`-returning -//! async functions with `AbortSignal` cancellation, and the same `.d.ts`. -//! Only the transport differs: Node.js calls an N-API addon, WebAssembly -//! stages values in linear memory. Both expose the raw calling convention -//! documented in [`api`] as an object named `$raw`. +//! `{Package}Error`, one class per error domain, one subclass per code, and +//! `CancelledError`), records as plain objects, rich enums as tagged unions, +//! interfaces as classes with `close()`, `[Symbol.dispose]`, and a +//! `FinalizationRegistry` backstop, lazy iterators with `close()`, +//! `Promise`-returning async functions with `AbortSignal` cancellation, and +//! the same `.d.ts`. Optional scalars cross as a flag and a value, numeric +//! lists as typed arrays. Only the transport differs: Node.js calls an +//! N-API addon, WebAssembly stages values in linear memory. Both expose the +//! raw calling convention documented in [`api`] as an object named `$raw`. //! -//! The fixed runtime (codec, error classes, object and iterator wrappers, -//! cancellation, the load-time checks) is a real JavaScript file, -//! `runtime/runtime.js`, emitted next to `index.js`. +//! The fixed runtime (codec, argument checks, error classes, object and +//! iterator wrappers, cancellation, the load-time checks) is a real +//! JavaScript file, `runtime/runtime.js`, emitted next to `index.js`. The +//! leak counters tests read are the package's `./debug` export +//! (`debug.js`), not part of its API. mod api; mod codec; mod dts; pub(crate) mod names; -use weaveffi_model::contract; use weaveffi_model::model::{Model, ABI_VERSION}; use weaveffi_model::pkg::Identity; +use crate::codegen::contract; use crate::codegen::CodeWriter; use crate::manifest::{JsonObject, JsonValue}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; @@ -41,9 +44,13 @@ const RUNTIME_IMPORTS: &[&str] = &[ "$fault", "$domain", "$raise", + "$untyped", "$W", "$WK", "$R", + "$check", + "$opt", + "$slice", "$encode", "$decode", "$Object", @@ -58,8 +65,17 @@ const RUNTIME_IMPORTS: &[&str] = &[ "$Iterator", "$cancellable", "$impl", - "$ret", "$verify", + "$setLive", +]; + +/// The files every JavaScript package ships beside the transport's own. +pub(crate) const PACKAGE_FILES: &[&str] = &[ + "index.js", + "index.d.ts", + "runtime.js", + "debug.js", + "debug.d.ts", ]; /// The root error class of a package: `{PascalName}Error` (`KvstoreError`). @@ -94,25 +110,26 @@ pub(crate) fn render_imports(w: &mut CodeWriter, identity: &Identity) { } /// The contract these bindings were generated with (`const $contract`): -/// for every top-level module, each declaration's `[id, hash, path]` from -/// [`contract::entries`], which the load-time check looks up in the -/// library's own tables. +/// for every top-level module, its table function and each declaration's +/// `[id, hash, path]` row from [`contract::tables`], which the load-time +/// check looks up in the library's own tables. pub(crate) fn render_contract(w: &mut CodeWriter, model: &Model) { w.line("// The declarations these bindings were generated with, per top-level"); - w.line("// module, as [id, hash, path]: the library's contract tables must hold"); - w.line("// each one (see $verify)."); + w.line("// module's table function, as [id, hash, path]: the library's tables must"); + w.line("// hold each one (see $verify)."); w.block("const $contract = [", "];", |w| { - for root in model.roots() { + for table in contract::tables(model) { w.block( - format!("[{}, [", names::js_string(&root.name)), + format!("[{}, [", names::js_string(&table.symbol)), "]],", |w| { - for e in contract::entries(model, root) { + for row in &table.rows { w.line(format!( - "[0x{:016x}n, 0x{:016x}n, {}],", - e.id, - e.hash, - names::js_string(&e.path) + "[{}n, {}n, {}], // {}", + contract::hex(row.id), + contract::hex(row.hash), + names::js_string(&row.path), + row.signature )); } }, @@ -123,26 +140,76 @@ pub(crate) fn render_contract(w: &mut CodeWriter, model: &Model) { /// The load-time check of the ABI revision and of `$contract` (see /// [`render_contract`]) against the library behind `$raw`. -pub(crate) fn verify_call(model: &Model, identity: &Identity) -> String { +pub(crate) fn verify_call(model: &Model) -> String { format!( "$verify($raw, {}, '{}', {ABI_VERSION}, $contract);", - names::js_string(&identity.name), + names::js_string(&model.identity.name), model.prefix(), ) } /// Render `index.d.ts`. `extra` carries the transport's own declarations. -pub(crate) fn render_dts(model: &Model, identity: &Identity, extra: &str) -> String { +pub(crate) fn render_dts(model: &Model, extra: &str) -> String { let mut w = CodeWriter::two_space(); w.raw(render_prelude(CommentStyle::DoubleSlash)); - dts::render_declarations(&mut w, model, &root_error_class(identity), extra); + dts::render_declarations(&mut w, model, &root_error_class(&model.identity), extra); w.blank(); w.raw(render_trailer(CommentStyle::DoubleSlash, "index.d.ts")); w.finish() } +/// Render `debug.js`, the package's `./debug` export: the leak counters a +/// test reads once the bindings are loaded (after `init()` on +/// WebAssembly). +pub(crate) fn render_debug_js() -> String { + let mut w = CodeWriter::two_space(); + w.raw(render_prelude(CommentStyle::DoubleSlash)); + w.line("// Diagnostics for tests of these bindings; not part of their API."); + w.line("import './index.js';"); + w.line("import { $live } from './runtime.js';"); + w.blank(); + w.line("/**"); + w.line(" * The native library's live-resource counter of `kind`: 0 objects,"); + w.line(" * 1 callback implementations, 2 iterators, 3 cancel tokens, 4 byte runs."); + w.line(" * Kind -1 is `1n` when the library counts at all (its `leak-check`"); + w.line(" * feature); otherwise every kind is `0n`."); + w.line(" */"); + w.block("export function debugLive(kind) {", "}", |w| { + w.line("return $live(kind);"); + }); + w.blank(); + w.raw(render_trailer(CommentStyle::DoubleSlash, "debug.js")); + w.finish() +} + +/// Render `debug.d.ts`. +pub(crate) fn render_debug_dts() -> String { + let mut w = CodeWriter::two_space(); + w.raw(render_prelude(CommentStyle::DoubleSlash)); + w.line("/**"); + w.line(" * The native library's live-resource counter of `kind`: 0 objects,"); + w.line(" * 1 callback implementations, 2 iterators, 3 cancel tokens, 4 byte runs."); + w.line(" * Kind -1 is `1n` when the library counts at all (its `leak-check`"); + w.line(" * feature); otherwise every kind is `0n`."); + w.line(" */"); + w.line("export declare function debugLive(kind: number): bigint;"); + w.blank(); + w.raw(render_trailer(CommentStyle::DoubleSlash, "debug.d.ts")); + w.finish() +} + +/// The npm tarball file name `npm pack` gives `name` at `version` +/// (`kvstore-1.2.0.tgz`, or `acme-kv-1.2.0.tgz` for `@acme/kv`). +pub(crate) fn npm_tarball_name(name: &str, version: &str) -> String { + format!( + "{}-{version}.tgz", + name.trim_start_matches('@').replace('/', "-") + ) +} + /// The npm metadata both `package.json` files share: the generated-file -/// notice, then name, version, description, and the optional fields. +/// notice, then name, version, description, the optional fields, and the +/// ES module entry points (`.` and `./debug`). pub(crate) fn npm_metadata(identity: &Identity, name: &str) -> JsonObject { let version = env!("CARGO_PKG_VERSION"); let mut obj = JsonObject::new() @@ -170,44 +237,50 @@ pub(crate) fn npm_metadata(identity: &Identity, name: &str) -> JsonObject { ), ); } + let entry = |types: &str, js: &str| { + JsonValue::Object( + JsonObject::new() + .str_entry("types", types) + .str_entry("default", js), + ) + }; obj.str_entry("type", "module") .str_entry("main", "index.js") .str_entry("types", "index.d.ts") .entry( "exports", JsonValue::Object( - JsonObject::new().entry( - ".", - JsonValue::Object( - JsonObject::new() - .str_entry("types", "./index.d.ts") - .str_entry("default", "./index.js"), - ), - ), + JsonObject::new() + .entry(".", entry("./index.d.ts", "./index.js")) + .entry("./debug", entry("./debug.d.ts", "./debug.js")), ), ) } /// A small API exercising every shape both JavaScript targets render, for -/// their unit tests: an error domain with a payload, a C-style and a rich -/// enum, a record holding objects, two callback interfaces (one with a -/// return of every family and a method that throws), an interface with a -/// canonical constructor, a method named `close`, a throwing method, an -/// iterator of objects, a nullable async result, and a cancellable async -/// method, free functions (one named after a keyword, one taking an +/// their unit tests: two error domains (one with a payload), a C-style and +/// a rich enum, a record holding objects, a callback interface with a +/// return of every family and methods of every error strategy, an +/// interface with a canonical constructor, a method named `close`, a +/// throwing method, an iterator of objects, a nullable async result, and a +/// cancellable async method, optional scalars and typed arrays in every +/// position, free functions (one named after a keyword, one taking an /// optional callback), and a nested module. #[cfg(test)] -pub(crate) fn test_api(name: &str) -> weaveffi_model::model::Model { +pub(crate) fn test_api(name: &str) -> Model { const YAML: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: kv doc: A key-value store. errors: - name: KvError - codes: - - { name: KEY_NOT_FOUND, code: 1, message: "key not found", fields: [{ name: key, type: string }] } - - { name: FULL, code: 2, message: "it's full" } + - name: KvError + codes: + - { name: KEY_NOT_FOUND, code: 1, message: "key not found", fields: [{ name: key, type: string }] } + - { name: FULL, code: 2, message: "it's full" } + - name: Other + codes: + - { name: Broken, code: 1, message: "broken" } enums: - name: Mode variants: @@ -225,44 +298,53 @@ modules: - { name: extras, type: "[Store]" } - { name: stamp, type: i64 } - { name: by_mode, type: "{Mode:bool}" } + - { name: weights, type: "[f64]" } callback_interfaces: - name: Listener methods: - name: on_message - params: [{ name: text, type: string }, { name: weight, type: u64 }] + params: [{ name: text, type: string }, { name: weight, type: u64 }, { name: level, type: "u8?" }] return: bool - name: on_bundle params: [{ name: bundle, type: Bundle }, { name: store, type: Store }, { name: alt, type: "Store?" }] - name: Policy methods: - - { name: admit, params: [{ name: bundle, type: Bundle }], return: Bundle, throws: true } - - { name: label, params: [], return: string } + - { name: admit, params: [{ name: bundle, type: Bundle }], return: Bundle, throws: KvError } + - { name: label, params: [], return: string, throws: any } - { name: blob, params: [], return: bytes } - { name: pick, params: [{ name: key, type: string }], return: Store } - { name: maybe, params: [], return: "Store?" } - { name: mode, params: [], return: Mode } - { name: weight, params: [], return: i64 } + - { name: limit, params: [{ name: hint, type: "i32?" }], return: "i16?" } + - { name: scores, params: [{ name: sizes, type: "[u64]" }], return: "[f32]", throws: Other } interfaces: - name: Store doc: A store. constructors: - { name: new, params: [{ name: path, type: string }] } methods: - - { name: get, params: [{ name: key, type: string }], return: bytes, throws: true } + - { name: get, params: [{ name: key, type: string }], return: bytes, throws: KvError } - { name: close, params: [] } - { name: scan, params: [], return: "iter" } - { name: fetch, params: [{ name: path, type: string }], return: "Store?", async: true } - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } + - { name: ttl, params: [{ name: key, type: string }], return: "i64?", async: true } + - { name: sizes, params: [], return: "[u32]", async: true, throws: any } functions: - { name: subscribe, params: [{ name: listener, type: Listener }, { name: mode, type: Mode }] } - { name: install, params: [{ name: policy, type: "Policy?" }] } - - { name: widen, params: [{ name: n, type: i64 }, { name: m, type: u64 }], return: u64 } + - { name: widen, params: [{ name: n, type: i64 }, { name: m, type: u64 }, { name: b, type: i8 }], return: u64 } - { name: delete, params: [{ name: class, type: "Store?" }], return: bool } - { name: shapes, params: [], return: "iter" } + - { name: maybe, params: [{ name: x, type: "u16?" }, { name: mode, type: "Mode?" }], return: "f32?", throws: Other } + - { name: totals, params: [{ name: xs, type: "[i32]" }, { name: ys, type: "[u64]" }], return: "[f64]" } + - { name: levels, params: [], return: "iter" } + - { name: chunks, params: [], return: "iter<[i16]>" } modules: - name: stats functions: - - { name: count, params: [{ name: store, type: Store }], return: i64, throws: true } + - { name: count, params: [{ name: store, type: Store }], return: i64, throws: KvError } "#; let api = weaveffi_model::parse::parse_api_str(YAML, "yaml").expect("test fixture parses"); weaveffi_model::validate::validate(&api, &Identity::named(name), None) @@ -282,196 +364,44 @@ mod tests { ); assert!(rt.contains("export class CancelledError extends MyKvError {")); assert!(!rt.contains("{{")); - assert!(!rt.contains("checksum")); assert_eq!(root_error_class(&Identity::named("io_error")), "IoError"); } - fn api_js() -> String { - let mut w = CodeWriter::two_space(); - render_api(&mut w, &test_api("acme-kv")); - w.finish() - } - - fn has(src: &str, needle: &str) { - assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); - } - #[test] - fn the_contract_lists_every_declaration_with_its_path() { + fn the_contract_lists_every_row_by_table_function() { let model = test_api("acme-kv"); let mut w = CodeWriter::two_space(); render_contract(&mut w, &model); let js = w.finish(); - let root = model.roots().next().unwrap(); - let entries = contract::entries(&model, root); - assert_eq!(js.matches("n, 0x").count(), entries.len()); - let put = entries.iter().find(|e| e.path == "kv.Store.get").unwrap(); - has(&js, "const $contract = [\n ['kv', [\n"); - has( - &js, - &format!( - " [0x{:016x}n, 0x{:016x}n, 'kv.Store.get'],\n", - put.id, put.hash - ), - ); + let tables = contract::tables(&model); + assert_eq!(js.matches("n, 0x").count(), tables[0].rows.len()); + assert!(js.contains("const $contract = [\n ['acme_kv_kv_contract', [\n")); assert_eq!( - verify_call(&model, &model.identity), - "$verify($raw, 'acme-kv', 'acme_kv', 4, $contract);" + verify_call(&model), + "$verify($raw, 'acme-kv', 'acme_kv', 5, $contract);" ); } #[test] - fn modules_are_frozen_namespaces_with_escaped_members() { - let js = api_js(); - has(&js, "export const kv = Object.freeze({"); - has(&js, " Store: kv$Store,\n"); - has(&js, " delete_: kv$delete_,\n"); - has( - &js, - " stats: Object.freeze({\n count: kv$stats$count,\n }),\n});", - ); - has(&js, "const kv$delete_ = function delete_(class_) {"); - has(&js, " close_() {"); - has(&js, " 7: 'Safe',\n '-1': 'Off',\n"); + fn npm_tarball_names_follow_npm_pack() { + assert_eq!(npm_tarball_name("kv", "1.0.0"), "kv-1.0.0.tgz"); + assert_eq!(npm_tarball_name("@acme/kv", "1.0.0"), "acme-kv-1.0.0.tgz"); } #[test] - fn calls_lend_objects_and_map_faults_by_strategy() { - let js = api_js(); - has( - &js, - " get(key) {\n const $self = $lend(this, kv$Store);\n try {\n return $raw.acme_kv_kv_Store_get($self, key);\n } catch ($e) {\n throw $from$kv$KvError($e);\n } finally {\n $unlend(this);\n }\n }", - ); - has(&js, "const $o0 = $lendOpt(class_, kv$Store);"); - has(&js, "throw $fault($e);"); - has( - &js, - "return $domain(e, $codes$kv$KvError, $payloads$kv$KvError);", - ); - has( - &js, - "const $payloads$kv$KvError = new Map([\n [1, (r) => ({ key: r.readString() })],\n]);", - ); - } - - #[test] - fn error_codes_with_fields_take_them_first() { - let js = api_js(); - has( - &js, - " constructor(fields, message = 'key not found') {\n super(1, message);\n this.key = fields.key;\n }", - ); - has( - &js, - " constructor(message = 'it\\'s full') {\n super(2, message);\n }", - ); - has( - &js, - "const $fields$kv$KvError = new Map([\n [1, (w, e) => { w.writeString(e.key); }],\n]);", - ); - has( - &js, - "return $raise(e, kv$KvError, $codes$kv$KvError, $fields$kv$KvError);", - ); - } - - #[test] - fn async_and_iterators_follow_the_raw_convention() { - let js = api_js(); - has( - &js, - "return await $cancellable($options?.signal, $tokens, ($t) => $raw.acme_kv_kv_Store_wait($self, ms, $t));", - ); - has( - &js, - "return $adoptOpt(kv$Store, await $raw.acme_kv_kv_Store_fetch($self, path));", - ); - has( - &js, - "return new $Iterator($raw.acme_kv_kv_Store_scan($self), $it$kv$Store$scan);", - ); - has(&js, "convert: (v) => $adopt(kv$Store, v),"); - has(&js, "convert: (v) => $decode(v, $r$kv$Shape),"); - } - - #[test] - fn adapters_convert_arguments_and_every_return_family() { - let js = api_js(); - has( - &js, - "return $ret.Bool(impl.onMessage($a0, $a1), 'Listener.onMessage');", - ); - has(&js, "impl.onBundle($decode($a0, $r$kv$Bundle), $adopt(kv$Store, $a1), $adoptOpt(kv$Store, $a2));"); - has( - &js, - " admit($a0) {\n try {\n return $encode(impl.admit($decode($a0, $r$kv$Bundle)), $w$kv$Bundle);\n } catch ($e) {\n throw $raise$kv$KvError($e);\n }\n },", - ); - has(&js, "return $ret.String(impl.label(), 'Policy.label');"); - has(&js, "return $ret.Bytes(impl.blob(), 'Policy.blob');"); - has(&js, "return $clone(impl.pick($a0), kv$Store);"); - has(&js, "return $cloneOpt(impl.maybe(), kv$Store);"); - has(&js, "return $ret.I32(impl.mode(), 'Policy.mode');"); - has(&js, "return $ret.I64(impl.weight(), 'Policy.weight');"); - has( - &js, - "$raw.acme_kv_kv_subscribe($adapt$kv$Listener(listener), mode);", - ); - has( - &js, - "$raw.acme_kv_kv_install(policy == null ? null : $adapt$kv$Policy(policy));", - ); - } - - #[test] - fn codecs_name_one_function_per_type() { - let js = api_js(); - has(&js, " $w$kv$Store(w, v.primary);\n"); - has(&js, " primary: $r$kv$Store(r),\n"); - has(&js, " $w_list_Store(w, v.extras);\n"); - has( - &js, - "function $w_map_Mode_bool(w, v) {\n w.writeMap(v, $WK.I32, $W.Bool);\n}", - ); - has( - &js, - "function $w$kv$Store(w, v) {\n w.writeU64(BigInt($clone(v, kv$Store)));\n}", - ); - has( - &js, - "function $r$kv$Store(r) {\n return $adopt(kv$Store, $token(r.readU64()));\n}", - ); - } - - #[test] - fn declarations_qualify_types_and_alias_the_root_error() { - let model = test_api("kv"); - let dts = render_dts(&model, &Identity::named("kv"), ""); - has(&dts, "export declare class KvError extends Error {"); - has(&dts, "declare const $Error: typeof KvError;"); - has(&dts, "export declare namespace kv {"); - has(&dts, " export class KvError extends $Error {"); - has(&dts, " export class KeyNotFoundError extends kv.KvError {"); - has(&dts, " readonly key: string;"); - has( - &dts, - " constructor(fields: { key: string }, message?: string);", - ); - has( - &dts, - " wait(ms: bigint, options?: { signal?: AbortSignal }): Promise;", - ); - has(&dts, " scan(): IterableIterator;"); - has(&dts, " close_(): void;"); - has(&dts, " by_mode: Partial>;"); - has( - &dts, - " export function delete_(class_: kv.Store | null): boolean;", - ); - has( - &dts, - " export function install(policy: kv.Policy | null): void;", - ); - has(&dts, " maybe(): kv.Store | null;"); - has(&dts, " export function count(store: kv.Store): bigint;"); + fn modules_are_frozen_namespaces_with_escaped_members() { + let mut w = CodeWriter::two_space(); + render_api(&mut w, &test_api("acme-kv")); + let js = w.finish(); + for needle in [ + "export const kv = Object.freeze({", + " delete_: kv$delete_,\n", + " stats: Object.freeze({\n count: kv$stats$count,\n }),\n});", + "const kv$delete_ = function delete_(class_) {", + " close_() {", + " 7: 'Safe',\n '-1': 'Off',\n", + ] { + assert!(js.contains(needle), "missing `{needle}` in:\n{js}"); + } } } diff --git a/crates/weaveffi-cli/src/targets/js/names.rs b/crates/weaveffi-cli/src/targets/js/names.rs index a3372038..7e924cfc 100644 --- a/crates/weaveffi-cli/src/targets/js/names.rs +++ b/crates/weaveffi-cli/src/targets/js/names.rs @@ -6,16 +6,18 @@ //! Identifiers that are reserved words gain a trailing underscore. //! * **Declaration names** are the module-scope bindings of user //! declarations, the declaring module's path and the name joined with `$` -//! (`kv$Store`, `kv$stats$getStats`). IDL identifiers never contain `$`, so two modules -//! declaring the same name never clash, and no declaration can shadow a -//! JavaScript global. +//! (`kv$Store`, `kv$stats$getStats`). IDL identifiers never contain `$`, +//! so two modules declaring the same name never clash, and no declaration +//! can shadow a JavaScript global. //! * **Helper names** start with `$` (`$lend`, `$w$kv$Entry`): runtime //! imports and generated support code, which no IDL name can reach. use heck::ToLowerCamelCase; -use weaveffi_model::model::{ErrorBinding, FnBinding, Model, ModuleBinding}; +use weaveffi_model::model::Model; +use weaveffi_model::plan::ErrorStrategy; use weaveffi_model::ty::{Prim, Ty, WireType}; +use crate::codegen::docs::{self, ApiNames, IdentKind}; use crate::lang; /// Identifiers that are not JS keywords but can't name a binding or @@ -75,21 +77,21 @@ pub(crate) fn decl(segments: &[String], name: &str) -> String { format!("{}${name}", segments.join("$")) } -/// The declaration name of the user type `name`, from its declaring -/// module's path (`Store` in `kv` becomes `kv$Store`). +/// The declaration name of the user type or error domain `name`, from its +/// declaring module's path (`Store` in `kv` becomes `kv$Store`). pub(crate) fn type_decl(model: &Model, name: &str) -> String { decl(&model.owner(name).segments, name) } -/// A helper name derived from a user type (`$w` and `Entry` in `kv` give -/// `$w$kv$Entry`). +/// A helper name derived from a user type or error domain (`$w` and +/// `Entry` in `kv` give `$w$kv$Entry`). pub(crate) fn helper(model: &Model, kind: &str, name: &str) -> String { format!("${kind}${}", type_decl(model, name)) } /// The public name of a module (its namespace object). -pub(crate) fn module_name(m: &ModuleBinding) -> String { - js_ident(&m.name) +pub(crate) fn module_name(name: &str) -> String { + js_ident(name) } /// The TypeScript path of `name` declared in module `segments`, each module @@ -105,37 +107,25 @@ pub(crate) fn ts_path(model: &Model, name: &str) -> String { ts_path_in(&model.owner(name).segments, name) } -/// The error class name of an error code (`KEY_NOT_FOUND` becomes -/// `KeyNotFoundError`). -pub(crate) fn code_class(code: &str) -> String { - weaveffi_model::errors::type_name(code, "Error") +/// The class name of error domain `name` or of an error code `name` +/// (`KvError` stays `KvError`, `KEY_NOT_FOUND` becomes `KeyNotFoundError`). +pub(crate) fn error_class(name: &str) -> String { + weaveffi_model::errors::type_name(name, "Error") } -/// The module that declares error domain `eb`. -pub(crate) fn error_owner<'a>( - modules: &'a [ModuleBinding], - eb: &ErrorBinding, -) -> &'a ModuleBinding { - modules - .iter() - .find(|m| m.path == eb.owner_path && m.errors.is_some()) - .expect("an error binding's owner module declares the domain") +/// The TypeScript path of error domain `name`'s class (`kv.KvError`). +pub(crate) fn ts_domain(model: &Model, name: &str) -> String { + ts_path_in(&model.owner(name).segments, &error_class(name)) } -/// The function that maps a native failure of callable `f` (declared in a -/// module whose domain is `error`) onto the error classes: the domain's -/// mapper for a throwing callable, the runtime's `$fault` otherwise. -pub(crate) fn error_mapper( - modules: &[ModuleBinding], - f: &FnBinding, - error: Option<&ErrorBinding>, -) -> String { - match error { - Some(eb) if f.throws => { - let owner = error_owner(modules, eb); - format!("$from${}", decl(&owner.segments, &eb.name)) - } - _ => "$fault".to_string(), +/// The function that maps a native failure of a callable with error +/// strategy `error` onto the error classes: its domain's mapper +/// (`$from$kv$KvError`) when it throws a domain, the runtime's `$fault` +/// otherwise (a trap, or an untyped failure, is the root error class). +pub(crate) fn error_mapper(model: &Model, error: &ErrorStrategy) -> String { + match error.domain() { + Some(domain) => helper(model, "from", domain), + None => "$fault".to_string(), } } @@ -156,11 +146,7 @@ pub(crate) fn ts_type(model: &Model, ty: &Ty) -> String { Ty::Prim(Prim::Bool) => "boolean".into(), Ty::Prim(Prim::String) => "string".into(), Ty::Prim(Prim::Bytes) => "Uint8Array".into(), - Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Enum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(n) => ts_path(model, n), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => ts_path(model, n), Ty::Optional(inner) => format!("{} | null", ts_type(model, inner)), Ty::List(inner) => { let t = ts_type(model, inner); @@ -180,24 +166,100 @@ pub(crate) fn ts_type(model: &Model, ty: &Ty) -> String { _ => format!("Record"), } } - Ty::Iterator(inner) => format!("IterableIterator<{}>", ts_type(model, inner)), } } -/// The wire primitive a direct-family type is read and written as (C-style -/// enums travel as their `i32` discriminant). +/// The TypeScript type a typed-array (Slice) value of element `elem` +/// accepts where the consumer supplies it (an argument, or a callback +/// method's return): a plain array or the matching typed array +/// (`readonly number[] | Float64Array`). +pub(crate) fn ts_slice_input(elem: Prim) -> String { + let scalar = ts_type_prim(elem); + format!("readonly {scalar}[] | {}", typed_array(elem)) +} + +/// The TypeScript type of primitive `p`. +fn ts_type_prim(p: Prim) -> &'static str { + match p { + Prim::I64 | Prim::U64 => "bigint", + Prim::Bool => "boolean", + Prim::String => "string", + Prim::Bytes => "Uint8Array", + _ => "number", + } +} + +/// The typed array class a Slice of `elem` crosses as (`Float64Array` for +/// `f64`, `BigInt64Array` for `i64`). /// /// # Panics /// -/// Panics on a type outside the direct family. -pub(crate) fn direct_prim(ty: &Ty) -> Prim { +/// Panics on a primitive that never crosses as a typed array (`bool`, +/// `u8`, `string`, `bytes`). +pub(crate) fn typed_array(elem: Prim) -> &'static str { + match elem { + Prim::I8 => "Int8Array", + Prim::I16 => "Int16Array", + Prim::I32 => "Int32Array", + Prim::I64 => "BigInt64Array", + Prim::U16 => "Uint16Array", + Prim::U32 => "Uint32Array", + Prim::U64 => "BigUint64Array", + Prim::F32 => "Float32Array", + Prim::F64 => "Float64Array", + Prim::Bool | Prim::U8 | Prim::String | Prim::Bytes => { + unreachable!("{elem} never crosses as a typed array") + } + } +} + +/// The runtime's name for the scalar a direct-family type is checked and +/// buffered as (`I32`, `U64`, `Bool`; C-style enums travel as their `i32` +/// discriminant): the key into `$W`, `$R`, `$ret`, and `$check`. +/// +/// # Panics +/// +/// Panics on a type that isn't a scalar or a C-style enum. +pub(crate) fn scalar_kind(ty: &Ty) -> &'static str { match ty.wire() { - WireType::Prim(p) => p, - WireType::Enum(_) => Prim::I32, - other => unreachable!("not a direct type: {other:?}"), + WireType::Prim(p) if p.is_scalar() => p.pascal(), + WireType::Enum(_) => "I32", + other => unreachable!("not a scalar: {other:?}"), + } +} + +/// The spelling closure doc and deprecation text is rewritten with: an +/// identifier in backticks becomes its JavaScript name (`new_op` becomes +/// `newOp`, an error domain or code its class), and each segment of a +/// dotted path (`Store.get`) is spelled on its own. +pub(crate) fn doc_spelling(names: &ApiNames) -> impl Fn(&str) -> Option + '_ { + let one = move |ident: &str| -> Option { + Some(match names.kind(ident)? { + IdentKind::Function => fn_name(ident), + IdentKind::Member | IdentKind::CallbackMethod | IdentKind::Param => { + ident.to_lower_camel_case() + } + IdentKind::ErrorDomain | IdentKind::ErrorCode => error_class(ident), + IdentKind::Module | IdentKind::Type | IdentKind::Variant | IdentKind::Field => { + return None + } + }) + }; + move |path: &str| { + let parts: Vec = path + .split('.') + .map(|p| one(p).unwrap_or_else(|| p.to_string())) + .collect(); + let spelled = parts.join("."); + (spelled != path).then_some(spelled) } } +/// `text` rewritten with [`doc_spelling`]. +pub(crate) fn rewrite_doc(names: &ApiNames, text: &str) -> String { + docs::rewrite(text, doc_spelling(names)) +} + /// A single-quoted JavaScript string literal of `s`. pub(crate) fn js_string(s: &str) -> String { let mut out = String::with_capacity(s.len() + 2); @@ -238,21 +300,25 @@ pub(crate) mod tests { /// `Entry` and `Kind` in `kv`, `C` in `a.b`, `Widget` in `class.inner`. pub(crate) fn model() -> Model { - let yaml = r#" -version: "0.11.0" + crate::codegen::test_model( + r#" +version: "0.12.0" modules: - name: kv enums: [{ name: Kind, variants: [{ name: A, value: 0 }] }] structs: [{ name: Entry, fields: [{ name: k, type: i32 }] }] interfaces: [{ name: Store, methods: [{ name: get }] }] + errors: + - name: KvErrors + codes: [{ name: KEY_NOT_FOUND, code: 1, message: missing }] + functions: + - { name: open_store, params: [{ name: ttl_seconds, type: i64 }] } - name: a modules: [{ name: b, structs: [{ name: C, fields: [{ name: x, type: i32 }] }] }] - name: class modules: [{ name: inner, structs: [{ name: Widget, fields: [{ name: x, type: i32 }] }] }] -"#; - let api = weaveffi_model::parse::parse_api_str(yaml, "yaml").unwrap(); - weaveffi_model::validate::validate(&api, &weaveffi_model::pkg::Identity::named("t"), None) - .unwrap() +"#, + ) } #[test] @@ -262,7 +328,10 @@ modules: assert_eq!(decl(&segs, "C"), "a$b$C"); assert_eq!(type_decl(&m, "C"), "a$b$C"); assert_eq!(helper(&m, "w", "Entry"), "$w$kv$Entry"); + assert_eq!(helper(&m, "from", "KvErrors"), "$from$kv$KvErrors"); assert_eq!(ts_path(&m, "Widget"), "class_.inner.Widget"); + assert_eq!(ts_domain(&m, "KvErrors"), "kv.KvError"); + assert_eq!(error_class("KEY_NOT_FOUND"), "KeyNotFoundError"); } #[test] @@ -296,6 +365,24 @@ modules: ), "Record" ); + assert_eq!( + ts_slice_input(Prim::U64), + "readonly bigint[] | BigUint64Array" + ); + assert_eq!(scalar_kind(&Ty::Enum("Kind".into())), "I32"); + } + + #[test] + fn doc_identifiers_take_their_javascript_spelling() { + let m = model(); + let names = ApiNames::new(&m); + assert_eq!( + rewrite_doc( + &names, + "Use `open_store` with `ttl_seconds`, or catch `KvErrors` (`KEY_NOT_FOUND`) from `Store.get`; see `Entry`." + ), + "Use `openStore` with `ttlSeconds`, or catch `KvError` (`KeyNotFoundError`) from `Store.get`; see `Entry`." + ); } #[test] diff --git a/crates/weaveffi-cli/src/targets/js/runtime/runtime.js b/crates/weaveffi-cli/src/targets/js/runtime/runtime.js index e740a252..a1de4c10 100644 --- a/crates/weaveffi-cli/src/targets/js/runtime/runtime.js +++ b/crates/weaveffi-cli/src/targets/js/runtime/runtime.js @@ -61,17 +61,18 @@ export function $fault(e) { * Map a fault onto an error domain: `codes` maps each declared code to its * class, and `payloads` maps each code that carries fields to the reader * that decodes them (the class's constructor then takes the fields first). - * Codes outside the domain fall back to `$fault`. + * Domains are open: a positive code the bindings don't know (a newer + * library added it) becomes the domain class `base` itself, with the code + * and message. Runtime codes (negative) fall back to `$fault`. */ -export function $domain(e, codes, payloads) { - if (e instanceof $Fault) { +export function $domain(e, base, codes, payloads) { + if (e instanceof $Fault && e.code > 0) { const cls = codes.get(e.code); - if (cls !== undefined) { - const message = e.message || undefined; - const read = payloads.get(e.code); - if (read === undefined) return new cls(message); - return new cls(e.payload === null ? {} : $decode(e.payload, read), message); - } + if (cls === undefined) return new base(e.code, e.message); + const message = e.message || undefined; + const read = payloads.get(e.code); + if (read === undefined) return new cls(message); + return new cls(e.payload === null ? {} : $decode(e.payload, read), message); } return $fault(e); } @@ -80,31 +81,170 @@ export function $domain(e, codes, payloads) { * The fault a callback implementation reports for exception `e` from a * method that throws the error domain `domain`: a declared code of the * domain travels with its fields (`fields` maps each code that has any to - * the writer that encodes them); anything else is rethrown unchanged and - * reported as a code -4 failure. + * the writer that encodes them); anything else is an untyped failure (see + * `$untyped`). */ export function $raise(e, domain, codes, fields) { if (e instanceof domain && codes.has(e.code)) { const write = fields.get(e.code); return new $Fault(e.code, e.message, write === undefined ? null : $encode(e, write)); } - return e; + return $untyped(e); +} + +/** + * The fault a callback implementation reports for exception `e` from a + * method declared `throws: any` (or for one its domain doesn't cover): + * code -1 with the exception's message. + */ +export function $untyped(e) { + if (e instanceof $Fault) return e; + return new $Fault(-1, $messageOf(e), null); +} + +/** The message of a thrown value: an error's message (or name), else its text. */ +function $messageOf(e) { + if (e instanceof Error) return e.message || e.name; + try { + return String(e); + } catch { + return 'callback implementation failed'; + } } function $malformed(what) { return new {{ERROR_CLASS}}(-3, 'malformed value buffer: ' + what); } +/** How a value reads in an error message: its type, or a number's value. */ +function $show(v) { + if (v === null) return 'null'; + if (typeof v === 'bigint') return `${v}n`; + if (typeof v === 'number') return String(v); + return typeof v; +} + function $expected(what, v) { - return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); + return new TypeError(`expected ${what}, got ${$show(v)}`); +} + +/** `expected {what} to be {shape}` (or `expected {shape}` without a label). */ +function $want(what, shape) { + return what === undefined ? shape : `${what} to be ${shape}`; +} + +/** The inclusive range of each narrow integer kind. */ +const $RANGE = Object.freeze({ + I8: [-128, 127], + I16: [-32768, 32767], + I32: [-2147483648, 2147483647], + U8: [0, 255], + U16: [0, 65535], + U32: [0, 4294967295], +}); + +function $int(kind) { + const [lo, hi] = $RANGE[kind]; + return (v, what) => { + if (typeof v !== 'number') throw $expected($want(what, 'a number'), v); + if (!Number.isInteger(v) || v < lo || v > hi) { + throw new RangeError(`expected ${$want(what, `an integer in [${lo}, ${hi}]`)}, got ${v}`); + } + return v; + }; +} + +function $int64(signed) { + const fit = signed ? BigInt.asIntN : BigInt.asUintN; + const shape = signed ? 'a signed 64-bit integer' : 'an unsigned 64-bit integer'; + return (v, what) => { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number') { + if (!Number.isInteger(v)) throw new RangeError(`expected ${$want(what, shape)}, got ${v}`); + b = BigInt(v); + } else { + throw $expected($want(what, 'a bigint'), v); + } + if (fit(64, b) !== b) throw new RangeError(`expected ${$want(what, shape)}, got ${b}`); + return b; + }; } -function $bigint(v) { - if (typeof v === 'bigint') return v; - if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); - throw $expected('a bigint', v); +function $float(v, what) { + if (typeof v !== 'number') throw $expected($want(what, 'a number'), v); + return v; } +/** + * Value checks by kind, each `(v, what?) => v`: a value of the wrong type + * throws a `TypeError`, and an integer that is fractional or out of its + * kind's range a `RangeError` (64-bit kinds take a `bigint` or an integral + * `number` and return a `bigint`). `what` names the value in the message. + * The value-buffer writer, callback returns, and typed-array arguments all + * check through these, so a value never wraps silently. + */ +export const $check = Object.freeze({ + Bool(v, what) { + if (typeof v !== 'boolean') throw $expected($want(what, 'a boolean'), v); + return v; + }, + I8: $int('I8'), + I16: $int('I16'), + I32: $int('I32'), + U8: $int('U8'), + U16: $int('U16'), + U32: $int('U32'), + I64: $int64(true), + U64: $int64(false), + F32: $float, + F64: $float, + String(v, what) { + if (typeof v !== 'string') throw $expected($want(what, 'a string'), v); + return v; + }, + Bytes(v, what) { + if (!(v instanceof Uint8Array)) throw $expected($want(what, 'a Uint8Array'), v); + return v; + }, +}); + +/** An optional scalar: `null` (or `undefined`) for none, else `check(v)`. */ +export function $opt(v, check, what) { + return v === null || v === undefined ? null : check(v, what); +} + +function $typed(Typed, check) { + return (v, what) => { + if (v instanceof Typed) return v; + if (!Array.isArray(v)) { + throw $expected($want(what, `an array or a ${Typed.name}`), v); + } + const out = new Typed(v.length); + const label = what === undefined ? 'element' : what; + for (let i = 0; i < v.length; i++) out[i] = check(v[i], `${label}[${i}]`); + return out; + }; +} + +/** + * Typed-array conversions by element kind, each `(v, what?) => typedArray`: + * the matching typed array passes through as is, and a plain array is + * checked element by element (see `$check`) and copied into one. + */ +export const $slice = Object.freeze({ + I8: $typed(Int8Array, $check.I8), + I16: $typed(Int16Array, $check.I16), + I32: $typed(Int32Array, $check.I32), + I64: $typed(BigInt64Array, $check.I64), + U16: $typed(Uint16Array, $check.U16), + U32: $typed(Uint32Array, $check.U32), + U64: $typed(BigUint64Array, $check.U64), + F32: $typed(Float32Array, $check.F32), + F64: $typed(Float64Array, $check.F64), +}); + const $utf8 = new TextEncoder(); const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); @@ -112,7 +252,8 @@ const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); * Writes the value-buffer wire format: little-endian, packed, no alignment. * Strings and bytes are a u32 length then the bytes, optionals a presence * byte then the value, lists a u32 count then the elements, and maps a u32 - * count then alternating keys and values. + * count then alternating keys and values. Every value is checked first (see + * `$check`): nothing is truncated or wrapped. */ export class $Writer { constructor() { @@ -131,36 +272,25 @@ export class $Writer { this.view = new DataView(grown.buffer); } - num(v) { - if (typeof v !== 'number') throw $expected('a number', v); - return v; - } - - big(v, bits) { - const b = $bigint(v); - if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); - return b; - } - writeBool(v) { - if (typeof v !== 'boolean') throw $expected('a boolean', v); + $check.Bool(v); this.reserve(1); this.buf[this.len++] = v ? 1 : 0; } - writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } - writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } - writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } - writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } - writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } - writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } - writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } - writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } - writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } - writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, $check.I8(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, $check.U8(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, $check.I16(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, $check.U16(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, $check.I32(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, $check.U32(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, $check.I64(v), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, $check.U64(v), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, $check.F32(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, $check.F64(v), true); this.len += 8; } writeString(v) { - if (typeof v !== 'string') throw $expected('a string', v); + $check.String(v); // UTF-8 never needs more than three bytes per UTF-16 code unit. this.reserve(4 + v.length * 3); const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); @@ -169,7 +299,7 @@ export class $Writer { } writeBytes(v) { - if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + $check.Bytes(v); this.reserve(4 + v.length); this.view.setUint32(this.len, v.length, true); this.buf.set(v, this.len + 4); @@ -546,54 +676,13 @@ export function $impl(impl, what) { return impl; } -function $num(v, where) { - if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); - return v; -} - -function $retBigint(v, where) { - if (typeof v === 'bigint') return v; - if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); - throw new TypeError(`${where} must return a bigint`); -} - -/** - * Checks for a callback method's return value, by wire primitive (plus - * `String` and `Bytes`). A value of the wrong type throws, which reports - * the call as a code -4 failure. - */ -export const $ret = Object.freeze({ - Bool(v, where) { - if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); - return v; - }, - I8: $num, - I16: $num, - I32: $num, - U8: $num, - U16: $num, - U32: $num, - F32: $num, - F64: $num, - I64: $retBigint, - U64: $retBigint, - String(v, where) { - if (typeof v !== 'string') throw new TypeError(`${where} must return a string`); - return v; - }, - Bytes(v, where) { - if (!(v instanceof Uint8Array)) throw new TypeError(`${where} must return a Uint8Array`); - return v; - }, -}); - /** * The load-time checks: the native library must implement ABI revision * `abi`, and, for every top-level module, its contract table must hold each * entry these bindings were generated with. `contract` lists - * `[module, [[id, hash, path], ...]]` per module; the native side returns - * a module's table as a `BigUint64Array` of `id, hash` pairs. Entries the - * library has and the bindings don't are fine. + * `[tableFunction, [[id, hash, path], ...]]` per module; the native side + * returns a module's table as a `BigUint64Array` of `id, hash` pairs. + * Entries the library has and the bindings don't are fine. */ export function $verify(raw, library, prefix, abi, contract) { const version = raw[`${prefix}_abi_version`]; @@ -606,10 +695,10 @@ export function $verify(raw, library, prefix, abi, contract) { `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, ); } - for (const [module, expected] of contract) { - const table = raw[`${prefix}_${module}_contract`]; + for (const [symbol, expected] of contract) { + const table = raw[symbol]; if (typeof table !== 'function') { - throw new Error(`${library}: the native library does not export ${prefix}_${module}_contract`); + throw new Error(`${library}: the native library does not export ${symbol}`); } const pairs = table(); const hashes = new Map(); @@ -625,3 +714,19 @@ export function $verify(raw, library, prefix, abi, contract) { } } } + +let $liveFn = null; + +/** + * Install the leak-counter reader of the loaded bindings (`index.js` does, + * once): `kind => bigint`. + */ +export function $setLive(fn) { + $liveFn = fn; +} + +/** Read a leak counter through the reader `index.js` installed. */ +export function $live(kind) { + if ($liveFn === null) throw new Error('the bindings are not loaded'); + return $liveFn(kind); +} diff --git a/crates/weaveffi-cli/src/targets/kotlin/bridge.rs b/crates/weaveffi-cli/src/targets/kotlin/bridge.rs index 7dad5d9f..b9d51024 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/bridge.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/bridge.rs @@ -5,12 +5,13 @@ use crate::codegen::CodeWriter; use weaveffi_model::model::{ - CallShape, CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding, FnBinding, Model, + CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, Model, }; -use weaveffi_model::ty::{Family, Ty}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; -use crate::targets::kotlin::calls::{item_slot, lift, lower}; -use crate::targets::kotlin::names::{jni_kind, kt_member, kt_param, Names}; +use crate::targets::kotlin::calls::{has_name, jni_params, lift, lift_split, lower}; +use crate::targets::kotlin::carrier::Carrier; +use crate::targets::kotlin::names::{kt_member, kt_param, Names, TRAP_DOMAIN, UNTYPED_DOMAIN}; /// The `JniBridge` member name of a callback method's dispatch shim. pub(crate) fn shim_name( @@ -21,6 +22,11 @@ pub(crate) fn shim_name( n.native(&format!("{}_{}", cb.c_tag, m.name)) } +/// The carrier of a callback method's return, or `None` for void. +fn shim_ret(m: &CallbackMethodBinding) -> Option { + Carrier::of_callback_ret(&m.ret_pass, m.ret.as_ref()) +} + /// The JVM descriptor of a callback method's dispatch shim: the /// implementation, the `out_err` address, the parameters, then the return. pub(crate) fn shim_descriptor( @@ -30,88 +36,53 @@ pub(crate) fn shim_descriptor( ) -> String { let mut sig = format!("(L{}/{};J", n.package_path, n.ty(&cb.name)); for p in &m.params { - sig.push_str(&n.jni_descriptor(Some(&p.ty))); + if let Some(c) = Carrier::of_arg(&p.pass) { + sig.push_str(&c.sig()); + } } sig.push(')'); - sig.push_str(&n.jni_descriptor(m.ret.as_ref())); + sig.push_str(&shim_ret(m).map_or_else(|| "V".to_string(), Carrier::sig)); sig } /// The `external fun` declarations backing callable `f`. fn callable_natives(w: &mut CodeWriter, n: &Names, f: &FnBinding) { - let mut params: Vec = Vec::new(); - if f.has_self { - params.push("_self: Long".to_string()); - } - params.extend( - f.params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), n.jni_type(&p.ty))), - ); - match &f.shape { - CallShape::Sync(abi) => { - let ret = f - .ret - .as_ref() - .map(|t| format!(": {}", n.jni_type(t))) - .unwrap_or_default(); - w.line(format!( - "@JvmStatic external fun {}({}){ret}", - n.native(&abi.symbol), - params.join(", ") - )); - } - CallShape::Async(ab) => { - if f.cancellable { + let mut params = jni_params(n, f); + let native = n.native(&f.abi.symbol); + match (&f.shape, &f.ret_pass) { + (CallShape::Async(ab), _) => { + if ab.cancellable() { params.push("_token: Long".to_string()); } params.push("_done: NativeCompletion<*>".to_string()); w.line(format!( - "@JvmStatic external fun {}({})", - n.native(&ab.launch.symbol), + "@JvmStatic external fun {native}({})", params.join(", ") )); } - CallShape::Iterator(it) => { + (CallShape::Sync, RetPass::Iterator(it)) => { w.line(format!( - "@JvmStatic external fun {}({}): Long", - n.native(&it.launch.symbol), + "@JvmStatic external fun {native}({}): Long", params.join(", ") )); - match item_slot(&it.elem) { - None => w.line(format!( - "@JvmStatic external fun {}(_iter: Long): ByteArray?", - n.native(&it.next.symbol) - )), - Some(slot) => w.line(format!( - "@JvmStatic external fun {}(_iter: Long, _out: {}): Boolean", - n.native(&it.next.symbol), - slot.kotlin_array - )), - }; + w.line(format!( + "@JvmStatic external fun {}(_iter: Long, _out: Array): Boolean", + n.native(&it.next.symbol) + )); w.line(format!( "@JvmStatic external fun {}(_iter: Long)", n.native(&it.destroy_symbol) )); } - } -} - -/// The value a dispatch shim returns after reporting a failure (the -/// producer ignores it). -fn failure_value(t: &Ty) -> &'static str { - match t.family() { - Family::Direct => match jni_kind(t) { - "Boolean" => "false", - "Byte" => "0.toByte()", - "Short" => "0.toShort()", - "Int" => "0", - "Float" => "0f", - "Double" => "0.0", - _ => "0L", - }, - Family::Object { .. } => "0L", - _ => "null", + (CallShape::Sync, pass) => { + let ret = Carrier::of_ret(pass, f.ret.as_ref().and_then(|r| r.value())) + .map(|c| format!(": {}", c.kotlin())) + .unwrap_or_default(); + w.line(format!( + "@JvmStatic external fun {native}({}){ret}", + params.join(", ") + )); + } } } @@ -119,73 +90,79 @@ fn failure_value(t: &Ty) -> &'static str { /// object arguments, lifts every other argument from its JNI form, calls /// the implementation, and lowers the result. A failure never unwinds into /// the producer: the shim reports it through `out_err` (`_err`) and returns -/// a zero value. `domain` is the error domain of a method declared -/// `throws`, whose typed errors keep their code and payload. +/// a zero value. A method that throws a domain reports that domain's +/// exceptions with their code and payload. fn callback_shim( w: &mut CodeWriter, n: &Names, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding, - domain: Option<&ErrorBinding>, ) { let mut params = vec![format!("_impl: {}", n.ty(&cb.name)), "_err: Long".into()]; - params.extend( - m.params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), n.jni_type(&p.ty))), - ); // Objects are adopted before anything that can fail, so a failure never // strands a reference the producer handed over. let mut adopted = Vec::new(); - let args: Vec = m - .params - .iter() - .map(|p| { - let name = kt_param(&p.name); - match p.ty.family() { - Family::Object { .. } => { - adopted.push(format!("val _{name} = {}", lift(n, &p.ty, &name))); - format!("_{name}") + let mut args = Vec::new(); + for p in &m.params { + let name = kt_param(&p.name); + match Carrier::of_arg(&p.pass) { + Some(Carrier::Split(k)) => { + let has = has_name(&name); + params.push(format!("{has}: Boolean")); + params.push(format!("{name}: {}", k.name())); + args.push(lift_split(n, &p.ty, k, &has, &name)); + } + Some(c) => { + params.push(format!("{name}: {}", c.kotlin())); + if matches!(p.pass, ArgPass::Object { .. }) { + adopted.push(format!("val _{name} = {}", lift(n, &p.ty, c, &name))); + args.push(format!("_{name}")); + } else { + args.push(lift(n, &p.ty, c, &name)); } - _ => lift(n, &p.ty, &name), } - }) - .collect(); + None => unreachable!("callback method parameters are values"), + } + } let call = format!("_impl.{}({})", kt_member(&m.name), args.join(", ")); - let typed = match domain { - Some(eb) if m.throws => format!("_e as? {}", n.exception(eb)), - _ => "null".to_string(), + let (typed, fallback) = match &m.error { + ErrorStrategy::Domain(d) => (format!("_e as? {}", n.exception(d)), -1), + ErrorStrategy::Untyped => ("null".to_string(), -1), + ErrorStrategy::Trap => ("null".to_string(), -4), + }; + let ret = shim_ret(m); + let ret_sig = match ret { + Some(c @ (Carrier::Prim(_) | Carrier::Handle)) => format!(": {}", c.kotlin()), + Some(c) => format!(": {}?", c.kotlin().trim_end_matches('?')), + None => String::new(), + }; + let failure = match ret { + Some(Carrier::Prim(k)) => Some(k.zero()), + Some(Carrier::Handle) => Some("0L"), + Some(_) => Some("null"), + None => None, }; - let ret = m - .ret - .as_ref() - .map(|t| match t.family() { - Family::String | Family::Bytes | Family::Buffer => ": ByteArray?".to_string(), - _ => format!(": {}", n.jni_type(t)), - }) - .unwrap_or_default(); let name = shim_name(n, cb, m); - let sig = format!("fun {name}({}){ret}", params.join(", ")); - // `head` is the line opening the `try`. + let sig = format!("fun {name}({}){ret_sig}", params.join(", ")); let attempt = |w: &mut CodeWriter, head: String| { w.line(head); w.scope(|w| { - match &m.ret { - Some(t) => w.line(lower(n, t, &call)), - None => w.line(&call), + match (ret, &m.ret) { + (Some(c), Some(t)) => w.line(lower(n, t, c, &call)), + _ => w.line(&call), }; }); w.line("} catch (_e: Throwable) {"); w.scope(|w| { - w.line(format!("fail(_err, _e, {typed})")); - if let Some(t) = &m.ret { - w.line(failure_value(t)); + w.line(format!("fail(_err, _e, {typed}, {fallback})")); + if let Some(v) = failure { + w.line(v); } }); w.line("}"); }; w.line("@JvmStatic"); - if adopted.is_empty() && m.ret.is_some() { + if adopted.is_empty() && ret.is_some() { attempt(w, format!("{sig} = try {{")); } else { w.line(format!("{sig} {{")); @@ -193,7 +170,7 @@ fn callback_shim( for a in &adopted { w.line(a); } - let head = if m.ret.is_some() { + let head = if ret.is_some() { "return try {" } else { "try {" @@ -218,12 +195,17 @@ pub(crate) fn render_bridge(n: &Names, model: &Model) -> String { w.line(" */"); w.block("internal object JniBridge {", "}", |w| { w.block("init {", "}", |w| { - w.line("NativeLibrary.ensureLoaded()"); + w.line("NativeLibrary.load()"); }); w.blank(); w.line("/**"); - w.line(" * The exception for a native failure: through error domain [domain] for a"); - w.line(" * throwing call, or a [NativeBugException] (domain 0) for one that can't fail."); + w.line(" * The exception for a native failure of a call whose errors map through"); + w.line(format!( + " * [domain]: {TRAP_DOMAIN} for a call that can't fail ([NativeBugException]), {UNTYPED_DOMAIN} for" + )); + w.line(" * `throws: any` ([FfiException]), or an error domain, whose unknown codes"); + w.line(" * map to the domain's base exception. Runtime (negative) codes are"); + w.line(" * [FfiException]s."); w.line(" */"); w.line("@JvmStatic"); w.block( @@ -231,14 +213,17 @@ pub(crate) fn render_bridge(n: &Names, model: &Model) -> String { "}", |w| { w.line("val text = decodeUtf8(message)"); - w.block("return when (domain) {", "}", |w| { - for (i, eb) in n.domains(model) { + w.block("return when {", "}", |w| { + w.line(format!( + "domain == {TRAP_DOMAIN} -> NativeBugException(code, text)" + )); + w.line("code < 0 -> FfiException(code, text)"); + for (i, exc) in n.domains() { w.line(format!( - "{i} -> {}.fromCode(code, text, payload)", - n.exception(eb) + "domain == {i} -> {exc}.fromCode(code, text, payload)" )); } - w.line("else -> NativeBugException(code, text)"); + w.line("else -> FfiException(code, text)"); }); }, ); @@ -246,11 +231,11 @@ pub(crate) fn render_bridge(n: &Names, model: &Model) -> String { w.blank(); w.line("/**"); w.line(" * Reports a failed callback through its vtable entry's `out_err` ([err]):"); - w.line(" * [typed], a domain error the method is declared to throw, with its code and"); - w.line(" * payload fields, and anything else as code -4 with its message."); + w.line(" * [typed], an exception of the domain the method throws, with its code and"); + w.line(" * payload fields, and anything else with code [fallback] and its message."); w.line(" */"); w.block( - "private fun fail(err: Long, error: Throwable, typed: FfiException?) {", + "private fun fail(err: Long, error: Throwable, typed: FfiException?, fallback: Int) {", "}", |w| { w.line("if (typed != null) {"); @@ -260,7 +245,7 @@ pub(crate) fn render_bridge(n: &Names, model: &Model) -> String { }); w.line("} else {"); w.scope(|w| { - w.line("error_set(err, -4, encodeUtf8(error.message ?: error.toString()))"); + w.line("error_set(err, fallback, encodeUtf8(error.message ?: error.toString()))"); }); w.line("}"); }, @@ -291,10 +276,10 @@ pub(crate) fn render_bridge(n: &Names, model: &Model) -> String { callable_natives(w, n, f); } } - for (owner, cb) in model.callback_interfaces() { + for (_, cb) in model.callback_interfaces() { for m in &cb.methods { w.blank(); - callback_shim(w, n, cb, m, model.error_domain(owner)); + callback_shim(w, n, cb, m); } } }); diff --git a/crates/weaveffi-cli/src/targets/kotlin/calls.rs b/crates/weaveffi-cli/src/targets/kotlin/calls.rs index d543a06d..ebfa1d78 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/calls.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/calls.rs @@ -1,14 +1,17 @@ //! Public Kotlin wrappers for callables: how each argument lowers to its -//! JNI form, how each result lifts back, and the borrow scopes that keep -//! every object (receiver and arguments) alive and unreleased for the whole -//! native call. +//! JNI [`Carrier`], how each result lifts back, and the borrow scopes that +//! keep every object (receiver and arguments) alive and unreleased for the +//! whole native call. use crate::codegen::CodeWriter; -use weaveffi_model::model::{CallShape, ErrorBinding, FnBinding, ParamBinding}; -use weaveffi_model::ty::{Family, Prim, Ty}; +use weaveffi_model::model::{CallShape, FnBinding}; +use weaveffi_model::plan::{ArgPass, RetPass}; +use weaveffi_model::ty::{ParamTy, Prim, Ty}; +use crate::targets::kotlin::carrier::{Carrier, Kind}; use crate::targets::kotlin::codec::{decode_expr, encode_expr}; -use crate::targets::kotlin::names::{jni_kind, kt_param, unsigned_conversions, Names}; +use crate::targets::kotlin::docs::Speller; +use crate::targets::kotlin::names::{kt_param, unsigned_conversions, Names}; /// One line of a generated body, with its depth relative to the body's /// first line. @@ -30,70 +33,214 @@ fn receiver(expr: &str) -> String { expr.to_string() } +/// The scalar inside an optional (the type itself otherwise). +fn scalar(t: &Ty) -> &Ty { + match t { + Ty::Optional(inner) => inner, + other => other, + } +} + +/// The element type of a list (the type itself otherwise). +fn element(t: &Ty) -> &Ty { + match t { + Ty::List(inner) => inner, + other => other, + } +} + /// The Kotlin expression lowering the public value `expr` of `t` to its JNI -/// form. An object lowers to a new strong reference (a callback method's -/// return, which the producer adopts); call arguments borrow objects -/// instead (see [`emit_callable`]). -pub(crate) fn lower(n: &Names, t: &Ty, expr: &str) -> String { - if let Some((_, to_signed)) = unsigned_conversions(t) { - return format!("{}.{to_signed}()", receiver(expr)); +/// form `c`. An object lowers to a new strong reference (a callback +/// method's return, which the producer adopts); call arguments borrow +/// objects instead (see [`emit_callable`]). +/// +/// # Panics +/// +/// Panics for [`Carrier::Split`], which lowers to two arguments (see +/// [`lower_split`]). +pub(crate) fn lower(n: &Names, t: &Ty, c: Carrier, expr: &str) -> String { + match c { + Carrier::Prim(_) => { + if let Some((_, to_signed)) = unsigned_conversions(t) { + format!("{}.{to_signed}()", receiver(expr)) + } else if matches!(t, Ty::Enum(_)) { + format!("{}.value", receiver(expr)) + } else { + expr.to_string() + } + } + Carrier::Boxed(_) => { + let inner = scalar(t); + if let Some((_, to_signed)) = unsigned_conversions(inner) { + format!("{}?.{to_signed}()", receiver(expr)) + } else if matches!(inner, Ty::Enum(_)) { + format!("{}?.value", receiver(expr)) + } else { + expr.to_string() + } + } + Carrier::Array(k) => match unsigned_conversions(element(t)) { + Some(_) => format!("{}.to{}ArrayBits()", receiver(expr), k.name()), + None => format!("{}.to{}Array()", receiver(expr), k.name()), + }, + Carrier::Bytes => match t { + Ty::Prim(Prim::String) => format!("encodeUtf8({expr})"), + Ty::Prim(Prim::Bytes) => expr.to_string(), + _ => encode_expr(n, t, expr), + }, + Carrier::Handle => match t { + Ty::Optional(_) => format!("{}?.cloneHandle() ?: 0L", receiver(expr)), + _ => format!("{}.cloneHandle()", receiver(expr)), + }, + Carrier::Split(_) => unreachable!("an optional scalar parameter lowers to two arguments"), } - match t.family() { - Family::Direct if matches!(t, Ty::Enum(_)) => format!("{}.value", receiver(expr)), - Family::String => format!("encodeUtf8({expr})"), - Family::Buffer => encode_expr(n, t, expr), - Family::Object { nullable: false } => format!("{}.cloneHandle()", receiver(expr)), - Family::Object { nullable: true } => format!("{}?.cloneHandle() ?: 0L", receiver(expr)), - _ => expr.to_string(), +} + +/// The two JNI arguments of an optional scalar `expr` of `t` (`T?`): its +/// presence, then its value (the kind's zero when absent). +pub(crate) fn lower_split(t: &Ty, k: Kind, expr: &str) -> (String, String) { + let inner = scalar(t); + let value = if let Some((_, to_signed)) = unsigned_conversions(inner) { + format!("{}?.{to_signed}() ?: {}", receiver(expr), k.zero()) + } else if matches!(inner, Ty::Enum(_)) { + format!("{}?.value ?: 0", receiver(expr)) + } else { + format!("{expr} ?: {}", k.zero()) + }; + (format!("{expr} != null"), value) +} + +/// The Kotlin expression lifting the JNI form `expr` (in carrier `c`) of +/// `t` back into its public value. Objects are adopted: the wrapper owes +/// the reference's release. +/// +/// # Panics +/// +/// Panics for [`Carrier::Split`], which lifts from two values (see +/// [`lift_split`]). +pub(crate) fn lift(n: &Names, t: &Ty, c: Carrier, expr: &str) -> String { + match c { + Carrier::Prim(_) => { + if let Some((to_unsigned, _)) = unsigned_conversions(t) { + format!("{}.{to_unsigned}()", receiver(expr)) + } else if let Ty::Enum(name) = t { + format!("{}.fromValue({expr})", n.ty(name)) + } else { + expr.to_string() + } + } + Carrier::Boxed(_) => { + let inner = scalar(t); + if let Some((to_unsigned, _)) = unsigned_conversions(inner) { + format!("{}?.{to_unsigned}()", receiver(expr)) + } else if let Ty::Enum(name) = inner { + format!("{}?.let {{ {}.fromValue(it) }}", receiver(expr), n.ty(name)) + } else { + expr.to_string() + } + } + Carrier::Array(k) => match unsigned_conversions(element(t)) { + Some(_) => format!("{}.toU{}List()", receiver(expr), k.name()), + None => format!("{}.asList()", receiver(expr)), + }, + Carrier::Bytes => match t { + Ty::Prim(Prim::String) => format!("decodeUtf8({expr})"), + Ty::Prim(Prim::Bytes) => expr.to_string(), + _ => decode_expr(n, t, expr), + }, + Carrier::Handle => { + let name = n.ty(scalar(t).interface_name().unwrap_or_default()); + match t { + Ty::Optional(_) => format!("{name}.fromHandleOrNull({expr})"), + _ => format!("{name}.fromHandle({expr})"), + } + } + Carrier::Split(_) => unreachable!("an optional scalar parameter lifts from two values"), } } -/// The Kotlin expression lifting the JNI form `expr` of `t` back into its -/// public value. Objects are adopted: the wrapper owes the reference's -/// release. -pub(crate) fn lift(n: &Names, t: &Ty, expr: &str) -> String { - if let Some((to_unsigned, _)) = unsigned_conversions(t) { - return format!("{}.{to_unsigned}()", receiver(expr)); +/// The Kotlin expression lifting a split optional scalar (`has`, `value`) +/// of `t` (`T?`). +pub(crate) fn lift_split(n: &Names, t: &Ty, k: Kind, has: &str, value: &str) -> String { + format!( + "if ({has}) {} else null", + lift(n, scalar(t), Carrier::Prim(k), value) + ) +} + +/// The Kotlin expression lifting `expr`, an `Any?` holding carrier `c` (an +/// async result or iterator item), into the public value of `t`. +pub(crate) fn lift_erased(n: &Names, t: &Ty, c: Carrier, expr: &str) -> String { + lift(n, t, c, &format!("{expr} as {}", c.kotlin())) +} + +/// The name of the presence flag of the split optional parameter `name` +/// (already spelled for Kotlin), mirroring its C slot `has_{name}`. Kotlin +/// spellings never contain an inner underscore, so it can't collide. +pub(crate) fn has_name(name: &str) -> String { + format!("has_{name}") +} + +/// The `external fun` parameters of callable `f`: the receiver, then every +/// parameter in its JNI form (an optional scalar as its flag and value, a +/// callback interface as the implementing object). +pub(crate) fn jni_params(n: &Names, f: &FnBinding) -> Vec { + let mut out = Vec::new(); + if f.has_self() { + out.push("_self: Long".to_string()); } - match t { - Ty::Enum(name) => format!("{}.fromValue({expr})", n.ty(name)), - Ty::Prim(Prim::String) => format!("decodeUtf8({expr})"), - Ty::Interface(name) => format!("{}.fromHandle({expr})", n.ty(name)), - Ty::Optional(inner) if !t.is_buffered() => { - let name = inner - .interface_name() - .expect("only Interface? is an unbuffered optional value"); - format!("{}.fromHandleOrNull({expr})", n.ty(name)) + for p in &f.params { + let name = kt_param(&p.name); + match Carrier::of_arg(&p.pass) { + Some(Carrier::Split(k)) => { + out.push(format!("{}: Boolean", has_name(&name))); + out.push(format!("{name}: {}", k.name())); + } + Some(c) => out.push(format!("{name}: {}", c.kotlin())), + None => out.push(format!("{name}: {}", n.kt_param_type(&p.ty))), } - _ if t.is_buffered() => decode_expr(n, t, expr), - _ => expr.to_string(), } + out } /// The borrow scopes and JNI arguments for a call to `f`: the receiver -/// (when `has_self`) and every object argument open one `borrow` scope each, -/// a callback interface passes the implementing object (the shim pins it), -/// and every other argument lowers inline. -fn lower_args(n: &Names, f: &FnBinding, has_self: bool) -> (Vec, Vec) { +/// and every object argument open one `borrow` scope each, a callback +/// interface passes the implementing object (the shim pins it), and every +/// other argument lowers inline. +fn lower_args(n: &Names, f: &FnBinding) -> (Vec, Vec) { let mut opens = Vec::new(); let mut args = Vec::new(); - if has_self { + if f.has_self() { opens.push("handle.borrow { _self ->".to_string()); args.push("_self".to_string()); } for (i, p) in f.params.iter().enumerate() { let name = kt_param(&p.name); - match p.ty.family() { - Family::Object { nullable: false } => { + let ParamTy::Value(t) = &p.ty else { + args.push(name); + continue; + }; + match (&p.pass, Carrier::of_arg(&p.pass)) { + ( + ArgPass::Object { + nullable: false, .. + }, + _, + ) => { opens.push(format!("{name}.handle.borrow {{ _h{i} ->")); args.push(format!("_h{i}")); } - Family::Object { nullable: true } => { + (ArgPass::Object { nullable: true, .. }, _) => { opens.push(format!("{name}?.handle.borrowOrNull {{ _h{i} ->")); args.push(format!("_h{i}")); } - Family::Callback { .. } => args.push(name), - _ => args.push(lower(n, &p.ty, &name)), + (_, Some(Carrier::Split(k))) => { + let (has, value) = lower_split(t, k, &name); + args.push(has); + args.push(value); + } + (_, Some(c)) => args.push(lower(n, t, c, &name)), + (_, None) => args.push(name), } } (opens, args) @@ -142,21 +289,6 @@ fn emit_body(w: &mut CodeWriter, sig: &str, returns: bool, lines: Vec) { } } -/// The Kotlin parameter list of a public wrapper. -pub(crate) fn params_sig(n: &Names, params: &[ParamBinding]) -> String { - params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), n.kt_type(&p.ty))) - .collect::>() - .join(", ") -} - -/// The `: Type` return annotation of a public wrapper, or nothing for void. -pub(crate) fn ret_sig(n: &Names, ret: Option<&Ty>) -> String { - ret.map(|t| format!(": {}", n.kt_type(t))) - .unwrap_or_default() -} - /// The `@Deprecated` annotation for a deprecation message. pub(crate) fn deprecated_line(msg: &str) -> String { format!("@Deprecated(\"{}\")", kt_string(msg)) @@ -167,148 +299,111 @@ pub(crate) fn kt_string(s: &str) -> String { s.replace('\\', "\\\\") .replace('"', "\\\"") .replace('$', "\\$") + .replace('\n', "\\n") + .replace('\r', "\\r") } -/// The step function of a `NativeIterator`: pulls one element through the -/// iterator's `_next` native, returning `NativeIterator.DONE` at the end. -fn iterator_step(n: &Names, f: &FnBinding) -> Vec { - let CallShape::Iterator(it) = &f.shape else { - unreachable!("iterator_step needs an iterator call shape"); - }; - let next = n.native(&it.next.symbol); - match item_slot(&it.elem) { - None => vec![ - (0, format!("val _item = JniBridge.{next}(_it)")), - ( - 0, - format!( - "if (_item == null) NativeIterator.DONE else {}", - lift(n, &it.elem, "_item") - ), - ), - ], - Some(slot) => vec![ - (0, format!("val _slot = {}(1)", slot.kotlin_array)), - ( - 0, - format!( - "if (JniBridge.{next}(_it, _slot)) {} else NativeIterator.DONE", - lift(n, &it.elem, "_slot[0]") - ), - ), - ], - } -} - -/// How an iterator's `_next` native hands back a direct or object element: -/// through a one-element primitive array of its JNI carrier (strings, -/// bytes, and buffers come back as a nullable `ByteArray` instead). -pub(crate) struct ItemSlot { - /// The Kotlin array type (`IntArray`). - pub kotlin_array: String, - /// The JNI array type (`jintArray`). - pub jni_array: String, - /// The JNI element type (`jint`). - pub jni_elem: String, - /// The JNI array-region setter stem (`Int` in `SetIntArrayRegion`). - pub region: &'static str, -} - -/// The slot an iterator element of `t` comes back through, or `None` for -/// the `ByteArray` families. -pub(crate) fn item_slot(t: &Ty) -> Option { - match t.family() { - Family::Direct | Family::Object { .. } => { - let kind = jni_kind(t); - Some(ItemSlot { - kotlin_array: format!("{kind}Array"), - jni_array: format!("j{}Array", kind.to_lowercase()), - jni_elem: format!("j{}", kind.to_lowercase()), - region: kind, - }) - } - _ => None, - } +/// How a public wrapper is declared. +pub(crate) struct Decl<'a> { + /// The Kotlin name (`count`, `invoke`). + pub name: &'a str, + /// Whether it's the companion's `operator fun invoke` (a `new` + /// constructor). + pub operator: bool, + /// Whether to annotate it `@JvmStatic` (module object and companion + /// functions, so Java sees static methods). + pub jvm_static: bool, } -/// Emit the public wrapper for callable `f`: `decl` is everything before the -/// parameter list (`fun count`, `operator fun invoke`; `suspend` is added -/// for async callables), `has_self` borrows the receiver, and `error` is the -/// domain a throwing callable maps failures through. +/// Emit the public wrapper for callable `f`, with its KDoc, annotations, +/// and body. pub(crate) fn emit_callable( w: &mut CodeWriter, n: &Names, + sp: &Speller, f: &FnBinding, - decl: &str, - has_self: bool, - error: Option<&ErrorBinding>, + decl: Decl, ) { - if let Some(msg) = &f.deprecated { - w.line(deprecated_line(msg)); + sp.fn_doc( + w, + sp.text(&f.doc), + f.params.iter().map(|p| (p.name.as_str(), &p.doc)), + ); + if let Some(msg) = sp.deprecation(&f.deprecated) { + w.line(deprecated_line(&msg)); + } + if let Some(exc) = n.thrown(&f.error) { + w.line(format!("@Throws({exc}::class)")); + } + if decl.jvm_static { + w.line("@JvmStatic"); } - let params = params_sig(n, &f.params); - let ret = ret_sig(n, f.ret.as_ref()); - let (opens, mut args) = lower_args(n, f, has_self); - match &f.shape { - CallShape::Sync(abi) => { - let call = format!("JniBridge.{}({})", n.native(&abi.symbol), args.join(", ")); - let body = match &f.ret { - Some(t) => lift(n, t, &call), - None => call, + let params: Vec = f + .params + .iter() + .map(|p| format!("{}: {}", kt_param(&p.name), n.kt_param_type(&p.ty))) + .collect(); + let params = params.join(", "); + let ret = f + .ret + .as_ref() + .map(|t| format!(": {}", n.kt_ret_type(t))) + .unwrap_or_default(); + let head = format!( + "{}fun {}", + if decl.operator { "operator " } else { "" }, + decl.name + ); + let (opens, mut args) = lower_args(n, f); + let native = n.native(&f.abi.symbol); + match (&f.shape, &f.ret_pass) { + (CallShape::Sync, RetPass::Iterator(it)) => { + let launch = format!("JniBridge.{native}({})", args.join(", ")); + let c = Carrier::of_item(&it.item, &it.elem); + let body = vec![( + 0, + format!( + "NativeIterator({launch}, JniBridge::{}, JniBridge::{}) {{ {} }}", + n.native(&it.destroy_symbol), + n.native(&it.next.symbol), + lift_erased(n, &it.elem, c, "it") + ), + )]; + let sig = format!("{head}({params}){ret}"); + emit_body(w, &sig, true, nest(&opens, body)); + } + (CallShape::Sync, pass) => { + let call = format!("JniBridge.{native}({})", args.join(", ")); + let value = f.ret.as_ref().and_then(|r| r.value()); + let body = match (Carrier::of_ret(pass, value), value) { + (Some(c), Some(t)) => lift(n, t, c, &call), + _ => call, }; - let sig = format!("{decl}({params}){ret}"); + let sig = format!("{head}({params}){ret}"); if opens.is_empty() && f.ret.is_some() { w.line(format!("{sig} = {body}")); } else { emit_body(w, &sig, f.ret.is_some(), nest(&opens, vec![(0, body)])); } } - CallShape::Iterator(it) => { - let launch = format!( - "JniBridge.{}({})", - n.native(&it.launch.symbol), - args.join(", ") - ); - let mut body = vec![( - 0, - format!( - "NativeIterator({launch}, JniBridge::{}) {{ _it ->", - n.native(&it.destroy_symbol) - ), - )]; - body.extend(iterator_step(n, f).into_iter().map(|(d, l)| (d + 1, l))); - body.push((0, "}".to_string())); - emit_body( - w, - &format!("{decl}({params}){ret}"), - true, - nest(&opens, body), - ); - } - CallShape::Async(ab) => { - if f.cancellable { + (CallShape::Async(ab), _) => { + if ab.cancellable() { args.push("_token".to_string()); } args.push("_done".to_string()); - let call = format!( - "JniBridge.{}({})", - n.native(&ab.launch.symbol), - args.join(", ") - ); - let convert = match &f.ret { - None => "{ }".to_string(), - Some(t) => format!( - "{{ _raw -> {} }}", - lift(n, t, &format!("_raw as {}", n.jni_type(t))) - ), + let call = format!("JniBridge.{native}({})", args.join(", ")); + let value = f.ret.as_ref().and_then(|r| r.value()); + let convert = match (Carrier::of_result(&ab.result, value), value) { + (Some(c), Some(t)) => format!("{{ {} }}", lift_erased(n, t, c, "it")), + _ => "{ }".to_string(), }; - let token = if f.cancellable { "_token" } else { "_" }; + let token = if ab.cancellable() { "_token" } else { "_" }; let mut body = vec![( 0, format!( "awaitNative({}, {}, {convert}) {{ {token}, _done ->", - f.cancellable, - n.domain(f, error) + ab.cancellable(), + n.domain_index(&f.error) ), )]; body.extend( @@ -319,7 +414,7 @@ pub(crate) fn emit_callable( body.push((0, "}".to_string())); emit_body( w, - &format!("suspend {decl}({params}){ret}"), + &format!("suspend {head}({params}){ret}"), f.ret.is_some(), body, ); diff --git a/crates/weaveffi-cli/src/targets/kotlin/carrier.rs b/crates/weaveffi-cli/src/targets/kotlin/carrier.rs new file mode 100644 index 00000000..ca1a6ecc --- /dev/null +++ b/crates/weaveffi-cli/src/targets/kotlin/carrier.rs @@ -0,0 +1,305 @@ +//! How each value crosses the JNI boundary, read from the model's passing +//! contracts ([`ArgPass`], [`RetPass`], [`ResultPass`], [`ItemPass`], +//! [`CallbackRetPass`]), never re-derived from the type. +//! +//! A [`Carrier`] names the JNI form: a primitive (`jint`), its nullable box +//! (`java.lang.Integer`, for an optional scalar crossing out of native code), +//! a primitive array (`jintArray`, for a typed-array slice), a `ByteArray` +//! (strings, bytes, and value buffers), or an object's address (`jlong`). An +//! optional scalar parameter crosses split, as a presence flag and a value, +//! exactly like its C slots. Async results and iterator items reach Kotlin +//! as `Any?` and are cast to their carrier's object type first. + +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{Prim, Ty}; + +/// A JNI primitive kind: the carrier of a direct value, the element of a +/// slice, and the payload of a box. Unsigned integers ride in the signed +/// kind of the same width and C-style enums in `Int`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Kind { + Boolean, + Byte, + Short, + Int, + Long, + Float, + Double, +} + +impl Kind { + /// The kind a scalar primitive crosses as. + pub(crate) fn of_prim(p: Prim) -> Self { + match p { + Prim::Bool => Self::Boolean, + Prim::I8 | Prim::U8 => Self::Byte, + Prim::I16 | Prim::U16 => Self::Short, + Prim::I32 | Prim::U32 => Self::Int, + Prim::F32 => Self::Float, + Prim::F64 => Self::Double, + Prim::I64 | Prim::U64 | Prim::String | Prim::Bytes => Self::Long, + } + } + + /// The kind a direct value (a scalar primitive or a C-style enum) + /// crosses as. + pub(crate) fn of(t: &Ty) -> Self { + match t { + Ty::Prim(p) => Self::of_prim(*p), + _ => Self::Int, + } + } + + /// The Kotlin type, which is also the `Call{Kind}Method` and + /// `New{Kind}Array` stem: `Int`. + pub(crate) fn name(self) -> &'static str { + match self { + Self::Boolean => "Boolean", + Self::Byte => "Byte", + Self::Short => "Short", + Self::Int => "Int", + Self::Long => "Long", + Self::Float => "Float", + Self::Double => "Double", + } + } + + /// The JNI C type: `jint`. + pub(crate) fn jni(self) -> &'static str { + match self { + Self::Boolean => "jboolean", + Self::Byte => "jbyte", + Self::Short => "jshort", + Self::Int => "jint", + Self::Long => "jlong", + Self::Float => "jfloat", + Self::Double => "jdouble", + } + } + + /// The JVM type descriptor: `I`. + pub(crate) fn sig(self) -> &'static str { + match self { + Self::Boolean => "Z", + Self::Byte => "B", + Self::Short => "S", + Self::Int => "I", + Self::Long => "J", + Self::Float => "F", + Self::Double => "D", + } + } + + /// The shim's `Jni_kind` constant: `Jni_I`. + pub(crate) fn c_const(self) -> String { + format!("Jni_{}", self.sig()) + } + + /// The `jvalue` member holding this kind: `i`. + pub(crate) fn jvalue(self) -> &'static str { + match self { + Self::Boolean => "z", + Self::Byte => "b", + Self::Short => "s", + Self::Int => "i", + Self::Long => "j", + Self::Float => "f", + Self::Double => "d", + } + } + + /// The box class's descriptor: `Ljava/lang/Integer;`. + pub(crate) fn box_sig(self) -> &'static str { + match self { + Self::Boolean => "Ljava/lang/Boolean;", + Self::Byte => "Ljava/lang/Byte;", + Self::Short => "Ljava/lang/Short;", + Self::Int => "Ljava/lang/Integer;", + Self::Long => "Ljava/lang/Long;", + Self::Float => "Ljava/lang/Float;", + Self::Double => "Ljava/lang/Double;", + } + } + + /// The Kotlin zero value of this kind, for an absent optional's value + /// slot and a failed callback's return. + pub(crate) fn zero(self) -> &'static str { + match self { + Self::Boolean => "false", + Self::Byte => "0.toByte()", + Self::Short => "0.toShort()", + Self::Int => "0", + Self::Long => "0L", + Self::Float => "0f", + Self::Double => "0.0", + } + } +} + +/// The JNI form of one value in one position. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Carrier { + /// A primitive: a direct value. + Prim(Kind), + /// An optional scalar parameter: a presence flag, then the value (0 + /// when absent), mirroring the C slots. + Split(Kind), + /// A nullable box: an optional scalar crossing out of native code. + Boxed(Kind), + /// A primitive array: a typed-array slice. + Array(Kind), + /// A `ByteArray`: a string's UTF-8, bytes, or a value buffer. + Bytes, + /// An object's address (`0L` is none). + Handle, +} + +impl Carrier { + /// A parameter (or callback method parameter); `None` for a callback + /// interface, which crosses as the implementing object. + pub(crate) fn of_arg(pass: &ArgPass) -> Option { + Some(match pass { + ArgPass::Direct { slot } => Self::Prim(kind_of_slot(&slot.ty)), + ArgPass::OptDirect { inner, .. } => Self::Split(Kind::of(inner)), + ArgPass::Slice { elem, .. } => Self::Array(Kind::of_prim(*elem)), + ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => Self::Bytes, + ArgPass::Object { .. } => Self::Handle, + ArgPass::Callback { .. } => return None, + }) + } + + /// A synchronous return (an iterator launcher's is its handle); `None` + /// for void. `direct` is the value type of a direct return. + pub(crate) fn of_ret(pass: &RetPass, direct: Option<&Ty>) -> Option { + Some(match pass { + RetPass::Void => return None, + RetPass::Direct => Self::Prim(Kind::of(direct?)), + RetPass::OptDirect { .. } => Self::Boxed(Kind::of(&opt_inner(direct?))), + RetPass::Slice { elem, .. } => Self::Array(Kind::of_prim(*elem)), + RetPass::String { .. } | RetPass::Bytes { .. } | RetPass::Buffer { .. } => Self::Bytes, + RetPass::Object { .. } | RetPass::Iterator(_) => Self::Handle, + }) + } + + /// An async result, delivered boxed as `Any?`; `None` for void. `ty` is + /// the result's value type. + pub(crate) fn of_result(pass: &ResultPass, ty: Option<&Ty>) -> Option { + Some(match pass { + ResultPass::Void => return None, + ResultPass::Direct { .. } => Self::Prim(Kind::of(ty?)), + ResultPass::OptDirect { .. } => Self::Boxed(Kind::of(&opt_inner(ty?))), + ResultPass::Slice { elem, .. } => Self::Array(Kind::of_prim(*elem)), + ResultPass::String { .. } | ResultPass::Bytes { .. } | ResultPass::Buffer { .. } => { + Self::Bytes + } + ResultPass::Object { .. } => Self::Handle, + }) + } + + /// An iterator item, delivered boxed as `Any?`. `elem` is the element + /// type. + pub(crate) fn of_item(pass: &ItemPass, elem: &Ty) -> Self { + match pass { + ItemPass::Direct { .. } => Self::Prim(Kind::of(elem)), + ItemPass::OptDirect { .. } => Self::Boxed(Kind::of(&opt_inner(elem))), + ItemPass::Slice { elem, .. } => Self::Array(Kind::of_prim(*elem)), + ItemPass::String { .. } | ItemPass::Bytes { .. } | ItemPass::Buffer { .. } => { + Self::Bytes + } + ItemPass::Object { .. } => Self::Handle, + } + } + + /// A callback method's return; `None` for void. `ty` is the return's + /// value type. + pub(crate) fn of_callback_ret(pass: &CallbackRetPass, ty: Option<&Ty>) -> Option { + Some(match pass { + CallbackRetPass::Void => return None, + CallbackRetPass::Direct => Self::Prim(Kind::of(ty?)), + CallbackRetPass::OptDirect { .. } => Self::Boxed(Kind::of(&opt_inner(ty?))), + CallbackRetPass::Slice { elem, .. } => Self::Array(Kind::of_prim(*elem)), + CallbackRetPass::String { .. } + | CallbackRetPass::Bytes { .. } + | CallbackRetPass::Buffer { .. } => Self::Bytes, + CallbackRetPass::Object { .. } => Self::Handle, + }) + } + + /// The Kotlin type of an `external fun` parameter or return in this + /// form (a split optional is two parameters; this is its value's). + pub(crate) fn kotlin(self) -> String { + match self { + Self::Prim(k) | Self::Split(k) => k.name().to_string(), + Self::Boxed(k) => format!("{}?", k.name()), + Self::Array(k) => format!("{}Array", k.name()), + Self::Bytes => "ByteArray".to_string(), + Self::Handle => "Long".to_string(), + } + } + + /// The JNI C type (a split optional's value). + pub(crate) fn jni(self) -> String { + match self { + Self::Prim(k) | Self::Split(k) => k.jni().to_string(), + Self::Boxed(_) => "jobject".to_string(), + Self::Array(k) => format!("{}Array", k.jni()), + Self::Bytes => "jbyteArray".to_string(), + Self::Handle => "jlong".to_string(), + } + } + + /// The JVM descriptor (a split optional is its flag, then its value). + pub(crate) fn sig(self) -> String { + match self { + Self::Prim(k) => k.sig().to_string(), + Self::Split(k) => format!("Z{}", k.sig()), + Self::Boxed(k) => k.box_sig().to_string(), + Self::Array(k) => format!("[{}", k.sig()), + Self::Bytes => "[B".to_string(), + Self::Handle => "J".to_string(), + } + } + + /// The JNI method-call stem a value of this form comes back through + /// (`CallStatic{stem}Method`). + pub(crate) fn call_stem(self) -> &'static str { + match self { + Self::Prim(k) | Self::Split(k) => k.name(), + Self::Handle => "Long", + Self::Boxed(_) | Self::Array(_) | Self::Bytes => "Object", + } + } + + /// The array kind of a run-shaped carrier: the slice's element kind, or + /// `Byte` for a `ByteArray`. + pub(crate) fn run_kind(self) -> Option { + match self { + Self::Array(k) => Some(k), + Self::Bytes => Some(Kind::Byte), + _ => None, + } + } +} + +/// The scalar inside an optional (the type itself otherwise). +fn opt_inner(t: &Ty) -> Ty { + match t { + Ty::Optional(inner) => (**inner).clone(), + other => other.clone(), + } +} + +/// The kind of a direct parameter from its C slot type (an enum slot is an +/// `int32_t` typedef). +fn kind_of_slot(t: &weaveffi_model::abi::CType) -> Kind { + use weaveffi_model::abi::CType; + match t { + CType::Bool => Kind::Boolean, + CType::Int8 | CType::Uint8 => Kind::Byte, + CType::Int16 | CType::Uint16 => Kind::Short, + CType::Int64 | CType::Uint64 => Kind::Long, + CType::Float => Kind::Float, + CType::Double => Kind::Double, + _ => Kind::Int, + } +} diff --git a/crates/weaveffi-cli/src/targets/kotlin/codec.rs b/crates/weaveffi-cli/src/targets/kotlin/codec.rs index f2680212..fbc54d97 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/codec.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/codec.rs @@ -1,34 +1,23 @@ //! Value-buffer codec: the Kotlin statement writing and the expression //! reading every wire shape, dispatched on [`Ty::wire`], and `Codecs.kt`, -//! which holds one `pack`/`unpack` pair per distinct composite type -//! (`[Entry]`, `{string:i64}`, `Shape?`) so no call site inlines a loop. -//! The runtime's `BufferWriter`/`BufferReader` name one method per [`Prim`] -//! (`writeI32`, `readString`), so primitives need a single arm. +//! which holds one `pack_{stem}`/`unpack_{stem}` pair per composite type +//! (`[Entry]` is `pack_list_Entry`, `{string:[i64]}` is +//! `pack_map_string_list_i64`), named with the shared +//! [`codecs::stem`], so no call site inlines +//! a loop. Records and rich enums get the same pair, named after their stem +//! (the type's name). The runtime's `BufferWriter`/`BufferReader` name one +//! method per [`Prim`] (`writeI32`, `readString`), so primitives need a +//! single arm. //! //! [`Prim`]: weaveffi_model::ty::Prim -use std::collections::BTreeMap; - +use crate::codegen::codecs; use crate::codegen::CodeWriter; -use weaveffi_model::model::{FieldBinding, Model, ParamBinding}; +use weaveffi_model::model::Model; use weaveffi_model::ty::{Ty, WireType}; use crate::targets::kotlin::names::Names; -/// The name of a composite's codec functions after `pack`/`unpack`, spelled -/// in prefix order so it's unambiguous: `[Entry]` is `ListOfEntry`, -/// `{string:[i32]}` is `MapOfStringToListOfI32`, `Shape?` is -/// `OptionalOfShape`. -fn codec_name(n: &Names, t: &Ty) -> String { - match t { - Ty::Prim(p) => p.pascal().to_string(), - Ty::Optional(inner) => format!("OptionalOf{}", codec_name(n, inner)), - Ty::List(inner) => format!("ListOf{}", codec_name(n, inner)), - Ty::Map(k, v) => format!("MapOf{}To{}", codec_name(n, k), codec_name(n, v)), - other => n.ty(other.user_name().expect("only user types remain")), - } -} - /// The Kotlin statement writing `expr` (the public Kotlin value of `t`) into /// the writer `w`. /// @@ -44,9 +33,8 @@ pub(crate) fn write_expr(n: &Names, t: &Ty, w: &str, expr: &str) -> String { "{w}.writeObject({expr}.cloneHandle(), JniBridge::{})", n.native(n.destroy_symbol(name)) ), - WireType::User(name) => format!("pack{}({w}, {expr})", n.ty(name)), - WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("pack{}({w}, {expr})", codec_name(n, t)) + WireType::User(_) | WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { + format!("pack_{}({w}, {expr})", codecs::stem(t)) } } } @@ -59,9 +47,8 @@ pub(crate) fn read_expr(n: &Names, t: &Ty, r: &str) -> String { WireType::Prim(p) => format!("{r}.read{}()", p.pascal()), WireType::Enum(name) => format!("{}.fromValue({r}.readI32())", n.ty(name)), WireType::Object(name) => format!("{}.fromHandle({r}.readObject())", n.ty(name)), - WireType::User(name) => format!("unpack{}({r})", n.ty(name)), - WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("unpack{}({r})", codec_name(n, t)) + WireType::User(_) | WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { + format!("unpack_{}({r})", codecs::stem(t)) } } } @@ -78,83 +65,16 @@ pub(crate) fn decode_expr(n: &Names, t: &Ty, expr: &str) -> String { format!("decodeBuffer({expr}) {{ _r -> {} }}", read_expr(n, t, "_r")) } -/// Every distinct composite type (optional, list, or map) that crosses -/// inside a value buffer, by codec name. -fn composites(n: &Names, model: &Model) -> BTreeMap { - /// A type in a top-level position: a parameter, return, or iterator - /// element. `Interface?` and `Cb?` aren't buffers there. - fn top(n: &Names, t: &Ty, out: &mut BTreeMap) { - match t { - Ty::Iterator(elem) => top(n, elem, out), - _ if t.is_buffered() => nested(n, t, out), - _ => {} - } - } - /// A type inside a value buffer, where every optional is encoded. - fn nested(n: &Names, t: &Ty, out: &mut BTreeMap) { - match t { - Ty::Optional(inner) | Ty::List(inner) => { - out.insert(codec_name(n, t), t.clone()); - nested(n, inner, out); - } - Ty::Map(k, v) => { - out.insert(codec_name(n, t), t.clone()); - nested(n, k, out); - nested(n, v, out); - } - _ => {} - } - } - let mut out = BTreeMap::new(); - let fields = |fs: &[FieldBinding], out: &mut BTreeMap| { - for f in fs { - nested(n, &f.ty, out); - } - }; - let signature = |ps: &[ParamBinding], ret: &Option, out: &mut BTreeMap| { - for p in ps { - top(n, &p.ty, out); - } - if let Some(t) = ret { - top(n, t, out); - } - }; - for m in &model.modules { - for s in &m.structs { - fields(&s.fields, &mut out); - } - for e in &m.enums { - for v in &e.variants { - fields(&v.fields, &mut out); - } - } - if let Some(eb) = &m.errors { - for c in &eb.codes { - fields(&c.fields, &mut out); - } - } - for f in m.callables() { - signature(&f.params, &f.ret, &mut out); - } - for cb in &m.callback_interfaces { - for f in &cb.methods { - signature(&f.params, &f.ret, &mut out); - } - } - } - out -} - /// The body of `Codecs.kt` (after the package line), or `None` when no /// composite type crosses in a value buffer. pub(crate) fn render_codecs(n: &Names, model: &Model) -> Option { - let all = composites(n, model); + let all = codecs::composites(model); if all.is_empty() { return None; } let mut w = CodeWriter::four_space(); - for t in all.values() { - let name = codec_name(n, t); + for t in &all { + let stem = codecs::stem(t); let kt = n.kt_type(t); let (write, read) = match t { Ty::Optional(inner) => ( @@ -183,15 +103,15 @@ pub(crate) fn render_codecs(n: &Names, model: &Model) -> Option { read_expr(n, v, "_r") ), ), - _ => unreachable!("only composites are collected"), + _ => unreachable!("composites are optionals, lists, and maps"), }; w.blank(); w.line(format!( - "internal fun pack{name}(_w: BufferWriter, _v: {kt}) = {write}" + "internal fun pack_{stem}(_w: BufferWriter, _v: {kt}) = {write}" )); w.blank(); w.line(format!( - "internal fun unpack{name}(_r: BufferReader): {kt} = {read}" + "internal fun unpack_{stem}(_r: BufferReader): {kt} = {read}" )); } Some(w.finish()) diff --git a/crates/weaveffi-cli/src/targets/kotlin/docs.rs b/crates/weaveffi-cli/src/targets/kotlin/docs.rs index b9a1462c..fc8aa2d0 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/docs.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/docs.rs @@ -1,49 +1,89 @@ -//! KDoc comment emission. +//! KDoc comments and deprecation messages, with backticked API identifiers +//! in IDL docs rewritten to their Kotlin spelling (`` `new_op` `` becomes +//! `` `newOp` ``, `` `KvError` `` becomes `` `KvException` ``). use crate::codegen::common::DocCommentStyle; +use crate::codegen::docs::{ApiNames, Doc, IdentKind}; use crate::codegen::CodeWriter; -use weaveffi_model::model::ParamBinding; -use crate::targets::kotlin::names::kt_param; +use crate::targets::kotlin::names::{kt_member, kt_param, Names}; -/// Emit one line of a KDoc block: ` * text`, or ` *` for an empty line. -fn kdoc_line(w: &mut CodeWriter, prefix: &str, text: &str) { - if text.is_empty() { - w.line(" *"); - } else { - w.line(format!(" *{prefix}{text}")); - } +/// Spells IDL identifiers the way the generated Kotlin declares them. +pub(crate) struct Speller<'a> { + api: ApiNames, + names: &'a Names, } -/// Emit the KDoc block of a function: its doc plus one `@param` tag per -/// documented parameter, named with its Kotlin spelling. Without parameter -/// docs this is the plain doc comment. -pub(crate) fn fn_doc(w: &mut CodeWriter, doc: &Option, params: &[ParamBinding]) { - let documented: Vec<(String, &str)> = params - .iter() - .filter_map(|p| { - let d = p.doc.as_deref()?.trim(); - (!d.is_empty()).then(|| (kt_param(&p.name), d)) - }) - .collect(); - if documented.is_empty() { - w.doc(doc, DocCommentStyle::Javadoc); - return; +impl<'a> Speller<'a> { + /// A speller over every identifier `model` declares. + pub(crate) fn new(model: &weaveffi_model::model::Model, names: &'a Names) -> Self { + Self { + api: ApiNames::new(model), + names, + } } - w.line("/**"); - if let Some(d) = doc.as_deref().map(str::trim).filter(|d| !d.is_empty()) { - for line in d.lines() { - kdoc_line(w, " ", line); + + /// The Kotlin spelling of `ident`, or `None` to keep it as written (an + /// identifier naming several kinds of declaration, a module, a variant, + /// or an error code). + pub(crate) fn spell(&self, ident: &str) -> Option { + match self.api.kind(ident)? { + IdentKind::Function | IdentKind::Member | IdentKind::CallbackMethod => { + Some(kt_member(ident)) + } + IdentKind::Param | IdentKind::Field => Some(kt_param(ident)), + IdentKind::Type => Some(self.names.ty(ident)), + IdentKind::ErrorDomain => Some(self.names.exception(ident).to_string()), + IdentKind::Module | IdentKind::Variant | IdentKind::ErrorCode => None, } } - for (name, d) in documented { - let mut lines = d.lines(); - if let Some(first) = lines.next() { - w.line(format!(" * @param {name} {first}")); + + /// A declaration's doc text, rewritten. + pub(crate) fn text(&self, doc: &Option) -> Option { + Doc::new(doc, &None).text(|i| self.spell(i)) + } + + /// A declaration's deprecation message, rewritten. + pub(crate) fn deprecation(&self, deprecated: &Option) -> Option { + Doc::new(&None, deprecated).deprecation(|i| self.spell(i)) + } + + /// Emit a declaration's KDoc block (rewritten). + pub(crate) fn doc(&self, w: &mut CodeWriter, doc: &Option) { + w.doc(&self.text(doc), DocCommentStyle::Javadoc); + } + + /// Emit the KDoc block of a callable: its doc plus one `@param` tag per + /// documented parameter, named with its Kotlin spelling. Without + /// parameter docs this is the plain doc comment. + pub(crate) fn fn_doc<'p>( + &self, + w: &mut CodeWriter, + doc: Option, + params: impl IntoIterator)>, + ) { + let documented: Vec<(String, String)> = params + .into_iter() + .filter_map(|(name, d)| Some((kt_param(name), self.text(d)?))) + .collect(); + if documented.is_empty() { + w.doc(&doc, DocCommentStyle::Javadoc); + return; + } + let mut text = doc.unwrap_or_default(); + if !text.is_empty() { + text.push_str("\n\n"); } - for line in lines { - kdoc_line(w, " ", line); + for (i, (name, d)) in documented.iter().enumerate() { + if i > 0 { + text.push('\n'); + } + let mut lines = d.lines(); + text.push_str(&format!("@param {name} {}", lines.next().unwrap_or(""))); + for line in lines { + text.push_str(&format!("\n {line}")); + } } + w.doc(&Some(text), DocCommentStyle::Javadoc); } - w.line(" */"); } diff --git a/crates/weaveffi-cli/src/targets/kotlin/entities.rs b/crates/weaveffi-cli/src/targets/kotlin/entities.rs index 8cf67047..954da430 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/entities.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/entities.rs @@ -1,69 +1,194 @@ -//! Kotlin declarations for one top-level module: enums, records, rich -//! enums, callback interfaces, interface wrapper classes, error domains, -//! and the module object holding the free functions (nested modules become +//! Kotlin declarations for one top-level module: error domains, enums, +//! records, rich enums, callback interfaces, interface wrapper classes, and +//! the module object holding the free functions (nested modules become //! nested objects). -use crate::codegen::common::{pascal_case, DocCommentStyle}; +use crate::codegen::common::pascal_case; use crate::codegen::CodeWriter; use weaveffi_model::errors; use weaveffi_model::model::{ CallbackInterfaceBinding, EnumBinding, ErrorBinding, FieldBinding, InterfaceBinding, Model, ModuleBinding, StructBinding, }; +use weaveffi_model::plan::ErrorStrategy; +use weaveffi_model::ty::{Prim, Ty}; -use crate::targets::kotlin::calls::{ - deprecated_line, emit_callable, kt_string, params_sig, ret_sig, -}; +use crate::targets::kotlin::calls::{deprecated_line, emit_callable, kt_string, Decl}; use crate::targets::kotlin::codec::{read_expr, write_expr}; -use crate::targets::kotlin::docs::fn_doc; -use crate::targets::kotlin::names::{kt_escape, kt_member, Names}; +use crate::targets::kotlin::docs::Speller; +use crate::targets::kotlin::names::{kt_escape, kt_fields, kt_member, kt_param, Names}; /// Emit `@Deprecated` for a deprecated declaration. -fn deprecated(w: &mut CodeWriter, msg: &Option) { - if let Some(msg) = msg { - w.line(deprecated_line(msg)); +fn deprecated(w: &mut CodeWriter, sp: &Speller, msg: &Option) { + if let Some(msg) = sp.deprecation(msg) { + w.line(deprecated_line(&msg)); } } -/// The `val name: Type` constructor properties of a record or variant. -fn field_decls(n: &Names, fields: &[FieldBinding]) -> Vec { - fields - .iter() - .map(|f| format!("val {}: {}", kt_escape(&f.name), n.kt_type(&f.ty))) +/// One constructor property of a record, variant, or error code: its Kotlin +/// name and type, and the IR field. +struct Prop<'a> { + name: String, + ty: String, + field: &'a FieldBinding, +} + +/// The constructor properties of `fields`, named for Kotlin (unique, and +/// away from `Throwable`'s members for an error code). +fn props<'a>(n: &Names, fields: &'a [FieldBinding], error_payload: bool) -> Vec> { + kt_fields(fields.iter().map(|f| f.name.as_str()), error_payload) + .into_iter() + .zip(fields) + .map(|(name, field)| Prop { + name, + ty: n.kt_type(&field.ty), + field, + }) .collect() } /// Emit a class header with constructor properties: on one line when no /// field is documented, one property per line (each with its KDoc) /// otherwise. `head` is everything before `(`, `tail` everything after `)`. -fn emit_properties(w: &mut CodeWriter, n: &Names, head: &str, fields: &[FieldBinding], tail: &str) { - if fields.iter().any(|f| f.doc.is_some()) { +fn emit_properties(w: &mut CodeWriter, sp: &Speller, head: &str, props: &[Prop], tail: &str) { + if props.iter().any(|p| p.field.doc.is_some()) { w.line(format!("{head}(")); w.scope(|w| { - for (f, decl) in fields.iter().zip(field_decls(n, fields)) { - w.doc(&f.doc, DocCommentStyle::Javadoc); - w.line(format!("{decl},")); + for p in props { + sp.doc(w, &p.field.doc); + w.line(format!("val {}: {},", p.name, p.ty)); } }); w.line(format!("){tail}")); } else { + let decls: Vec = props + .iter() + .map(|p| format!("val {}: {}", p.name, p.ty)) + .collect(); + w.line(format!("{head}({}){tail}", decls.join(", "))); + } +} + +/// How a field compares: by `==` unless its type holds a `ByteArray`, +/// whose `equals` is identity. +enum Compare { + /// No array inside: the data class default. + Value, + /// A `bytes` or `bytes?` field: the stdlib `content*` functions. + Array, + /// Arrays nested in a list, map, or optional: the runtime's `deep*` + /// helpers. + Deep, +} + +fn compare(t: &Ty) -> Compare { + let bytes = |t: &Ty| matches!(t, Ty::Prim(Prim::Bytes)); + match t { + _ if bytes(t) => Compare::Array, + Ty::Optional(inner) if bytes(inner) => Compare::Array, + _ if t.any(&bytes) => Compare::Deep, + _ => Compare::Value, + } +} + +/// Emit a data class (`head` is `data class Name`, `tail` its supertype), +/// overriding `equals`, `hashCode`, and `toString` with content semantics +/// when a field holds a `ByteArray` (whose own `equals` is identity). +fn emit_data_class( + w: &mut CodeWriter, + sp: &Speller, + class: &str, + head: &str, + props: &[Prop], + tail: &str, +) { + if props + .iter() + .all(|p| matches!(compare(&p.field.ty), Compare::Value)) + { + emit_properties(w, sp, head, props, tail); + return; + } + emit_properties(w, sp, head, props, &format!("{tail} {{")); + w.scope(|w| { + w.line("override fun equals(other: Any?): Boolean {"); + w.scope(|w| { + w.line("if (this === other) return true"); + w.line(format!("if (other !is {class}) return false")); + let terms: Vec = props + .iter() + .map(|p| { + let x = &p.name; + match compare(&p.field.ty) { + Compare::Value => format!("{x} == other.{x}"), + Compare::Array => format!("{x}.contentEquals(other.{x})"), + Compare::Deep => format!("deepEquals({x}, other.{x})"), + } + }) + .collect(); + for (i, t) in terms.iter().enumerate() { + match (i, i + 1 == terms.len()) { + (0, true) => w.line(format!("return {t}")), + (0, false) => w.line(format!("return {t} &&")), + (_, last) => { + w.indent(); + w.line(if last { t.clone() } else { format!("{t} &&") }); + w.dedent() + } + }; + } + }); + w.line("}"); + w.blank(); + w.line("override fun hashCode(): Int {"); + w.scope(|w| { + for (i, p) in props.iter().enumerate() { + let x = &p.name; + let h = match compare(&p.field.ty) { + Compare::Value => format!("{x}.hashCode()"), + Compare::Array => format!("{x}.contentHashCode()"), + Compare::Deep => format!("deepHashCode({x})"), + }; + if i == 0 { + w.line(format!("var result = {h}")); + } else { + w.line(format!("result = 31 * result + {h}")); + } + } + w.line("return result"); + }); + w.line("}"); + w.blank(); + let parts: Vec = props + .iter() + .map(|p| { + let x = &p.name; + match compare(&p.field.ty) { + Compare::Value => format!("{x}=${{{x}}}"), + Compare::Array => format!("{x}=${{{x}.contentToString()}}"), + Compare::Deep => format!("{x}=${{deepToString({x})}}"), + } + }) + .collect(); + let simple = class.rsplit('.').next().unwrap_or(class); w.line(format!( - "{head}({}){tail}", - field_decls(n, fields).join(", ") + "override fun toString(): String = \"{simple}({})\"", + parts.join(", ") )); - } + }); + w.line("}"); } /// A C-style enum: an `enum class` carrying its ABI value. -fn render_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { +fn render_enum(w: &mut CodeWriter, n: &Names, sp: &Speller, e: &EnumBinding) { let name = n.ty(&e.name); w.blank(); - w.doc(&e.doc, DocCommentStyle::Javadoc); - deprecated(w, &e.deprecated); + sp.doc(w, &e.doc); + deprecated(w, sp, &e.deprecated); w.line(format!("enum class {name}(val value: Int) {{")); w.scope(|w| { for (i, v) in e.variants.iter().enumerate() { - w.doc(&v.doc, DocCommentStyle::Javadoc); + sp.doc(w, &v.doc); let end = if i + 1 == e.variants.len() { ";" } else { "," }; w.line(format!("{}({}){end}", kt_escape(&v.name), v.value)); } @@ -71,6 +196,7 @@ fn render_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { w.line("companion object {"); w.scope(|w| { w.line("/** The variant whose ABI value is [value]. */"); + w.line("@JvmStatic"); w.line(format!( "fun fromValue(value: Int): {name} = entries.firstOrNull {{ it.value == value }}" )); @@ -85,24 +211,27 @@ fn render_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { /// A rich enum: a sealed class with one subclass per variant, plus its /// buffer codec (an `i32` tag, then the variant's fields). -fn render_rich_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { +fn render_rich_enum(w: &mut CodeWriter, n: &Names, sp: &Speller, e: &EnumBinding) { let name = n.ty(&e.name); + let variant = |v: &str| kt_escape(&pascal_case(v)); w.blank(); - w.doc(&e.doc, DocCommentStyle::Javadoc); - deprecated(w, &e.deprecated); + sp.doc(w, &e.doc); + deprecated(w, sp, &e.deprecated); w.line(format!("sealed class {name} {{")); w.scope(|w| { for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::Javadoc); - let vn = kt_escape(&pascal_case(&v.name)); + sp.doc(w, &v.doc); + let vn = variant(&v.name); if v.fields.is_empty() { - w.line(format!("object {vn} : {name}()")); + w.line(format!("data object {vn} : {name}()")); } else { - emit_properties( + let props = props(n, &v.fields, false); + emit_data_class( w, - n, + sp, + &format!("{name}.{vn}"), &format!("data class {vn}"), - &v.fields, + &props, &format!(" : {name}()"), ); } @@ -111,22 +240,23 @@ fn render_rich_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { w.line("}"); w.blank(); w.line(format!( - "internal fun pack{name}(_w: BufferWriter, _v: {name}) {{" + "internal fun pack_{}(_w: BufferWriter, _v: {name}) {{", + e.name )); w.scope(|w| { w.line("when (_v) {"); w.scope(|w| { for v in &e.variants { - let vn = kt_escape(&pascal_case(&v.name)); + let vn = variant(&v.name); if v.fields.is_empty() { w.line(format!("is {name}.{vn} -> _w.writeI32({})", v.value)); } else { w.line(format!("is {name}.{vn} -> {{")); w.scope(|w| { w.line(format!("_w.writeI32({})", v.value)); - for f in &v.fields { - let expr = format!("_v.{}", kt_escape(&f.name)); - w.line(write_expr(n, &f.ty, "_w", &expr)); + for p in props(n, &v.fields, false) { + let expr = format!("_v.{}", p.name); + w.line(write_expr(n, &p.field.ty, "_w", &expr)); } }); w.line("}"); @@ -138,11 +268,12 @@ fn render_rich_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { w.line("}"); w.blank(); w.line(format!( - "internal fun unpack{name}(_r: BufferReader): {name} = when (val _tag = _r.readI32()) {{" + "internal fun unpack_{}(_r: BufferReader): {name} = when (val _tag = _r.readI32()) {{", + e.name )); w.scope(|w| { for v in &e.variants { - let vn = kt_escape(&pascal_case(&v.name)); + let vn = variant(&v.name); if v.fields.is_empty() { w.line(format!("{} -> {name}.{vn}", v.value)); } else { @@ -160,38 +291,50 @@ fn render_rich_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { /// A record: a `data class` (a plain `class` when it has no fields), plus /// its buffer codec (the fields in declaration order). -fn render_struct(w: &mut CodeWriter, n: &Names, s: &StructBinding) { +fn render_struct(w: &mut CodeWriter, n: &Names, sp: &Speller, s: &StructBinding) { let name = n.ty(&s.name); + let stem = &s.name; w.blank(); - w.doc(&s.doc, DocCommentStyle::Javadoc); - deprecated(w, &s.deprecated); + sp.doc(w, &s.doc); + deprecated(w, sp, &s.deprecated); if s.fields.is_empty() { - w.line(format!("class {name}")); + w.line(format!("class {name} {{")); + w.scope(|w| { + w.line(format!( + "override fun equals(other: Any?): Boolean = other is {name}" + )); + w.blank(); + w.line("override fun hashCode(): Int = 0"); + w.blank(); + w.line(format!("override fun toString(): String = \"{name}()\"")); + }); + w.line("}"); w.blank(); w.line(format!( - "internal fun pack{name}(_w: BufferWriter, _v: {name}) {{}}" + "internal fun pack_{stem}(_w: BufferWriter, _v: {name}) {{}}" )); w.blank(); w.line(format!( - "internal fun unpack{name}(_r: BufferReader): {name} = {name}()" + "internal fun unpack_{stem}(_r: BufferReader): {name} = {name}()" )); return; } - emit_properties(w, n, &format!("data class {name}"), &s.fields, ""); + let props = props(n, &s.fields, false); + emit_data_class(w, sp, &name, &format!("data class {name}"), &props, ""); w.blank(); w.line(format!( - "internal fun pack{name}(_w: BufferWriter, _v: {name}) {{" + "internal fun pack_{stem}(_w: BufferWriter, _v: {name}) {{" )); w.scope(|w| { - for f in &s.fields { - let expr = format!("_v.{}", kt_escape(&f.name)); - w.line(write_expr(n, &f.ty, "_w", &expr)); + for p in &props { + let expr = format!("_v.{}", p.name); + w.line(write_expr(n, &p.field.ty, "_w", &expr)); } }); w.line("}"); w.blank(); w.line(format!( - "internal fun unpack{name}(_r: BufferReader): {name} = {name}(" + "internal fun unpack_{stem}(_r: BufferReader): {name} = {name}(" )); w.scope(|w| { for f in &s.fields { @@ -201,45 +344,68 @@ fn render_struct(w: &mut CodeWriter, n: &Names, s: &StructBinding) { w.line(")"); } -/// A callback interface: the Kotlin `interface` the consumer implements. -/// Its JNI dispatch shims live on `JniBridge`. A method declared `throws` -/// may throw the module's domain exception, which reaches the producer with -/// its code and fields; anything else it throws reaches it as code -4. +/// A callback interface: the Kotlin `interface` the consumer implements (a +/// `fun interface` when it has one method, so a lambda can implement it). +/// Its JNI dispatch shims live on `JniBridge`. A method that throws a +/// domain may throw that domain's exceptions, which reach the producer +/// with their code and fields; anything else it throws reaches the +/// producer as a failure with its message. fn render_callback_interface( w: &mut CodeWriter, n: &Names, + sp: &Speller, cb: &CallbackInterfaceBinding, - error: Option<&ErrorBinding>, ) { w.blank(); - w.doc(&cb.doc, DocCommentStyle::Javadoc); - deprecated(w, &cb.deprecated); - w.line(format!("interface {} {{", n.ty(&cb.name))); + sp.doc(w, &cb.doc); + deprecated(w, sp, &cb.deprecated); + let kind = if cb.methods.len() == 1 { + "fun interface" + } else { + "interface" + }; + w.line(format!("{kind} {} {{", n.ty(&cb.name))); w.scope(|w| { for (i, m) in cb.methods.iter().enumerate() { if i > 0 { w.blank(); } - let doc = match (m.throws, error) { - (true, Some(eb)) => { - let throws = format!( - "Throw a [{}] to report a typed error to the library.", - n.exception(eb) - ); - Some(match m.doc.as_deref().map(str::trim) { - Some(d) if !d.is_empty() => format!("{d}\n\n{throws}"), - _ => throws, - }) - } - _ => m.doc.clone(), + let note = match &m.error { + ErrorStrategy::Domain(d) => Some(format!( + "Throw a [{}] to report a typed error to the library; anything else\nthrown reaches it as an untyped failure with its message.", + n.exception(d) + )), + ErrorStrategy::Untyped => Some( + "Anything thrown reaches the library as a failure with its message." + .to_string(), + ), + ErrorStrategy::Trap => None, }; - fn_doc(w, &doc, &m.params); - deprecated(w, &m.deprecated); + let doc = match (sp.text(&m.doc), note) { + (Some(d), Some(note)) => Some(format!("{d}\n\n{note}")), + (d, note) => d.or(note), + }; + sp.fn_doc(w, doc, m.params.iter().map(|p| (p.name.as_str(), &p.doc))); + deprecated(w, sp, &m.deprecated); + match &m.error { + ErrorStrategy::Domain(d) => w.line(format!("@Throws({}::class)", n.exception(d))), + ErrorStrategy::Untyped => w.line("@Throws(Exception::class)"), + ErrorStrategy::Trap => w, + }; + let params: Vec = m + .params + .iter() + .map(|p| format!("{}: {}", kt_param(&p.name), n.kt_type(&p.ty))) + .collect(); + let ret = m + .ret + .as_ref() + .map(|t| format!(": {}", n.kt_type(t))) + .unwrap_or_default(); w.line(format!( - "fun {}({}){}", + "fun {}({}){ret}", kt_member(&m.name), - params_sig(n, &m.params), - ret_sig(n, m.ret.as_ref()) + params.join(", ") )); } }); @@ -249,19 +415,15 @@ fn render_callback_interface( /// An interface: an `AutoCloseable` wrapper owning one strong reference. /// Every call borrows the reference (so neither `close()` nor the cleaner /// can release it mid-call); constructors and statics live on the -/// companion, with the `new` constructor as `operator fun invoke`. -fn render_interface( - w: &mut CodeWriter, - n: &Names, - i: &InterfaceBinding, - error: Option<&ErrorBinding>, -) { +/// companion (as `@JvmStatic` functions), with the `new` constructor as +/// `operator fun invoke`. +fn render_interface(w: &mut CodeWriter, n: &Names, sp: &Speller, i: &InterfaceBinding) { let name = n.ty(&i.name); let destroy = n.native(&i.destroy_symbol); let clone = n.native(&i.clone_symbol); w.blank(); - w.doc(&i.doc, DocCommentStyle::Javadoc); - deprecated(w, &i.deprecated); + sp.doc(w, &i.doc); + deprecated(w, sp, &i.deprecated); // The address constructor is private so a `new` constructor taking one // `Long` (exposed as `invoke`) is never shadowed by it. w.line(format!( @@ -278,8 +440,18 @@ fn render_interface( )); for f in &i.methods { w.blank(); - fn_doc(w, &f.doc, &f.params); - emit_callable(w, n, f, &format!("fun {}", kt_member(&f.name)), true, error); + let name = kt_member(&f.name); + emit_callable( + w, + n, + sp, + f, + Decl { + name: &name, + operator: false, + jvm_static: false, + }, + ); } w.blank(); w.line("/** Releases this wrapper's native reference; safe to call more than once. */"); @@ -288,7 +460,9 @@ fn render_interface( w.line("companion object {"); w.scope(|w| { w.line("/** Adopts one strong reference (a return value or a buffer token). */"); - w.line(format!("internal fun fromHandle(address: Long): {name} = {name}(address)")); + w.line(format!( + "internal fun fromHandle(address: Long): {name} = {name}(address)" + )); w.blank(); w.line("/** [fromHandle], with address 0 meaning no object. */"); w.line(format!( @@ -296,18 +470,37 @@ fn render_interface( )); for c in &i.constructors { w.blank(); - fn_doc(w, &c.doc, &c.params); - let decl = if c.name == "new" { - "operator fun invoke".to_string() + let (cname, operator) = if c.name == "new" { + ("invoke".to_string(), true) } else { - format!("fun {}", kt_member(&c.name)) + (kt_member(&c.name), false) }; - emit_callable(w, n, c, &decl, false, error); + emit_callable( + w, + n, + sp, + c, + Decl { + name: &cname, + operator, + jvm_static: true, + }, + ); } for f in &i.statics { w.blank(); - fn_doc(w, &f.doc, &f.params); - emit_callable(w, n, f, &format!("fun {}", kt_member(&f.name)), false, error); + let name = kt_member(&f.name); + emit_callable( + w, + n, + sp, + f, + Decl { + name: &name, + operator: false, + jvm_static: true, + }, + ); } }); w.line("}"); @@ -315,24 +508,39 @@ fn render_interface( w.line("}"); } -/// An error domain: a sealed exception class with one subclass per code -/// (payload fields as constructor properties, then the message with the -/// documented default) and the `fromCode` factory the bridge maps raw codes -/// through. Unknown codes, including every runtime (negative) code, map to -/// the generic `FfiException`. A subclass with fields encodes them for a -/// callback that throws it. -fn render_error_domain(w: &mut CodeWriter, n: &Names, eb: &ErrorBinding) { - let exc = n.exception(eb); +/// An error domain: an exception class (constructible only by the +/// bindings) with one nested subclass per code (payload fields as +/// constructor properties, then the message with the documented default) +/// and the `fromCode` factory the bridge maps positive codes through. +/// Domains are open: a code these bindings don't know (from a newer +/// library) is the base class itself, with its code and message. A +/// subclass with fields encodes them for a callback that throws it. +fn render_error_domain(w: &mut CodeWriter, n: &Names, sp: &Speller, eb: &ErrorBinding) { + let exc = n.exception(&eb.name); w.blank(); - w.line(format!("/** Errors of the `{}` domain. */", eb.name)); + w.line("/**"); w.line(format!( - "sealed class {exc}(code: Int, message: String) : FfiException(code, message) {{" + " * Errors of the `{}` domain, one subclass per code.", + eb.name + )); + w.line(" *"); + w.line(format!( + " * A code these bindings don't declare (from a newer library) is a plain `{exc}`" + )); + w.line(" * carrying that [code] and the library's message."); + w.line(" */"); + w.line(format!( + "open class {exc} internal constructor(code: Int, message: String) : FfiException(code, message) {{" )); w.scope(|w| { for ec in &eb.codes { - w.doc(&ec.doc, DocCommentStyle::Javadoc); + sp.doc(w, &ec.doc); let class = errors::pascal(&ec.name); - let mut params = field_decls(n, &ec.fields); + let props = props(n, &ec.fields, true); + let mut params: Vec = props + .iter() + .map(|p| format!("val {}: {}", p.name, p.ty)) + .collect(); params.push(format!("message: String = \"{}\"", kt_string(&ec.message))); let head = format!( "class {class}({}) : {exc}({}, message)", @@ -348,8 +556,8 @@ fn render_error_domain(w: &mut CodeWriter, n: &Names, eb: &ErrorBinding) { "override fun encodePayload(): ByteArray = encodeBuffer { _w ->", "}", |w| { - for f in &ec.fields { - w.line(write_expr(n, &f.ty, "_w", &kt_escape(&f.name))); + for p in &props { + w.line(write_expr(n, &p.field.ty, "_w", &p.name)); } }, ); @@ -358,8 +566,10 @@ fn render_error_domain(w: &mut CodeWriter, n: &Names, eb: &ErrorBinding) { w.blank(); w.line("internal companion object {"); w.scope(|w| { - w.line("/** The exception for a raw code and its serialized payload. */"); - w.line("fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) {"); + w.line("/** The exception for a positive code and its serialized payload. */"); + w.line(format!( + "fun fromCode(code: Int, message: String, payload: ByteArray?): {exc} = when (code) {{" + )); w.scope(|w| { for ec in &eb.codes { let class = errors::pascal(&ec.name); @@ -369,13 +579,13 @@ fn render_error_domain(w: &mut CodeWriter, n: &Names, eb: &ErrorBinding) { let reads: Vec = ec.fields.iter().map(|f| read_expr(n, &f.ty, "_r")).collect(); w.line(format!( - "{} -> if (payload == null) FfiException(code, message) else decodeBuffer(payload) {{ _r -> {class}({}, message) }}", + "{} -> if (payload == null) {exc}(code, message) else decodeBuffer(payload) {{ _r -> {class}({}, message) }}", ec.value, reads.join(", ") )); } } - w.line("else -> FfiException(code, message)"); + w.line(format!("else -> {exc}(code, message)")); }); w.line("}"); }); @@ -389,10 +599,17 @@ fn has_functions(model: &Model, m: &ModuleBinding) -> bool { !m.functions.is_empty() || model.children(m).any(|c| has_functions(model, c)) } -/// The module object for `m`: its free functions, then one nested object -/// per submodule that has functions of its own. -fn render_module_object(w: &mut CodeWriter, n: &Names, model: &Model, m: &ModuleBinding) { - w.doc(&m.doc, DocCommentStyle::Javadoc); +/// The module object for `m`: its free functions (`@JvmStatic`, so Java +/// sees static methods), then one nested object per submodule that has +/// functions of its own. +fn render_module_object( + w: &mut CodeWriter, + n: &Names, + sp: &Speller, + model: &Model, + m: &ModuleBinding, +) { + sp.doc(w, &m.doc); w.line(format!("object {} {{", n.object(m))); w.scope(|w| { let mut first = true; @@ -401,14 +618,17 @@ fn render_module_object(w: &mut CodeWriter, n: &Names, model: &Model, m: &Module w.blank(); } first = false; - fn_doc(w, &f.doc, &f.params); + let name = n.function(f); emit_callable( w, n, + sp, f, - &format!("fun {}", n.function(f)), - false, - model.error_domain(m), + Decl { + name: &name, + operator: false, + jvm_static: true, + }, ); } for child in model.children(m).filter(|c| has_functions(model, c)) { @@ -416,7 +636,7 @@ fn render_module_object(w: &mut CodeWriter, n: &Names, model: &Model, m: &Module w.blank(); } first = false; - render_module_object(w, n, model, child); + render_module_object(w, n, sp, model, child); } }); w.line("}"); @@ -424,36 +644,36 @@ fn render_module_object(w: &mut CodeWriter, n: &Names, model: &Model, m: &Module /// Render every declaration of the top-level module `root` and its /// submodules (the body of `{Root}.kt`, after the package line). -pub(crate) fn render_root(n: &Names, model: &Model, root: &ModuleBinding) -> String { +pub(crate) fn render_root(n: &Names, sp: &Speller, model: &Model, root: &ModuleBinding) -> String { let mut w = CodeWriter::four_space(); let tree = model .modules .iter() .filter(|m| m.segments.first() == root.segments.first()); for m in tree { - if let Some(eb) = m.errors.as_ref() { - render_error_domain(&mut w, n, eb); + for eb in &m.errors { + render_error_domain(&mut w, n, sp, eb); } for e in &m.enums { if e.is_rich() { - render_rich_enum(&mut w, n, e); + render_rich_enum(&mut w, n, sp, e); } else { - render_enum(&mut w, n, e); + render_enum(&mut w, n, sp, e); } } for s in &m.structs { - render_struct(&mut w, n, s); + render_struct(&mut w, n, sp, s); } for cb in &m.callback_interfaces { - render_callback_interface(&mut w, n, cb, model.error_domain(m)); + render_callback_interface(&mut w, n, sp, cb); } for i in &m.interfaces { - render_interface(&mut w, n, i, model.error_domain(m)); + render_interface(&mut w, n, sp, i); } } if has_functions(model, root) { w.blank(); - render_module_object(&mut w, n, model, root); + render_module_object(&mut w, n, sp, model, root); } w.finish() } diff --git a/crates/weaveffi-cli/src/targets/kotlin/jni.rs b/crates/weaveffi-cli/src/targets/kotlin/jni.rs index f3391e3a..4dc97f71 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/jni.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/jni.rs @@ -2,29 +2,34 @@ //! the load-time contract check, callback-interface trampolines and static //! vtables, and one `Java_..._JniBridge_*` export per `JniBridge` native. //! -//! Every export receives strings, bytes, and value buffers as a `ByteArray` -//! it borrows for the call as a `(ptr, len)` pair, and returns them as a -//! fresh `ByteArray` after freeing the producer's allocation with -//! `{prefix}_free_bytes`. The C argument list is built by walking the -//! callable's lowered [`AbiFn`] slots, so it matches the header exactly. +//! Every export reads each argument's C slots off the model's passing +//! contracts ([`ArgPass`], [`RetPass`], [`ResultPass`], [`ItemPass`], +//! [`CallbackRetPass`]), so the C call matches the header exactly. Strings, +//! bytes, value buffers, and typed arrays arrive as Java arrays and are +//! copied once (`GetArrayRegion`) into inline or heap storage for the +//! call; the producer's runs come back as new Java arrays +//! (`NewArray`), after which the shim frees them with +//! `{prefix}_free_bytes`. An optional scalar parameter arrives as its +//! presence flag and value; an optional scalar result leaves as a box. +use crate::codegen::contract; use crate::codegen::CodeWriter; use weaveffi_model::abi::{AbiParam, CType}; -use weaveffi_model::contract; use weaveffi_model::model::{ - contract_symbol, AbiFn, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, - InterfaceBinding, IteratorBinding, Model, + CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, InterfaceBinding, + IteratorBinding, Model, }; -use weaveffi_model::plan::{ArgPass, RetPass}; -use weaveffi_model::ty::{Family, Prim, Ty}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; use crate::targets::kotlin::bridge::{shim_descriptor, shim_name}; -use crate::targets::kotlin::calls::item_slot; -use crate::targets::kotlin::names::{jni_c_type, jni_kind, Names}; +use crate::targets::kotlin::carrier::{Carrier, Kind}; +use crate::targets::kotlin::names::Names; -/// The C name of a user parameter inside an export (`p_{name}`), which no -/// C keyword and no local of the shim can spell. -fn c_param(name: &str) -> String { +/// The C name of a user parameter's JNI argument inside an export +/// (`p_{name}`); its presence flag is `h_{name}`, its copied run +/// `r_{name}`, and its pinned callback `c_{name}`. The prefixes keep the +/// four apart and away from every fixed local (`env`, `err`, `rv`, ...). +fn arg(name: &str) -> String { format!("p_{name}") } @@ -43,11 +48,6 @@ fn trampoline(n: &Names, cb: &CallbackInterfaceBinding, method: &str) -> String format!("Jni_{}_{method}", n.native(&cb.c_tag)) } -/// The expected contract table of the top-level module `root`. -fn contract_var(root: &str) -> String { - format!("Jni_contract_{root}") -} - /// The pointee of a pointer slot (an iterator's `out_item`). fn pointee(t: &CType) -> &CType { match t { @@ -56,94 +56,165 @@ fn pointee(t: &CType) -> &CType { } } -/// One export under construction: its JNI parameters, the statements before -/// and after the C call, and the C call's arguments in slot order. +/// The C expression of a C scalar `v` as the JNI value of `kind`. +fn to_jni(kind: Kind, v: &str) -> String { + if kind == Kind::Boolean { + format!("{v} ? JNI_TRUE : JNI_FALSE") + } else { + format!("({}){v}", kind.jni()) + } +} + +/// The C expression boxing the C scalar `v` of `kind`. +fn boxed(kind: Kind, v: &str) -> String { + format!( + "Jni_box(env, {}, (jvalue){{.{} = {}}})", + kind.c_const(), + kind.jvalue(), + to_jni(kind, v) + ) +} + +/// The C expression of the JNI value `v` of `kind` as the C type `ty`. +fn from_jni(kind: Kind, ty: &str, v: &str) -> String { + if kind == Kind::Boolean { + format!("{v} == JNI_TRUE") + } else { + format!("({ty}){v}") + } +} + +/// One step before a native call that can fail on the JVM side: its +/// statements, the condition under which it failed (with an exception +/// pending), and how to undo it if a later step fails. +struct Step { + lines: Vec, + failed: String, + undo: String, +} + +/// One export under construction: its JNI parameters (after `env` and +/// `cls`), the fallible steps, the C call's arguments in slot order, and +/// the releases after the call. +#[derive(Default)] struct Export { params: Vec, - before: Vec, - after: Vec, + steps: Vec, args: Vec, + after: Vec, +} + +impl Export { + /// Emit the steps, each failure undoing the earlier ones and returning + /// `fail` (` 0`, ` NULL`, or empty for void). + fn emit_steps(&self, w: &mut CodeWriter, fail: &str) { + for (i, step) in self.steps.iter().enumerate() { + for l in &step.lines { + w.line(l); + } + w.line(format!("if ({}) {{", step.failed)); + w.scope(|w| { + for earlier in self.steps[..i].iter().rev() { + w.line(&earlier.undo); + } + w.line(format!("return{fail};")); + }); + w.line("}"); + } + } } -/// Lower `f`'s receiver and parameters onto the slots of `abi`, then the -/// trailing slots (`out_len`, `out_err`, `cancel_token`, `callback`, -/// `context`) by name. -fn lower_export(n: &Names, model: &Model, f: &FnBinding, abi: &AbiFn, done: &str) -> Export { +/// Lower `f`'s receiver and parameters onto their C slots, borrowing every +/// array argument and pinning every callback implementation. +fn lower_export(n: &Names, model: &Model, f: &FnBinding) -> Export { let p = &n.prefix; - let mut e = Export { - params: Vec::new(), - before: Vec::new(), - after: Vec::new(), - args: Vec::new(), - }; - let mut slots = abi.params.iter(); - if f.has_self { - let slot = slots.next().expect("a method has a receiver slot"); + let mut e = Export::default(); + if let Some(slot) = &f.receiver { e.params.push("jlong self".into()); e.args .push(format!("({})(intptr_t)self", slot.ty.render_c(p))); } for param in &f.params { - for _ in ¶m.abi { - slots.next(); - } - let name = c_param(¶m.name); - e.params.push(format!("{} {name}", jni_c_type(¶m.ty))); - match param.arg_pass() { - ArgPass::Direct { slot } => e.args.push(if matches!(param.ty, Ty::Prim(Prim::Bool)) { - format!("{name} == JNI_TRUE") - } else { - format!("({}){name}", slot.ty.render_c(p)) - }), - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { - e.before.push(format!( - "Jni_bytes {name}_b = Jni_borrow_bytes(env, {name});" - )); - e.after.push(format!("Jni_release_bytes(env, &{name}_b);")); - e.args.push(format!("{name}_b.ptr")); - e.args.push(format!("{name}_b.len")); + let name = arg(¶m.name); + let carrier = Carrier::of_arg(¶m.pass); + match (¶m.pass, carrier) { + (ArgPass::Direct { slot }, Some(Carrier::Prim(k))) => { + e.params.push(format!("{} {name}", k.jni())); + e.args.push(from_jni(k, &slot.ty.render_c(p), &name)); } - ArgPass::Object { slot, .. } => { + (ArgPass::OptDirect { value, .. }, Some(Carrier::Split(k))) => { + let has = format!("h_{}", param.name); + e.params.push(format!("jboolean {has}")); + e.params.push(format!("{} {name}", k.jni())); + e.args.push(format!("{has} == JNI_TRUE")); + e.args.push(from_jni(k, &value.ty.render_c(p), &name)); + } + ( + ArgPass::Slice { ptr, .. } + | ArgPass::String { ptr, .. } + | ArgPass::Bytes { ptr, .. } + | ArgPass::Buffer { ptr, .. }, + Some(c), + ) => { + let kind = c.run_kind().expect("runs have an element kind"); + let run = format!("r_{}", param.name); + e.params.push(format!("{} {name}", c.jni())); + e.steps.push(Step { + lines: vec![format!("Jni_run {run};")], + failed: format!( + "!Jni_borrow(env, {name}, {}, &{run})", + Carrier::Array(kind).c_kind() + ), + undo: format!("Jni_unborrow(&{run});"), + }); + e.args.push(format!("({}){run}.ptr", ptr.ty.render_c(p))); + e.args.push(format!("{run}.len")); + e.after.push(format!("Jni_unborrow(&{run});")); + } + (ArgPass::Object { slot, .. }, _) => { + e.params.push(format!("jlong {name}")); e.args .push(format!("({})(intptr_t){name}", slot.ty.render_c(p))); } - ArgPass::Callback { nullable, .. } => { - let cb = model.callback_interface( - param - .ty - .callback_interface_name() - .expect("callback slots name a callback interface"), - ); - // The producer owns this global reference from here on and - // drops it through the vtable's `free`. - let ctx = format!("(void*)(*env)->NewGlobalRef(env, {name})"); + ( + ArgPass::Callback { + nullable, + interface, + .. + }, + _, + ) => { + let cb = model.callback_interface(interface); + let ctx = format!("c_{}", param.name); + e.params.push(format!("jobject {name}")); + // The producer owns this global reference from the call on + // and drops it through the vtable's `free`. + e.steps.push(Step { + lines: vec![format!("void* {ctx} = Jni_pin(env, {name});")], + failed: format!("{name} != NULL && {ctx} == NULL"), + undo: format!("Jni_unpin(env, {ctx});"), + }); + e.args.push(ctx); let vtable = format!("&{}", vtable_var(n, cb)); - if nullable { - e.args.push(format!("{name} != NULL ? {ctx} : NULL")); - e.args.push(format!("{name} != NULL ? {vtable} : NULL")); + e.args.push(if *nullable { + format!("{name} != NULL ? {vtable} : NULL") } else { - e.args.push(ctx); - e.args.push(vtable); - } + vtable + }); } + (pass, c) => unreachable!("no JNI form for {pass:?} as {c:?}"), } } - for slot in slots { - e.args.push(match slot.name.as_str() { - "out_len" => "&out_len".into(), - "out_err" => "&err".into(), - "cancel_token" => { - e.params.push("jlong cancel_token".into()); - format!("({p}_cancel_token*)(intptr_t)cancel_token") - } - "callback" => done.to_string(), - "context" => "context".into(), - other => unreachable!("unexpected trailing slot '{other}'"), - }); - } e } +impl Carrier { + /// The shim's `Jni_kind` constant of an array or byte-array carrier. + fn c_kind(self) -> String { + self.run_kind().unwrap_or(Kind::Byte).c_const() + } +} + /// Emit `JNIEXPORT {ret} JNICALL {export}(...) {` with the shared leading /// parameters. fn open_export(w: &mut CodeWriter, n: &Names, ret: &str, native: &str, params: &[String]) { @@ -156,7 +227,7 @@ fn open_export(w: &mut CodeWriter, n: &Names, ret: &str, native: &str, params: & )); } -/// The error check after a sync call: throw through `domain` and return. +/// The error check after a call: throw through `domain` and return. fn error_check(w: &mut CodeWriter, domain: u32, fail: &str) { w.line("if (err.code != 0) {"); w.line(format!(" Jni_throw(env, &err, {domain});")); @@ -164,135 +235,167 @@ fn error_check(w: &mut CodeWriter, domain: u32, fail: &str) { w.line("}"); } -/// The JNI value of a direct C value `v` of `t`. -fn direct_to_jni(t: &Ty, v: &str) -> String { - if matches!(t, Ty::Prim(Prim::Bool)) { - format!("{v} ? JNI_TRUE : JNI_FALSE") - } else { - format!("({}){v}", jni_c_type(t)) - } +/// The zero-initialized error every export passes as `out_err`. +fn err_decl(p: &str) -> String { + format!("{p}_error err = {{0, NULL, 0, NULL, 0}};") } -/// A synchronous callable (or an iterator launcher). -fn render_sync( - w: &mut CodeWriter, - n: &Names, - model: &Model, - f: &FnBinding, - abi: &AbiFn, - domain: u32, -) { +/// A synchronous callable or an iterator launcher. +fn render_sync(w: &mut CodeWriter, n: &Names, model: &Model, f: &FnBinding) { let p = &n.prefix; - let native = n.native(&abi.symbol); - let e = lower_export(n, model, f, abi, ""); - let is_iter = matches!(f.shape, CallShape::Iterator(_)); - let ret = if is_iter { - RetPass::Object { nullable: false } - } else { - RetPass::of(f.ret.as_ref()) - }; - let (jret, fail) = match &ret { - RetPass::Void => ("void", ""), - RetPass::String | RetPass::Bytes | RetPass::Buffer => ("jbyteArray", " NULL"), - RetPass::Object { .. } => ("jlong", " 0"), - RetPass::Direct => ( - jni_c_type(f.ret.as_ref().expect("direct returns have a type")), - " 0", - ), + let native = n.native(&f.abi.symbol); + let e = lower_export(n, model, f); + let value = f.ret.as_ref().and_then(|r| r.value()); + let carrier = Carrier::of_ret(&f.ret_pass, value); + let (jret, fail) = match carrier { + None => ("void".to_string(), ""), + Some(c @ (Carrier::Prim(_) | Carrier::Handle)) => (c.jni(), " 0"), + Some(c) => (c.jni(), " NULL"), }; - open_export(w, n, jret, &native, &e.params); + open_export(w, n, &jret, &native, &e.params); w.scope(|w| { - w.line(format!("{p}_error err = {{0, NULL, NULL, 0}};")); + w.line(err_decl(p)); + // The return's out slots, then `out_err`. + let mut args = e.args.clone(); + match &f.ret_pass { + RetPass::OptDirect { out_value } => { + w.line(format!( + "{} {} = 0;", + pointee(&out_value.ty).render_c(p), + out_value.name + )); + args.push(format!("&{}", out_value.name)); + } + RetPass::Slice { out_len, .. } + | RetPass::String { out_len } + | RetPass::Bytes { out_len } + | RetPass::Buffer { out_len } => { + w.line(format!("size_t {} = 0;", out_len.name)); + args.push(format!("&{}", out_len.name)); + } + _ => {} + } + args.push("&err".into()); w.line("(void)cls;"); - for s in &e.before { - w.line(s); + e.emit_steps(w, fail); + let call = format!("{}({})", f.abi.symbol, args.join(", ")); + if carrier.is_none() { + w.line(format!("{call};")); + } else { + w.line(format!("{} rv = {call};", f.abi.ret.render_c(p))); } - let call = format!("{}({})", abi.symbol, e.args.join(", ")); - match &ret { - RetPass::Void => w.line(format!("{call};")), - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - w.line("size_t out_len = 0;"); - w.line(format!("{} rv = {call};", abi.ret.render_c(p))) - } - _ => w.line(format!("{} rv = {call};", abi.ret.render_c(p))), - }; for s in &e.after { w.line(s); } - error_check(w, domain, fail); - match &ret { - RetPass::Void => {} - RetPass::Object { .. } => { - w.line("return (jlong)(intptr_t)rv;"); + error_check(w, n.domain_index(&f.error), fail); + match (&f.ret_pass, carrier) { + (_, None) => {} + (RetPass::OptDirect { out_value }, Some(Carrier::Boxed(k))) => { + w.line(format!("return rv ? {} : NULL;", boxed(k, &out_value.name))); } - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - w.line("return Jni_take_bytes(env, rv, out_len);"); + ( + RetPass::Slice { out_len, .. } + | RetPass::String { out_len } + | RetPass::Bytes { out_len } + | RetPass::Buffer { out_len }, + Some(c), + ) => { + w.line(format!( + "return ({})Jni_take_array(env, {}, rv, {});", + c.jni(), + c.c_kind(), + out_len.name + )); + } + (_, Some(Carrier::Handle)) => { + w.line("return (jlong)(intptr_t)rv;"); } - RetPass::Direct => { - let t = f.ret.as_ref().expect("direct returns have a type"); - w.line(format!("return {};", direct_to_jni(t, "rv"))); + (_, Some(Carrier::Prim(k))) => { + w.line(format!("return {};", to_jni(k, "rv"))); } - }; + (pass, c) => unreachable!("no JNI return for {pass:?} as {c:?}"), + } }); w.line("}"); w.blank(); } +/// The C expression making the `Any?` Kotlin receives for an async result. +fn result_value(pass: &ResultPass, c: Option) -> String { + match (pass, c) { + (ResultPass::Void, _) | (_, None) => "NULL".to_string(), + (ResultPass::Direct { result }, Some(Carrier::Prim(k))) => boxed(k, &result.name), + (ResultPass::OptDirect { has, value }, Some(Carrier::Boxed(k))) => { + format!("{} ? {} : NULL", has.name, boxed(k, &value.name)) + } + ( + ResultPass::Slice { ptr, len, .. } + | ResultPass::String { ptr, len } + | ResultPass::Bytes { ptr, len } + | ResultPass::Buffer { ptr, len }, + Some(c), + ) => format!( + "Jni_take_array(env, {}, {}, {})", + c.c_kind(), + ptr.name, + len.name + ), + (ResultPass::Object { result, .. }, _) => { + boxed(Kind::Long, &format!("(intptr_t){}", result.name)) + } + (pass, c) => unreachable!("no async result for {pass:?} as {c:?}"), + } +} + /// An async callable: its completion trampoline, then the launcher export. fn render_async(w: &mut CodeWriter, n: &Names, model: &Model, f: &FnBinding) { let CallShape::Async(ab) = &f.shape else { unreachable!("render_async needs an async call shape"); }; let p = &n.prefix; - let native = n.native(&ab.launch.symbol); + let native = n.native(&f.abi.symbol); let done = format!("Jni_done_{native}"); - let result_slots: Vec<&AbiParam> = ab.callback_params.iter().skip(2).collect(); - let decls: String = result_slots + let decls: Vec = ab + .callback_params .iter() - .map(|s| format!(", {} {}", s.ty.render_c(p), s.name)) + .map(|s| format!("{} {}", s.ty.render_c(p), s.name)) .collect(); - w.line(format!( - "static void {done}(void* context, {p}_error* err{decls}) {{" - )); + w.line(format!("static void {done}({}) {{", decls.join(", "))); w.scope(|w| { w.line("int detach = 0;"); w.line("JNIEnv* env = Jni_complete_begin(context, err, &detach);"); w.line("if (env == NULL) {"); w.line(" return;"); w.line("}"); - let deliver = match RetPass::of(f.ret.as_ref()) { - RetPass::Void => "Jni_on_unit".to_string(), - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - "Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)".to_string() - } - RetPass::Object { .. } => "Jni_on_long, (jlong)(intptr_t)result".to_string(), - RetPass::Direct => { - let t = f.ret.as_ref().expect("direct results have a type"); - format!( - "Jni_on_{}, {}", - jni_kind(t).to_lowercase(), - direct_to_jni(t, "result") - ) - } - }; + let value = f.ret.as_ref().and_then(|r| r.value()); + let c = Carrier::of_result(&ab.result, value); w.line(format!( - "(*env)->CallVoidMethod(env, (jobject)context, {deliver});" + "Jni_complete(env, context, detach, {});", + result_value(&ab.result, c) )); - w.line("Jni_complete_end(env, context, detach);"); }); w.line("}"); w.blank(); - let mut e = lower_export(n, model, f, &ab.launch, &done); + let mut e = lower_export(n, model, f); + let mut args = e.args.clone(); + if let Some(token) = &ab.cancel_token { + e.params.push("jlong cancel_token".into()); + args.push(format!("({})(intptr_t)cancel_token", token.ty.render_c(p))); + } + args.push(done); + args.push("context".into()); e.params.push("jobject completion".into()); + e.steps.push(Step { + lines: vec!["void* context = Jni_pin(env, completion);".into()], + failed: "context == NULL".into(), + undo: String::new(), + }); open_export(w, n, "void", &native, &e.params); w.scope(|w| { - w.line("void* context = (void*)(*env)->NewGlobalRef(env, completion);"); w.line("(void)cls;"); - for s in &e.before { - w.line(s); - } - w.line(format!("{}({});", ab.launch.symbol, e.args.join(", "))); + e.emit_steps(w, ""); + w.line(format!("{}({});", f.abi.symbol, args.join(", "))); for s in &e.after { w.line(s); } @@ -301,64 +404,75 @@ fn render_async(w: &mut CodeWriter, n: &Names, model: &Model, f: &FnBinding) { w.blank(); } -/// An iterator's `_next` and `_destroy` exports. -fn render_iterator_natives(w: &mut CodeWriter, n: &Names, it: &IteratorBinding, domain: u32) { +/// An iterator's `_next` and `_destroy` exports. `_next` hands the item to +/// Kotlin through a one-element `Array` and returns whether there was +/// one. +fn render_iterator_natives(w: &mut CodeWriter, n: &Names, f: &FnBinding, it: &IteratorBinding) { let p = &n.prefix; let tag = &it.iter_tag; - let item_ty = pointee(&it.next.params[1].ty).render_c(p); let next = n.native(&it.next.symbol); - match item_slot(&it.elem) { - None => { - open_export(w, n, "jbyteArray", &next, &["jlong iter".to_string()]); - w.scope(|w| { - w.line(format!("{p}_error err = {{0, NULL, NULL, 0}};")); - w.line(format!("{item_ty} item = NULL;")); - w.line("size_t out_len = 0;"); - w.line("(void)cls;"); - w.line(format!( - "int32_t has = {}(({tag}*)(intptr_t)iter, &item, &out_len, &err);", - it.next.symbol - )); - error_check(w, domain, " NULL"); - w.line("if (has == 0) {"); - w.line(" return NULL;"); - w.line("}"); - w.line("return Jni_take_bytes(env, item, out_len);"); - }); + let local = |slot: &AbiParam, init: &str| { + format!("{} {} = {init};", pointee(&slot.ty).render_c(p), slot.name) + }; + let c = Carrier::of_item(&it.item, &it.elem); + let (decls, value) = match (&it.item, c) { + (ItemPass::Direct { out_item }, Carrier::Prim(k)) => { + (vec![local(out_item, "0")], boxed(k, &out_item.name)) } - Some(slot) => { - open_export( - w, - n, - "jboolean", - &next, - &["jlong iter".to_string(), format!("{} out", slot.jni_array)], - ); - w.scope(|w| { - w.line(format!("{p}_error err = {{0, NULL, NULL, 0}};")); - w.line(format!("{item_ty} item = ({item_ty})0;")); - w.line("(void)cls;"); - w.line(format!( - "int32_t has = {}(({tag}*)(intptr_t)iter, &item, &err);", - it.next.symbol - )); - error_check(w, domain, " JNI_FALSE"); - w.line("if (has == 0) {"); - w.line(" return JNI_FALSE;"); - w.line("}"); - let value = match it.elem.family() { - Family::Object { .. } => "(jlong)(intptr_t)item".to_string(), - _ => direct_to_jni(&it.elem, "item"), - }; - w.line(format!("{} value = {value};", slot.jni_elem)); - w.line(format!( - "(*env)->Set{}ArrayRegion(env, out, 0, 1, &value);", - slot.region - )); - w.line("return JNI_TRUE;"); - }); + (ItemPass::OptDirect { out_has, out_item }, Carrier::Boxed(k)) => ( + vec![local(out_has, "false"), local(out_item, "0")], + format!("{} ? {} : NULL", out_has.name, boxed(k, &out_item.name)), + ), + ( + ItemPass::Slice { + out_item, out_len, .. + } + | ItemPass::String { out_item, out_len } + | ItemPass::Bytes { out_item, out_len } + | ItemPass::Buffer { out_item, out_len }, + c, + ) => ( + vec![local(out_item, "NULL"), local(out_len, "0")], + format!( + "Jni_take_array(env, {}, {}, {})", + c.c_kind(), + out_item.name, + out_len.name + ), + ), + (ItemPass::Object { out_item, .. }, _) => ( + vec![local(out_item, "NULL")], + boxed(Kind::Long, &format!("(intptr_t){}", out_item.name)), + ), + (item, c) => unreachable!("no iterator item for {item:?} as {c:?}"), + }; + let mut args = vec![format!("({tag}*)(intptr_t)iter")]; + args.extend(it.item.slots().iter().map(|s| format!("&{}", s.name))); + args.push("&err".into()); + open_export( + w, + n, + "jboolean", + &next, + &["jlong iter".to_string(), "jobjectArray out".to_string()], + ); + w.scope(|w| { + w.line(err_decl(p)); + for d in &decls { + w.line(d); } - } + w.line("(void)cls;"); + w.line(format!( + "int32_t has = {}({});", + it.next.symbol, + args.join(", ") + )); + error_check(w, n.domain_index(&f.error), " JNI_FALSE"); + w.line("if (has == 0) {"); + w.line(" return JNI_FALSE;"); + w.line("}"); + w.line(format!("return Jni_yield(env, out, {value});")); + }); w.line("}"); w.blank(); open_export( @@ -424,28 +538,40 @@ fn render_trampoline( m: &CallbackMethodBinding, ) { let p = &n.prefix; - // `ctx` first and `out_err` last; the parameters' slots, then the - // return's out slots, in between. - let inner = &m.abi_params[1..m.abi_params.len() - 1]; - let param_slots: usize = m.params.iter().map(|x| x.abi.len()).sum(); - let (params, outs) = inner.split_at(param_slots); + // `ctx`, the parameters' slots (as `p_{slot}`), the return's out slots, + // then `out_err`. let mut decls = vec!["void* ctx".to_string()]; - decls.extend( - params - .iter() - .map(|s| format!("{} {}", s.ty.render_c(p), c_param(&s.name))), - ); - decls.extend( - outs.iter() - .map(|s| format!("{} {}", s.ty.render_c(p), s.name)), - ); + for param in &m.params { + for s in param.pass.slots() { + decls.push(format!("{} {}", s.ty.render_c(p), arg(&s.name))); + } + } + for s in m.ret_pass.out_slots() { + decls.push(format!("{} {}", s.ty.render_c(p), s.name)); + } decls.push(format!("{p}_error* out_err")); - let ret_c = m.abi_ret.render_c(p); - let ret = RetPass::of(m.ret.as_ref()); - let fail = match ret { - RetPass::Direct | RetPass::Object { .. } => format!(" ({ret_c})0"), - _ => String::new(), + let ret_c = m.abi.ret.render_c(p); + let fail = if matches!(m.abi.ret, CType::Void) { + String::new() + } else { + format!(" ({ret_c})0") }; + // The object arguments are the trampoline's to release when the call + // can't reach the implementation. + let adopted: Vec = m + .params + .iter() + .filter_map(|param| match ¶m.pass { + ArgPass::Object { + slot, interface, .. + } => Some(format!( + "if ({0} != NULL) {{ {1}({0}); }}", + arg(&slot.name), + model.interface(interface).destroy_symbol + )), + _ => None, + }) + .collect(); w.line(format!( "static {ret_c} {}({}) {{", trampoline(n, cb, &m.name), @@ -456,18 +582,8 @@ fn render_trampoline( w.line("JNIEnv* env = Jni_callback_begin(out_err, &detach);"); w.line("if (env == NULL) {"); w.scope(|w| { - // The object arguments are ours to release when the call can't - // reach the implementation. - let mut slots = params.iter(); - for param in &m.params { - let own: Vec<&AbiParam> = param.abi.iter().filter_map(|_| slots.next()).collect(); - if let Some(name) = param.ty.interface_name() { - w.line(format!( - "{}({});", - model.interface(name).destroy_symbol, - c_param(&own[0].name) - )); - } + for a in &adopted { + w.line(a); } w.line(format!("return{fail};")); }); @@ -476,57 +592,114 @@ fn render_trampoline( "(jobject)ctx".to_string(), "(jlong)(intptr_t)out_err".to_string(), ]; - let mut slots = params.iter(); + let mut arrays = false; for param in &m.params { - let own: Vec<&AbiParam> = param.abi.iter().filter_map(|_| slots.next()).collect(); - args.push(match param.ty.family() { - Family::String | Family::Bytes | Family::Buffer => format!( - "Jni_new_bytes(env, {}, {})", - c_param(&own[0].name), - c_param(&own[1].name) - ), - Family::Object { .. } => format!("(jlong)(intptr_t){}", c_param(&own[0].name)), - _ => direct_to_jni(¶m.ty, &c_param(&own[0].name)), + match (¶m.pass, Carrier::of_arg(¶m.pass)) { + (ArgPass::Direct { slot }, Some(Carrier::Prim(k))) => { + args.push(to_jni(k, &arg(&slot.name))); + } + (ArgPass::OptDirect { has, value, .. }, Some(Carrier::Split(k))) => { + args.push(to_jni(Kind::Boolean, &arg(&has.name))); + args.push(to_jni(k, &arg(&value.name))); + } + ( + ArgPass::Slice { ptr, len, .. } + | ArgPass::String { ptr, len } + | ArgPass::Bytes { ptr, len } + | ArgPass::Buffer { ptr, len }, + Some(c), + ) => { + let local = format!("a_{}", param.name); + w.line(format!( + "jarray {local} = Jni_new_array(env, {}, {}, {});", + c.c_kind(), + arg(&ptr.name), + arg(&len.name) + )); + arrays = true; + args.push(local); + } + (ArgPass::Object { slot, .. }, _) => { + args.push(format!("(jlong)(intptr_t){}", arg(&slot.name))); + } + (pass, c) => unreachable!("no callback argument for {pass:?} as {c:?}"), + } + } + if arrays { + // An argument the JVM couldn't make: the call never happens. + w.line("if ((*env)->ExceptionCheck(env)) {"); + w.scope(|w| { + for a in &adopted { + w.line(a); + } + w.line("Jni_callback_end(env, out_err, detach);"); + w.line(format!("return{fail};")); }); + w.line("}"); } - let kind = match (&ret, m.ret.as_ref()) { - (RetPass::Void, _) => "Void", - (RetPass::String | RetPass::Bytes | RetPass::Buffer, _) => "Object", - (RetPass::Object { .. }, _) => "Long", - (RetPass::Direct, Some(t)) => jni_kind(t), - (RetPass::Direct, None) => unreachable!("direct returns have a type"), - }; + let ret = Carrier::of_callback_ret(&m.ret_pass, m.ret.as_ref()); + let stem = ret.map_or("Void", Carrier::call_stem); let call = format!( - "(*env)->CallStatic{kind}Method(env, Jni_bridge, {}, {})", + "(*env)->CallStatic{stem}Method(env, Jni_bridge, {}, {})", shim_mid(n, cb, &m.name), args.join(", ") ); - match (&ret, m.ret.as_ref()) { - (RetPass::Void, _) => { + match (&m.ret_pass, ret) { + (CallbackRetPass::Void, _) | (_, None) => { w.line(format!("{call};")); w.line("Jni_callback_end(env, out_err, detach);"); } - (RetPass::String | RetPass::Bytes | RetPass::Buffer, _) => { - w.line(format!("jbyteArray rv = (jbyteArray){call};")); - w.line("Jni_callback_return_bytes(env, rv, out_ptr, out_len, out_err);"); + (CallbackRetPass::Direct, Some(Carrier::Prim(k))) => { + // A failure leaves the zero value the shim (or JNI) returned. + w.line(format!("{} rv = {call};", k.jni())); w.line("Jni_callback_end(env, out_err, detach);"); + w.line(format!("return {};", from_jni(k, &ret_c, "rv"))); } - (RetPass::Object { .. }, _) => { - w.line(format!("jlong rv = {call};")); + (CallbackRetPass::OptDirect { out_value }, Some(Carrier::Boxed(k))) => { + w.line("jvalue value = {0};"); + w.line(format!( + "bool present = Jni_callback_opt(env, {}, {call}, &value);", + k.c_const() + )); + w.line("if (present) {"); + w.line(format!( + " *{} = {};", + out_value.name, + from_jni( + k, + &pointee(&out_value.ty).render_c(p), + &format!("value.{}", k.jvalue()) + ) + )); + w.line("}"); w.line("Jni_callback_end(env, out_err, detach);"); - w.line(format!("return ({ret_c})(intptr_t)rv;")); + w.line("return present;"); } - (RetPass::Direct, Some(t)) => { - // A failure leaves the zero value the shim (or JNI) returned. - w.line(format!("{} rv = {call};", jni_c_type(t))); - w.line("Jni_callback_end(env, out_err, detach);"); - if matches!(t, Ty::Prim(Prim::Bool)) { - w.line("return rv == JNI_TRUE;"); - } else { - w.line(format!("return ({ret_c})rv;")); + ( + CallbackRetPass::Slice { + out_ptr, out_len, .. } + | CallbackRetPass::String { out_ptr, out_len } + | CallbackRetPass::Bytes { out_ptr, out_len } + | CallbackRetPass::Buffer { out_ptr, out_len }, + Some(c), + ) => { + w.line(format!("jarray rv = (jarray){call};")); + w.line(format!( + "*{} = ({})Jni_callback_run(env, {}, rv, {}, out_err);", + out_ptr.name, + pointee(&out_ptr.ty).render_c(p), + c.c_kind(), + out_len.name + )); + w.line("Jni_callback_end(env, out_err, detach);"); + } + (CallbackRetPass::Object { .. }, Some(Carrier::Handle)) => { + w.line(format!("jlong rv = {call};")); + w.line("Jni_callback_end(env, out_err, detach);"); + w.line(format!("return ({ret_c})(intptr_t)rv;")); } - (RetPass::Direct, None) => unreachable!("direct returns have a type"), + (pass, c) => unreachable!("no callback return for {pass:?} as {c:?}"), } }); w.line("}"); @@ -569,42 +742,46 @@ fn render_callback_interface( /// The contract tables these bindings were generated with (one per /// top-level module that declares anything), then `Jni_load`: the contract -/// checks and the method IDs the generated code caches. +/// checks and the classes and method IDs the shim caches. fn render_load(w: &mut CodeWriter, n: &Names, model: &Model) { - let mut checked = Vec::new(); - for root in model.roots() { - let entries = contract::entries(model, root); - if entries.is_empty() { - continue; - } + let tables: Vec<_> = contract::tables(model) + .into_iter() + .filter(|t| !t.rows.is_empty()) + .collect(); + let var = |root: &str| format!("Jni_contract_{root}"); + for table in &tables { w.line(format!( "static const Jni_contract_entry {}[] = {{", - contract_var(&root.name) + var(&table.root.name) )); w.scope(|w| { - for e in &entries { + for row in &table.rows { w.line(format!( - "{{UINT64_C(0x{:016x}), UINT64_C(0x{:016x}), \"{}\"}},", - e.id, e.hash, e.path + "{{UINT64_C({}), UINT64_C({}), \"{}\"}}, /* {} */", + contract::hex(row.id), + contract::hex(row.hash), + row.path, + row.signature )); } }); w.line("};"); w.blank(); - checked.push(root); } w.line("static jint Jni_load(JNIEnv* env) {"); w.scope(|w| { - w.line("(void)env;"); - for root in checked { - let var = contract_var(&root.name); + for table in &tables { + let v = var(&table.root.name); w.line(format!( - "if (Jni_check_contract(env, {}, {var}, sizeof {var} / sizeof {var}[0]) != JNI_OK) {{", - contract_symbol(&n.prefix, &root.name) + "if (Jni_check_contract(env, {}, {v}, sizeof {v} / sizeof {v}[0]) != JNI_OK) {{", + table.symbol )); w.line(" return JNI_ERR;"); w.line("}"); } + w.line("if (Jni_load_boxes(env) != JNI_OK) {"); + w.line(" return JNI_ERR;"); + w.line("}"); if model.has_async() { w.line("if (Jni_load_async(env) != JNI_OK) {"); w.line(" return JNI_ERR;"); @@ -642,14 +819,13 @@ pub(crate) fn render_exports(n: &Names, model: &Model) -> String { render_interface_natives(&mut w, n, i); } for f in m.callables() { - let domain = n.domain(f, model.error_domain(m)); - match &f.shape { - CallShape::Sync(abi) => render_sync(&mut w, n, model, f, abi, domain), - CallShape::Iterator(it) => { - render_sync(&mut w, n, model, f, &it.launch, domain); - render_iterator_natives(&mut w, n, it, domain); + match (&f.shape, &f.ret_pass) { + (CallShape::Async(_), _) => render_async(&mut w, n, model, f), + (CallShape::Sync, RetPass::Iterator(it)) => { + render_sync(&mut w, n, model, f); + render_iterator_natives(&mut w, n, f, it); } - CallShape::Async(_) => render_async(&mut w, n, model, f), + (CallShape::Sync, _) => render_sync(&mut w, n, model, f), } } } diff --git a/crates/weaveffi-cli/src/targets/kotlin/mod.rs b/crates/weaveffi-cli/src/targets/kotlin/mod.rs index 6f20f568..0a913f39 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/mod.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/mod.rs @@ -9,22 +9,27 @@ //! the Android flavor builds it through the NDK's CMake and the JVM flavor //! with CMake directly. //! -//! The Kotlin surface: records as `data class`es and rich enums as sealed -//! classes (decoded from value buffers, with one codec function per -//! composite type in `Codecs.kt`), C-style enums as `enum class`es, unsigned -//! integers as `UByte`/`UShort`/`UInt`/`ULong`, interfaces as -//! `AutoCloseable` wrappers whose calls borrow the native reference (so -//! neither `close()` nor the phantom-reference cleaner frees it mid-call), -//! `iter` returns as `NativeIterator`, async callables as `suspend -//! fun`s (cancellation cancels the native token), callback interfaces as -//! Kotlin `interface`s (whose methods return any family and may throw the -//! module's domain exception), and one `object` per module holding its free -//! functions. A failure of a call that can't fail raises the unchecked +//! The Kotlin surface: records as `data class`es (with content equality +//! for `ByteArray` fields) and rich enums as sealed classes (decoded from +//! value buffers, with one codec function per composite type in +//! `Codecs.kt`), C-style enums as `enum class`es, unsigned integers as +//! `UByte`/`UShort`/`UInt`/`ULong`, optional scalars as nullable types and +//! typed arrays as lists (both crossing JNI without a value buffer), +//! interfaces as `AutoCloseable` wrappers whose calls borrow the native +//! reference (so neither `close()` nor the phantom-reference cleaner frees +//! it mid-call), `iter` returns as `NativeIterator`, async callables +//! as `suspend fun`s (cancellation cancels the native token), callback +//! interfaces as Kotlin `interface`s (a `fun interface` with one method), +//! error domains as exception classes with one subclass per code, and one +//! `object` per module holding its free functions as `@JvmStatic`s. A +//! failure of a call that can't fail raises the unchecked //! `NativeBugException`. `JNI_OnLoad` checks the ABI revision and every -//! top-level module's contract table before the first call. +//! top-level module's contract table before the first call; a failed load +//! is a catchable `UnsatisfiedLinkError` (see `NativeLibrary.load`). mod bridge; mod calls; +mod carrier; mod codec; mod docs; mod entities; @@ -37,11 +42,14 @@ use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use weaveffi_model::model::Model; -use crate::backend::{LanguageBackend, OutputFile}; +use crate::codegen::OutputFile; use crate::package::{Artifact, PackageContext, PackagedFile}; use crate::targets::kotlin::runtime::jni_library; +use crate::targets::{Glue, GlueKind, Target}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use miette::Result; +use crate::targets::kotlin::docs::Speller; use crate::targets::kotlin::names::{resolve_package, Names}; /// Which Gradle module the Kotlin target emits. @@ -94,7 +102,15 @@ impl KotlinConfig { } /// Kotlin backend: a Gradle module with Kotlin sources over a JNI shim. -pub struct KotlinGenerator; +pub struct KotlinGenerator { + config: KotlinConfig, +} + +impl From for KotlinGenerator { + fn from(config: KotlinConfig) -> Self { + Self { config } + } +} /// Everything one generation run renders from. struct Plan<'a> { @@ -105,7 +121,7 @@ struct Plan<'a> { } impl<'a> Plan<'a> { - fn new(model: &'a Model, out_dir: &Utf8Path, config: &'a KotlinConfig) -> Self { + fn new(model: &'a Model, config: &'a KotlinConfig) -> Self { let id = &model.identity; let package = resolve_package(config.package(), &id.prefix); let names = Names::new(model, &package, &id.library); @@ -113,7 +129,7 @@ impl<'a> Plan<'a> { model, names, config, - dir: out_dir.join("kotlin"), + dir: Utf8PathBuf::new(), } } @@ -214,9 +230,10 @@ impl<'a> Plan<'a> { deprecations, ), )); + let speller = Speller::new(model, n); for root in model.roots() { let file = format!("{}.kt", n.object(root)); - let body = entities::render_root(n, model, root); + let body = entities::render_root(n, &speller, model, root); files.push(( src.join(&file), self.kotlin_file(&file, &body, deprecations), @@ -239,15 +256,31 @@ fn uses_deprecated(model: &Model) -> bool { }) } -impl LanguageBackend for KotlinGenerator { - type Config = KotlinConfig; - +impl Target for KotlinGenerator { fn name(&self) -> &'static str { "kotlin" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { - Plan::new(model, out_dir, config) + fn glue(&self, model: &Model) -> Option { + let source = format!("{}.c", jni_library(&model.identity.library)); + Glue::from_files(GlueKind::JniShim, &self.render(model), &source, model) + } + + fn fixed_files(&self) -> &'static [&'static str] { + &[ + "Async.kt", + "Buffers.kt", + "CMakeLists.txt", + "Runtime.kt", + "build.gradle.kts", + "consumer-rules.pro", + "settings.gradle.kts", + ] + } + + fn render(&self, model: &Model) -> Vec { + let config = &self.config; + Plan::new(model, config) .files() .into_iter() .map(|(path, contents)| OutputFile::new(path, contents)) @@ -259,12 +292,8 @@ impl LanguageBackend for KotlinGenerator { /// `src/main/jniLibs//` (so Gradle skips the CMake build), and per /// desktop platform under `src/main/resources/natives//`, /// where the loader extracts them from the classpath. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let id = &model.identity; let jni = jni_library(&id.library); let android: Vec<_> = ctx @@ -301,9 +330,9 @@ impl LanguageBackend for KotlinGenerator { )); } if natives.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } - let plan = Plan::new(model, Utf8Path::new(""), config); + let plan = Plan::new(model, config); let mut files: Vec = plan .files() .into_iter() @@ -320,7 +349,7 @@ impl LanguageBackend for KotlinGenerator { package::packaged_readme(id, &plan.names.package, ctx), )); files.extend(natives); - Some(vec![Artifact::directory( + Ok(vec![Artifact::directory( format!("kotlin/{}", id.name), files, )]) diff --git a/crates/weaveffi-cli/src/targets/kotlin/names.rs b/crates/weaveffi-cli/src/targets/kotlin/names.rs index 94f58f95..c8a23a75 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/names.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/names.rs @@ -1,22 +1,24 @@ //! Naming policy: the Kotlin package, the Kotlin name of every user type, -//! error domain, and module object, the Kotlin and JNI types each IR type -//! crosses as, and JNI name mangling. +//! error domain, member, field, and module object, the public Kotlin type of +//! every IR type, and JNI name mangling. //! //! User types live at the top level of the package, so a type whose name is //! a Kotlin keyword, a type Kotlin or Java imports by default (`Unit`, //! `String`, `Result`, ...), or a name the generated runtime declares gains a //! trailing underscore (`Unit` becomes `Unit_`). A module object whose name //! would equal a type's gains a `Module` suffix (`kv.stats` beside a `Stats` -//! record becomes `Kv.StatsModule`). +//! record becomes `Kv.StatsModule`). Callables, parameters, and fields are +//! lowerCamelCased (`expires_at` becomes `expiresAt`). use std::collections::{HashMap, HashSet}; use std::fmt::Write as _; use crate::codegen::common::pascal_case; +use crate::codegen::errors; use crate::lang; -use weaveffi_model::errors; -use weaveffi_model::model::{ErrorBinding, FnBinding, Model, ModuleBinding}; -use weaveffi_model::ty::{Family, Prim, Ty}; +use weaveffi_model::model::{FnBinding, Model, ModuleBinding}; +use weaveffi_model::plan::ErrorStrategy; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty}; /// Names user types must not take: Kotlin and Java types the compiler /// imports into every file, plus the declarations of the generated runtime. @@ -52,6 +54,7 @@ pub(crate) const RESERVED_TYPE_NAMES: &[&str] = &[ "Iterable", "Iterator", "JniBridge", + "JvmField", "JvmStatic", "Lazy", "List", @@ -61,11 +64,11 @@ pub(crate) const RESERVED_TYPE_NAMES: &[&str] = &[ "MutableList", "MutableMap", "MutableSet", + "NativeBugException", "NativeCleaner", "NativeCompletion", "NativeHandle", "NativeIterator", - "NativeBugException", "NativeLibrary", "NoSuchElementException", "Nothing", @@ -84,6 +87,7 @@ pub(crate) const RESERVED_TYPE_NAMES: &[&str] = &[ "System", "Thread", "Throwable", + "Throws", "Triple", "UByte", "UInt", @@ -118,6 +122,18 @@ const RESERVED_MEMBER_NAMES: &[&str] = &[ "wait", ]; +/// Properties every exception inherits from `Throwable` (plus the `code` of +/// `FfiException`); an error payload field spelled the same gains a trailing +/// underscore. +const THROWABLE_MEMBERS: &[&str] = &[ + "cause", + "code", + "localizedMessage", + "message", + "stackTrace", + "suppressed", +]; + /// Escape a user identifier for a Kotlin declaration or expression position: /// a reserved word gains a trailing underscore. pub(crate) fn kt_escape(name: &str) -> String { @@ -134,20 +150,44 @@ pub(crate) fn lower_camel(s: &str) -> String { } } -/// The Kotlin spelling of a parameter: lowerCamelCased, then escaped. It -/// never starts with `_`, which keeps every generated local (all spelled -/// `_name`) out of its way. +/// The Kotlin spelling of a parameter or field: lowerCamelCased, then +/// escaped. It never starts with `_`, which keeps every generated local (all +/// spelled `_name`) out of its way. pub(crate) fn kt_param(name: &str) -> String { kt_escape(&lower_camel(name)) } -/// The Kotlin spelling of a method, static, or callback method: like -/// [`kt_param`], and also escaped away from the members every wrapper -/// declares. +/// The Kotlin spelling of a callable or callback method: like [`kt_param`], +/// and also escaped away from the members every wrapper declares. pub(crate) fn kt_member(name: &str) -> String { lang::escape_member(&kt_param(name), RESERVED_MEMBER_NAMES) } +/// The Kotlin spellings of one declaration's fields (or parameters), in +/// order: each [`kt_param`], with a later field whose camelCase spelling an +/// earlier one already took (`foo_bar` beside `fooBar`) gaining trailing +/// underscores until it's unique. `error_payload` also escapes the +/// properties every exception inherits. +pub(crate) fn kt_fields<'a>( + names: impl IntoIterator, + error_payload: bool, +) -> Vec { + let mut taken = HashSet::new(); + names + .into_iter() + .map(|raw| { + let mut name = kt_param(raw); + if error_payload { + name = lang::escape_member(&name, THROWABLE_MEMBERS); + } + while !taken.insert(name.clone()) { + name.push('_'); + } + name + }) + .collect() +} + /// Escape a user type name away from keywords and reserved type names. fn kt_type_ident(name: &str) -> String { if lang::is_reserved(name, lang::KOTLIN_KEYWORDS) || RESERVED_TYPE_NAMES.contains(&name) { @@ -175,6 +215,13 @@ pub(crate) fn jni_mangle(ident: &str) -> String { out } +/// The `JniBridge.error` domain of a call that can't fail: its failure +/// raises `NativeBugException`. +pub(crate) const TRAP_DOMAIN: u32 = 0; +/// The `JniBridge.error` domain of a `throws: any` call: its failure raises +/// `FfiException` with the code and message. +pub(crate) const UNTYPED_DOMAIN: u32 = 1; + /// Every name the generated Kotlin and JNI code needs, resolved once from /// the identity, the target configuration, and the model. pub(crate) struct Names { @@ -188,8 +235,9 @@ pub(crate) struct Names { /// from (`kvstore`). pub library: String, types: HashMap, - exceptions: HashMap, - domains: HashMap, + /// Every error domain's exception class and `JniBridge.error` index, by + /// the domain's (global) name, in declaration order. + domains: Vec<(String, String)>, objects: HashMap, /// Every interface's `_destroy` symbol, by interface name. destroys: HashMap, @@ -200,8 +248,6 @@ impl Names { /// (already validated) and `library` the producer library name. pub(crate) fn new(model: &Model, package: &str, library: &str) -> Self { let mut types = HashMap::new(); - let mut exceptions = HashMap::new(); - let mut domains = HashMap::new(); let mut destroys = HashMap::new(); for m in &model.modules { for i in &m.interfaces { @@ -217,16 +263,15 @@ impl Names { for name in declared { types.insert(name.clone(), kt_type_ident(name)); } - if let Some(eb) = m.errors.as_ref() { - exceptions.insert( - eb.c_tag.clone(), - kt_type_ident(&errors::exception_type_name(&eb.name)), - ); - let next = u32::try_from(domains.len() + 1).unwrap_or(u32::MAX); - domains.insert(eb.c_tag.clone(), next); - } } - let taken: HashSet<&String> = types.values().chain(exceptions.values()).collect(); + let domains: Vec<(String, String)> = errors::tables(model, "Exception") + .into_iter() + .map(|t| (t.domain.name.clone(), kt_type_ident(&t.type_name))) + .collect(); + let taken: HashSet<&String> = types + .values() + .chain(domains.iter().map(|(_, exc)| exc)) + .collect(); let objects = model .modules .iter() @@ -244,7 +289,6 @@ impl Names { prefix: model.prefix().to_string(), library: library.to_string(), types, - exceptions, domains, objects, destroys, @@ -270,34 +314,54 @@ impl Names { .unwrap_or_else(|| kt_type_ident(name)) } - /// The Kotlin exception class of an error domain. - pub(crate) fn exception(&self, eb: &ErrorBinding) -> String { - self.exceptions - .get(&eb.c_tag) - .cloned() - .unwrap_or_else(|| kt_type_ident(&errors::exception_type_name(&eb.name))) + /// The Kotlin exception class of the error domain `domain` (its raw + /// name): the shared exception spelling (`KvError` is `KvException`, + /// `KitchenErrors` is `KitchenException`), escaped like a type. + /// + /// # Panics + /// + /// Panics when no domain has that name, which validation rules out. + pub(crate) fn exception(&self, domain: &str) -> &str { + self.domains + .iter() + .find(|(name, _)| name == domain) + .map(|(_, exc)| exc.as_str()) + .unwrap_or_else(|| panic!("error domain '{domain}' is not declared")) } - /// The domain index `JniBridge.error` maps a failure of `f` through: - /// the module's domain for a throwing callable, else 0 (the trap, - /// `NativeBugException`). - pub(crate) fn domain(&self, f: &FnBinding, error: Option<&ErrorBinding>) -> u32 { + /// The `JniBridge.error` domain a failure of a callable with `error` + /// maps through: [`TRAP_DOMAIN`], [`UNTYPED_DOMAIN`], or the domain's + /// index (from 2, in declaration order). + pub(crate) fn domain_index(&self, error: &ErrorStrategy) -> u32 { match error { - Some(eb) if f.throws => self.domains.get(&eb.c_tag).copied().unwrap_or(0), - _ => 0, + ErrorStrategy::Trap => TRAP_DOMAIN, + ErrorStrategy::Untyped => UNTYPED_DOMAIN, + ErrorStrategy::Domain(name) => { + let i = self + .domains + .iter() + .position(|(d, _)| d == name) + .unwrap_or_else(|| panic!("error domain '{name}' is not declared")); + u32::try_from(i).map_or(u32::MAX, |i| i + 2) + } } } - /// Every declared domain with its index, in index order. - pub(crate) fn domains<'a>(&self, model: &'a Model) -> Vec<(u32, &'a ErrorBinding)> { - let mut out: Vec<(u32, &ErrorBinding)> = model - .modules - .iter() - .filter_map(|m| m.errors.as_ref()) - .filter_map(|eb| self.domains.get(&eb.c_tag).map(|i| (*i, eb))) - .collect(); - out.sort_by_key(|(i, _)| *i); - out + /// Every declared domain's `JniBridge.error` index and exception class, + /// in index order. + pub(crate) fn domains(&self) -> impl Iterator { + (2u32..).zip(self.domains.iter().map(|(_, exc)| exc.as_str())) + } + + /// The exception class a callable with `error` raises, for its + /// `@Throws` annotation: the domain's, `FfiException` for `throws: any`, + /// or `None` for a call that can't fail. + pub(crate) fn thrown(&self, error: &ErrorStrategy) -> Option<&str> { + match error { + ErrorStrategy::Trap => None, + ErrorStrategy::Untyped => Some("FfiException"), + ErrorStrategy::Domain(name) => Some(self.exception(name)), + } } /// The simple Kotlin name of a module's object. @@ -333,7 +397,7 @@ impl Names { format!("{}_JniBridge", pkg.join("_")) } - /// The public Kotlin type of an IR type. + /// The public Kotlin type of an IR value type. pub(crate) fn kt_type(&self, t: &Ty) -> String { match t { Ty::Prim(Prim::I8) => "Byte".into(), @@ -349,101 +413,34 @@ impl Names { Ty::Prim(Prim::Bool) => "Boolean".into(), Ty::Prim(Prim::String) => "String".into(), Ty::Prim(Prim::Bytes) => "ByteArray".into(), - Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Enum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(n) => self.ty(n), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => self.ty(n), Ty::Optional(inner) => format!("{}?", self.kt_type(inner)), Ty::List(inner) => format!("List<{}>", self.kt_type(inner)), Ty::Map(k, v) => format!("Map<{}, {}>", self.kt_type(k), self.kt_type(v)), - Ty::Iterator(inner) => format!("NativeIterator<{}>", self.kt_type(inner)), } } - /// The Kotlin type a value crosses the JNI boundary as: unsigned - /// integers in the signed type of the same width (an `external fun` - /// can't take Kotlin's unsigned value classes), enums as `Int`, strings, - /// bytes, and value buffers as `ByteArray`, objects and iterators as - /// their address (`0L` is none), and a callback interface as the - /// implementing object. - pub(crate) fn jni_type(&self, t: &Ty) -> String { - match t.family() { - Family::Direct => jni_kind(t).into(), - Family::String | Family::Bytes | Family::Buffer => "ByteArray".into(), - Family::Object { .. } | Family::Iterator => "Long".into(), - Family::Callback { nullable } => { - let name = t - .callback_interface_name() - .expect("callback families name a callback interface"); - format!("{}{}", self.ty(name), if nullable { "?" } else { "" }) + /// The public Kotlin type of a parameter: its value type, or the + /// callback interface (nullable when optional). + pub(crate) fn kt_param_type(&self, t: &ParamTy) -> String { + match t { + ParamTy::Value(t) => self.kt_type(t), + ParamTy::Callback { name, nullable } => { + format!("{}{}", self.ty(name), if *nullable { "?" } else { "" }) } } } - /// The JVM type descriptor of [`Names::jni_type`], for the method IDs - /// the shim caches; `None` is `void`. - pub(crate) fn jni_descriptor(&self, t: Option<&Ty>) -> String { - let Some(t) = t else { - return "V".into(); - }; - match t.family() { - Family::Direct => match jni_kind(t) { - "Boolean" => "Z", - "Byte" => "B", - "Short" => "S", - "Int" => "I", - "Float" => "F", - "Double" => "D", - _ => "J", - } - .into(), - Family::String | Family::Bytes | Family::Buffer => "[B".into(), - Family::Object { .. } | Family::Iterator => "J".into(), - Family::Callback { .. } => { - let name = t - .callback_interface_name() - .expect("callback families name a callback interface"); - format!("L{}/{};", self.package_path, self.ty(name)) - } + /// The public Kotlin type of a return: its value type, or a + /// `NativeIterator` of the element type. + pub(crate) fn kt_ret_type(&self, t: &RetTy) -> String { + match t { + RetTy::Value(t) => self.kt_type(t), + RetTy::Iterator(elem) => format!("NativeIterator<{}>", self.kt_type(elem)), } } } -/// The JNI C type of [`Names::jni_type`]. -pub(crate) fn jni_c_type(t: &Ty) -> &'static str { - match t.family() { - Family::Direct => match jni_kind(t) { - "Boolean" => "jboolean", - "Byte" => "jbyte", - "Short" => "jshort", - "Int" => "jint", - "Float" => "jfloat", - "Double" => "jdouble", - _ => "jlong", - }, - Family::String | Family::Bytes | Family::Buffer => "jbyteArray", - Family::Object { .. } | Family::Iterator => "jlong", - Family::Callback { .. } => "jobject", - } -} - -/// The JNI carrier of a direct-family value, which is also the -/// `Call{Kind}Method` and `on{Kind}` stem: `Boolean`, `Byte`, `Short`, -/// `Int`, `Long`, `Float`, or `Double`. Unsigned integers ride in the signed -/// type of the same width; objects ride in `Long`. -pub(crate) fn jni_kind(t: &Ty) -> &'static str { - match t { - Ty::Prim(Prim::Bool) => "Boolean", - Ty::Prim(Prim::I8 | Prim::U8) => "Byte", - Ty::Prim(Prim::I16 | Prim::U16) => "Short", - Ty::Prim(Prim::I32 | Prim::U32) | Ty::Enum(_) => "Int", - Ty::Prim(Prim::F32) => "Float", - Ty::Prim(Prim::F64) => "Double", - _ => "Long", - } -} - /// For an unsigned integer type, the Kotlin conversions from its JNI carrier /// (`toUInt`) and back (`toInt`), both of which reinterpret the bits; `None` /// for every other type. @@ -482,4 +479,16 @@ mod tests { assert_eq!(kt_member("dispose"), "dispose"); assert_eq!(kt_member("object"), "object_"); } + + #[test] + fn fields_are_camel_cased_and_unique() { + assert_eq!( + kt_fields(["expires_at", "expiresAt", "in", "_hidden"], false), + ["expiresAt", "expiresAt_", "in_", "hidden"] + ); + assert_eq!( + kt_fields(["message", "code", "key"], true), + ["message_", "code_", "key"] + ); + } } diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime.rs b/crates/weaveffi-cli/src/targets/kotlin/runtime.rs index d310e9df..9fa65a65 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime.rs @@ -1,13 +1,12 @@ //! The fixed runtime sources under `runtime/`, spliced into the output with //! `{{PLACEHOLDER}}` substitution: the Kotlin runtime (`FfiException`, //! `NativeBugException`, `NativeIterator`, the loader, `NativeHandle`, the -//! phantom-reference cleaner), the value-buffer codec, the coroutine bridge, -//! and the JNI shim's helpers (the contract checker, thread attach and +//! phantom-reference cleaner, typed-array conversions), the value-buffer +//! codec and content equality, the coroutine bridge, and the JNI shim's +//! helpers (array copies, boxes, the contract checker, thread attach and //! detach, async completions, and callback dispatch). -use crate::cabi::ABI_VERSION; -use weaveffi_model::model::Model; -use weaveffi_model::ty::Family; +use weaveffi_model::model::{Model, ABI_VERSION}; use crate::targets::kotlin::names::Names; @@ -15,10 +14,10 @@ const RUNTIME_KT: &str = include_str!("runtime/Runtime.kt"); const BUFFERS_KT: &str = include_str!("runtime/Buffers.kt"); const ASYNC_KT: &str = include_str!("runtime/Async.kt"); const JNI_CORE_C: &str = include_str!("runtime/jni_core.c"); +const JNI_BOXES_C: &str = include_str!("runtime/jni_boxes.c"); const JNI_THREADS_C: &str = include_str!("runtime/jni_threads.c"); const JNI_ASYNC_C: &str = include_str!("runtime/jni_async.c"); const JNI_CALLBACKS_C: &str = include_str!("runtime/jni_callbacks.c"); -const JNI_CALLBACK_RUNS_C: &str = include_str!("runtime/jni_callback_runs.c"); /// Replace every `{{KEY}}` in `template` with its value. fn fill(template: &str, vars: &[(&str, &str)]) -> String { @@ -61,8 +60,9 @@ pub(crate) fn async_kt(n: &Names) -> String { fill(ASYNC_KT, &[("PACKAGE", &n.package)]) } -/// The fixed head of the JNI shim: the core helpers and `JNI_OnLoad`, plus -/// the thread, async, and callback support the model needs. +/// The fixed head of the JNI shim: the core helpers, the boxes, and +/// `JNI_OnLoad`, plus the thread, async, and callback support the model +/// needs. pub(crate) fn jni_runtime(n: &Names, model: &Model, header: &str, name: &str) -> String { let abi = ABI_VERSION.to_string(); let jni_class = n.jni_class(); @@ -78,6 +78,7 @@ pub(crate) fn jni_runtime(n: &Names, model: &Model, header: &str, name: &str) -> ("LIBRARY", &n.library), ]; let mut out = fill(JNI_CORE_C, &vars); + out.push_str(&fill(JNI_BOXES_C, &vars)); let has_async = model.has_async(); let has_callbacks = model.has_callback_interfaces(); if has_async || has_callbacks { @@ -89,15 +90,5 @@ pub(crate) fn jni_runtime(n: &Names, model: &Model, header: &str, name: &str) -> if has_callbacks { out.push_str(&fill(JNI_CALLBACKS_C, &vars)); } - let returns_runs = model.callback_interfaces().any(|(_, cb)| { - cb.methods.iter().any(|m| { - m.ret.as_ref().is_some_and(|t| { - matches!(t.family(), Family::String | Family::Bytes | Family::Buffer) - }) - }) - }); - if returns_runs { - out.push_str(&fill(JNI_CALLBACK_RUNS_C, &vars)); - } out } diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/Async.kt b/crates/weaveffi-cli/src/targets/kotlin/runtime/Async.kt index a7106072..bb04d1a2 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/Async.kt +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/Async.kt @@ -9,7 +9,8 @@ import kotlinx.coroutines.suspendCancellableCoroutine /** * Bridges one native async call to a suspended coroutine. The JNI shim pins * this object with a global reference and calls exactly one `on*` method, - * from whichever producer thread completes the call. + * from whichever producer thread completes the call. [convert] lifts the + * boxed JNI result into the public value. */ internal class NativeCompletion( private val cont: CancellableContinuation, @@ -52,43 +53,33 @@ internal class NativeCompletion( token = 0L } - fun onUnit() = succeed(Unit) - - fun onBoolean(value: Boolean) = succeed(value) - - fun onByte(value: Byte) = succeed(value) - - fun onShort(value: Short) = succeed(value) - - fun onInt(value: Int) = succeed(value) - - fun onLong(value: Long) = succeed(value) - - fun onFloat(value: Float) = succeed(value) - - fun onDouble(value: Double) = succeed(value) - - fun onBytes(value: ByteArray) = succeed(value) + /** The call succeeded with [raw]: the result in its JNI form, boxed. */ + fun onValue(raw: Any?) { + releaseToken() + val result = try { + convert(raw) + } catch (t: Throwable) { + cont.resumeWithException(t) + return + } + cont.resume(result) + } - fun onError(code: Int, message: ByteArray, payload: ByteArray?) { + /** The call failed with a native error (code -5 is a cancellation). */ + fun onError(code: Int, message: ByteArray?, payload: ByteArray?) { releaseToken() val error = if (code == -5) { - CancellationException(decodeUtf8(message)) + CancellationException(message?.let(::decodeUtf8) ?: "cancelled") } else { - JniBridge.error(domain, code, message, payload) + JniBridge.error(domain, code, message ?: ByteArray(0), payload) } cont.resumeWithException(error) } - private fun succeed(raw: Any?) { + /** The JVM couldn't take the result (out of memory, typically). */ + fun onFailure(error: Throwable) { releaseToken() - val result = try { - convert(raw) - } catch (t: Throwable) { - cont.resumeWithException(t) - return - } - cont.resume(result) + cont.resumeWithException(error) } } diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/Buffers.kt b/crates/weaveffi-cli/src/targets/kotlin/runtime/Buffers.kt index ccf40a9d..6e6e9d07 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/Buffers.kt +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/Buffers.kt @@ -244,3 +244,33 @@ internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T r.expectEnd() return v } + +/** + * Structural equality that compares `ByteArray`s by content, also inside + * lists, maps, and optionals: what a record field holding bytes compares + * by (a `ByteArray`'s own `equals` is identity). + */ +internal fun deepEquals(a: Any?, b: Any?): Boolean = when { + a === b -> true + a is ByteArray && b is ByteArray -> a.contentEquals(b) + a is List<*> && b is List<*> -> a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + a is Map<*, *> && b is Map<*, *> -> + a.size == b.size && a.all { (k, v) -> b.containsKey(k) && deepEquals(v, b[k]) } + else -> a == b +} + +/** The hash matching [deepEquals]. */ +internal fun deepHashCode(a: Any?): Int = when (a) { + is ByteArray -> a.contentHashCode() + is List<*> -> a.fold(1) { h, e -> 31 * h + deepHashCode(e) } + is Map<*, *> -> a.entries.sumOf { (k, v) -> k.hashCode() xor deepHashCode(v) } + else -> a.hashCode() +} + +/** The `toString` matching [deepEquals]: byte arrays print their content. */ +internal fun deepToString(a: Any?): String = when (a) { + is ByteArray -> a.contentToString() + is List<*> -> a.joinToString(", ", "[", "]") { deepToString(it) } + is Map<*, *> -> a.entries.joinToString(", ", "{", "}") { (k, v) -> "$k=${deepToString(v)}" } + else -> a.toString() +} diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/Runtime.kt b/crates/weaveffi-cli/src/targets/kotlin/runtime/Runtime.kt index 22758b45..73483a4e 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/Runtime.kt +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/Runtime.kt @@ -34,52 +34,88 @@ class NativeBugException(val code: Int, message: String) : * A lazy sequence streamed from the native library, one element per step. * The native iterator is released when the sequence is exhausted, when * [close] is called, or once this object becomes unreachable, whichever - * comes first. + * comes first. Not thread-safe: iterate it from one thread at a time. */ class NativeIterator internal constructor( address: Long, release: (Long) -> Unit, - private val step: (Long) -> Any?, + private val next: (Long, Array) -> Boolean, + private val convert: (Any?) -> T, ) : Iterator, AutoCloseable { private val handle = NativeCleaner.register(this, NativeHandle(address, release)) - private var lookahead: Any? = EMPTY + private val slot = arrayOfNulls(1) + private var ready = false + private var lookahead: T? = null override fun hasNext(): Boolean { - if (lookahead !== EMPTY) return true + if (ready) return true if (handle.isClosed) return false - val item = handle.borrow(step) - if (item === DONE) { + if (!handle.borrow { next(it, slot) }) { close() return false } - lookahead = item + val raw = slot[0] + slot[0] = null + lookahead = convert(raw) + ready = true return true } override fun next(): T { if (!hasNext()) throw NoSuchElementException() - val item = lookahead - lookahead = EMPTY @Suppress("UNCHECKED_CAST") - return item as T + val item = lookahead as T + lookahead = null + ready = false + return item } /** Releases the native iterator; safe to call more than once. */ override fun close() = handle.close() - - internal companion object { - /** What a step function returns once the native sequence is exhausted. */ - val DONE = Any() - private val EMPTY = Any() - } } -/** Loads the producer library and then the JNI shim, exactly once. */ -internal object NativeLibrary { +/** + * Loads the native library and its JNI shim, exactly once, and checks that + * the library implements the C ABI revision and every declaration these + * bindings were generated against. + * + * The first use of the bindings loads them implicitly. Call [load] first to + * handle a missing or mismatched library gracefully: it throws the same + * [UnsatisfiedLinkError] on every call after a failure, while a binding + * used after a failed load throws [NoClassDefFoundError] (both are + * [LinkageError]s). + * + * The library loads from the path in the `{{LIBRARY_ENV}}` environment + * variable when it's set, else from the `natives/-/` classpath + * resources of a packaged desktop build, else from `java.library.path`. + */ +object NativeLibrary { private const val LIBRARY = "{{LIBRARY}}" private const val JNI_LIBRARY = "{{JNI_LIBRARY}}" - init { + private val failure: Throwable? = try { + loadLibraries() + null + } catch (t: Throwable) { + t + } + + /** + * Loads and checks the native library if that hasn't happened yet. + * + * @throws UnsatisfiedLinkError when the library or its JNI shim can't be + * loaded, implements a different C ABI revision, or lacks or changed a + * declaration these bindings use; the message says which. + */ + @JvmStatic + fun load() { + val cause = failure ?: return + val error = UnsatisfiedLinkError(cause.message ?: cause.toString()) + error.initCause(cause) + throw error + } + + private fun loadLibraries() { val explicit = System.getenv("{{LIBRARY_ENV}}") if (!explicit.isNullOrEmpty()) { System.load(explicit) @@ -95,8 +131,6 @@ internal object NativeLibrary { if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) } - fun ensureLoaded() {} - /** * Loads `base` from the `natives/-/` classpath resources a * packaged desktop build bundles; false when it isn't bundled. @@ -218,3 +252,36 @@ internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.U /** The string a native UTF-8 byte sequence spells. */ internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) + +/** The bits of each element, for a `[u16]` crossing as a `ShortArray`. */ +internal fun Collection.toShortArrayBits(): ShortArray { + val out = ShortArray(size) + var i = 0 + for (v in this) out[i++] = v.toShort() + return out +} + +/** The bits of each element, for a `[u32]` crossing as an `IntArray`. */ +internal fun Collection.toIntArrayBits(): IntArray { + val out = IntArray(size) + var i = 0 + for (v in this) out[i++] = v.toInt() + return out +} + +/** The bits of each element, for a `[u64]` crossing as a `LongArray`. */ +internal fun Collection.toLongArrayBits(): LongArray { + val out = LongArray(size) + var i = 0 + for (v in this) out[i++] = v.toLong() + return out +} + +/** A `[u16]` that crossed as a `ShortArray`. */ +internal fun ShortArray.toUShortList(): List = List(size) { this[it].toUShort() } + +/** A `[u32]` that crossed as an `IntArray`. */ +internal fun IntArray.toUIntList(): List = List(size) { this[it].toUInt() } + +/** A `[u64]` that crossed as a `LongArray`. */ +internal fun LongArray.toULongList(): List = List(size) { this[it].toULong() } diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_async.c b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_async.c index 2fc4e1cc..0ef004cd 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_async.c +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_async.c @@ -1,40 +1,40 @@ -/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ -static jmethodID Jni_on_unit = NULL; -static jmethodID Jni_on_boolean = NULL; -static jmethodID Jni_on_byte = NULL; -static jmethodID Jni_on_short = NULL; -static jmethodID Jni_on_int = NULL; -static jmethodID Jni_on_long = NULL; -static jmethodID Jni_on_float = NULL; -static jmethodID Jni_on_double = NULL; -static jmethodID Jni_on_bytes = NULL; +/* Async completions deliver to the pinned Kotlin `NativeCompletion`: the + result as one object (boxed, an array, or NULL), a native failure, or a + JVM failure making the result. */ +static jmethodID Jni_on_value = NULL; static jmethodID Jni_on_error = NULL; +static jmethodID Jni_on_failure = NULL; static jint Jni_load_async(JNIEnv* env) { jclass cls = (*env)->FindClass(env, "{{PACKAGE_PATH}}/NativeCompletion"); if (cls == NULL) { return JNI_ERR; } - Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); - Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); - Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); - Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); - Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); - Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); - Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); - Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); - Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); + Jni_on_value = (*env)->GetMethodID(env, cls, "onValue", "(Ljava/lang/Object;)V"); Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); - if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { + Jni_on_failure = (*env)->GetMethodID(env, cls, "onFailure", "(Ljava/lang/Throwable;)V"); + if (Jni_on_value == NULL || Jni_on_error == NULL || Jni_on_failure == NULL) { return JNI_ERR; } return JNI_OK; } +/* Reports anything the Kotlin side threw (it has no caller to propagate + to), then releases the frame, `context`, and the thread. */ +static void Jni_complete_end(JNIEnv* env, void* context, int detach) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + Jni_unpin(env, context); + Jni_env_done(detach); +} + /* Starts a completion on the producer thread: returns the JNIEnv with a local frame pushed, or NULL when the call failed (the failure, or the - loss of the JVM, has then been handled and `context` released). */ + loss of the JVM, has then been delivered and `context` released). */ static JNIEnv* Jni_complete_begin(void* context, {{PREFIX}}_error* err, int* detach) { JNIEnv* env = Jni_env(detach); if (env == NULL) { @@ -44,41 +44,42 @@ static JNIEnv* Jni_complete_begin(void* context, {{PREFIX}}_error* err, int* det if ((*env)->PushLocalFrame(env, 8) != 0) { (*env)->ExceptionClear(env); {{PREFIX}}_error_free(err); + Jni_unpin(env, context); Jni_env_done(*detach); return NULL; } if (err == NULL) { return env; } - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; + jarray text = Jni_new_array(env, Jni_B, err->message_ptr, err->message_len); + jarray payload = NULL; if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + payload = Jni_new_array(env, Jni_B, err->payload_ptr, err->payload_len); } jint code = (jint)err->code; {{PREFIX}}_error_free(err); - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); + text = NULL; + payload = NULL; } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(*detach); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); + Jni_complete_end(env, context, *detach); return NULL; } -/* Finishes a successful completion: reports anything the Kotlin side threw - (it has no caller to propagate to) and releases `context`. */ -static void Jni_complete_end(JNIEnv* env, void* context, int detach) { - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); +/* Delivers a successful result (`value`, made after Jni_complete_begin) + and finishes the completion. A JVM failure making `value` is delivered + instead, so the caller always resumes. */ +static void Jni_complete(JNIEnv* env, void* context, int detach, jobject value) { + jthrowable failure = (*env)->ExceptionOccurred(env); + if (failure != NULL) { (*env)->ExceptionClear(env); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_failure, failure); + } else { + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_value, value); } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(detach); + Jni_complete_end(env, context, detach); } JNIEXPORT jlong JNICALL Java_{{JNI_CLASS}}_cancel_1token_1create(JNIEnv* env, jclass cls) { diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_boxes.c b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_boxes.c new file mode 100644 index 00000000..15ae1e13 --- /dev/null +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_boxes.c @@ -0,0 +1,72 @@ + +/* Boxes: an optional scalar crossing out of native code is a nullable + `java.lang.Integer` (and the like), and an async result or iterator item + reaches Kotlin as an `Any?`, so scalars box through `valueOf` (which + caches small values) and unbox through `intValue` (and the like). */ +static jclass Jni_box_class[7]; +static jmethodID Jni_box_of[7]; +static jmethodID Jni_unbox_of[7]; + +static jint Jni_load_boxes(JNIEnv* env) { + static const char* const classes[7] = { + "java/lang/Boolean", "java/lang/Byte", "java/lang/Short", "java/lang/Integer", + "java/lang/Long", "java/lang/Float", "java/lang/Double", + }; + static const char* const value_of[7] = { + "(Z)Ljava/lang/Boolean;", "(B)Ljava/lang/Byte;", "(S)Ljava/lang/Short;", "(I)Ljava/lang/Integer;", + "(J)Ljava/lang/Long;", "(F)Ljava/lang/Float;", "(D)Ljava/lang/Double;", + }; + static const char* const unbox[7] = { + "booleanValue", "byteValue", "shortValue", "intValue", "longValue", "floatValue", "doubleValue", + }; + static const char* const unbox_sig[7] = {"()Z", "()B", "()S", "()I", "()J", "()F", "()D"}; + for (int i = 0; i < 7; i++) { + jclass cls = (*env)->FindClass(env, classes[i]); + if (cls == NULL) { + return JNI_ERR; + } + Jni_box_class[i] = (jclass)Jni_pin(env, cls); + if (Jni_box_class[i] == NULL) { + return JNI_ERR; + } + Jni_box_of[i] = (*env)->GetStaticMethodID(env, cls, "valueOf", value_of[i]); + Jni_unbox_of[i] = (*env)->GetMethodID(env, cls, unbox[i], unbox_sig[i]); + if (Jni_box_of[i] == NULL || Jni_unbox_of[i] == NULL) { + return JNI_ERR; + } + } + return JNI_OK; +} + +/* The box of `value` (NULL with an exception pending if the JVM can't make + one). */ +Jni_helper jobject Jni_box(JNIEnv* env, Jni_kind kind, jvalue value) { + return (*env)->CallStaticObjectMethodA(env, Jni_box_class[kind], Jni_box_of[kind], &value); +} + +/* The scalar in the box `obj`. */ +Jni_helper jvalue Jni_unbox(JNIEnv* env, Jni_kind kind, jobject obj) { + jvalue v; + memset(&v, 0, sizeof v); + switch (kind) { + case Jni_Z: v.z = (*env)->CallBooleanMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_B: v.b = (*env)->CallByteMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_S: v.s = (*env)->CallShortMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_I: v.i = (*env)->CallIntMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_J: v.j = (*env)->CallLongMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_F: v.f = (*env)->CallFloatMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_D: v.d = (*env)->CallDoubleMethod(env, obj, Jni_unbox_of[kind]); break; + } + return v; +} + +/* Hands one iterator item (an object, or NULL for an absent optional) to + Kotlin through the one-element array `out`; JNI_FALSE when making the item + failed (the exception stays pending). */ +Jni_helper jboolean Jni_yield(JNIEnv* env, jobjectArray out, jobject item) { + if ((*env)->ExceptionCheck(env)) { + return JNI_FALSE; + } + (*env)->SetObjectArrayElement(env, out, 0, item); + return JNI_TRUE; +} diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callback_runs.c b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callback_runs.c deleted file mode 100644 index dd6e5662..00000000 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callback_runs.c +++ /dev/null @@ -1,20 +0,0 @@ - -/* Hands a string, bytes, or buffer return to the producer: a run allocated - with {{PREFIX}}_alloc in the out slots, which the producer adopts. A NULL - `value` (the shim reported a failure) leaves the slots empty. */ -static void Jni_callback_return_bytes(JNIEnv* env, jbyteArray value, uint8_t** out_ptr, size_t* out_len, {{PREFIX}}_error* out_err) { - if (value == NULL || (*env)->ExceptionCheck(env)) { - return; - } - jsize len = (*env)->GetArrayLength(env, value); - uint8_t* run = {{PREFIX}}_alloc((size_t)len); - if (len > 0 && run == NULL) { - {{PREFIX}}_error_set(out_err, -4, "out of memory"); - return; - } - if (len > 0) { - (*env)->GetByteArrayRegion(env, value, 0, len, (jbyte*)run); - } - *out_ptr = run; - *out_len = (size_t)len; -} diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callbacks.c b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callbacks.c index fd6bcd5b..500719ce 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callbacks.c +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_callbacks.c @@ -3,19 +3,20 @@ implementation as `ctx`; trampolines call the matching static dispatch shim on JniBridge from whatever thread the producer uses. The shim never lets an exception escape: it reports a failure itself through `out_err` - (with the two exports below) and returns a zero value. */ + (with the two exports below) and returns a zero value. Every vtable's + `flags` is 0: the JVM can run a method on any thread. */ /* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after reporting the failure through `out_err`. */ static JNIEnv* Jni_callback_begin({{PREFIX}}_error* out_err, int* detach) { JNIEnv* env = Jni_env(detach); if (env == NULL) { - {{PREFIX}}_error_set(out_err, -4, "could not attach the calling thread to the JVM"); + Jni_set_error(out_err, -4, "could not attach the calling thread to the JVM"); return NULL; } if ((*env)->PushLocalFrame(env, 16) != 0) { (*env)->ExceptionClear(env); - {{PREFIX}}_error_set(out_err, -4, "JNI local frame exhausted"); + Jni_set_error(out_err, -4, "JNI local frame exhausted"); Jni_env_done(*detach); return NULL; } @@ -28,7 +29,7 @@ static JNIEnv* Jni_callback_begin({{PREFIX}}_error* out_err, int* detach) { static void Jni_callback_end(JNIEnv* env, {{PREFIX}}_error* out_err, int detach) { if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); - {{PREFIX}}_error_set(out_err, -4, "the JVM failed to dispatch the callback"); + Jni_set_error(out_err, -4, "the JVM failed to dispatch the callback"); } (*env)->PopLocalFrame(env, NULL); Jni_env_done(detach); @@ -40,35 +41,67 @@ static void Jni_release_callback(void* ctx) { int detach = 0; JNIEnv* env = Jni_env(&detach); if (env != NULL) { - if (ctx != NULL) { - (*env)->DeleteGlobalRef(env, (jobject)ctx); - } + Jni_unpin(env, ctx); Jni_env_done(detach); } } +/* Hands a run-shaped callback return (a string, bytes, a value buffer, or a + typed array, as a Java array of `kind`) to the producer: a copy in a + {{PREFIX}}_alloc run, which the producer adopts, with its element count in + `*out_len`. A NULL `value` (the shim reported a failure) is no run. */ +Jni_helper void* Jni_callback_run(JNIEnv* env, Jni_kind kind, jarray value, size_t* out_len, {{PREFIX}}_error* out_err) { + *out_len = 0; + if (value == NULL || (*env)->ExceptionCheck(env)) { + return NULL; + } + jsize len = (*env)->GetArrayLength(env, value); + if (len <= 0) { + return NULL; + } + uint8_t* run = {{PREFIX}}_alloc((size_t)len * Jni_kind_size[kind]); + if (run == NULL) { + Jni_set_error(out_err, -4, "out of memory"); + return NULL; + } + Jni_get_region(env, value, kind, len, run); + *out_len = (size_t)len; + return run; +} + +/* Reads an optional scalar callback return (a box, or NULL for none) into + `*out`; whether it was present. */ +Jni_helper bool Jni_callback_opt(JNIEnv* env, Jni_kind kind, jobject value, jvalue* out) { + if (value == NULL || (*env)->ExceptionCheck(env)) { + return false; + } + *out = Jni_unbox(env, kind, value); + return !(*env)->ExceptionCheck(env); +} + /* `JniBridge.error_set`: reports a failed callback through the trampoline's - `out_err` (the message is cut at its first NUL). */ + `out_err`, with the message as UTF-8. */ JNIEXPORT void JNICALL Java_{{JNI_CLASS}}_error_1set(JNIEnv* env, jclass cls, jlong err, jint code, jbyteArray message) { + Jni_run m; (void)cls; - Jni_bytes m = Jni_borrow_bytes(env, message); - char* text = (char*)malloc(m.len + 1u); - if (text != NULL) { - if (m.len > 0) { - memcpy(text, m.ptr, m.len); - } - text[m.len] = '\0'; + if (!Jni_borrow(env, message, Jni_B, &m)) { + (*env)->ExceptionClear(env); + Jni_set_error(({{PREFIX}}_error*)(intptr_t)err, (int32_t)code, "callback failed"); + return; } - Jni_release_bytes(env, &m); - {{PREFIX}}_error_set(({{PREFIX}}_error*)(intptr_t)err, (int32_t)code, text != NULL ? text : "callback failed"); - free(text); + {{PREFIX}}_error_set(({{PREFIX}}_error*)(intptr_t)err, (int32_t)code, (const uint8_t*)m.ptr, m.len); + Jni_unborrow(&m); } /* `JniBridge.error_set_payload`: attaches a typed error's fields (a value buffer) to the failure `error_set` reported. */ JNIEXPORT void JNICALL Java_{{JNI_CLASS}}_error_1set_1payload(JNIEnv* env, jclass cls, jlong err, jbyteArray payload) { + Jni_run p; (void)cls; - Jni_bytes p = Jni_borrow_bytes(env, payload); - {{PREFIX}}_error_set_payload(({{PREFIX}}_error*)(intptr_t)err, p.ptr, p.len); - Jni_release_bytes(env, &p); + if (!Jni_borrow(env, payload, Jni_B, &p)) { + (*env)->ExceptionClear(env); + return; + } + {{PREFIX}}_error_set_payload(({{PREFIX}}_error*)(intptr_t)err, (const uint8_t*)p.ptr, p.len); + Jni_unborrow(&p); } diff --git a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_core.c b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_core.c index 0dc28f50..3bd9176e 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_core.c +++ b/crates/weaveffi-cli/src/targets/kotlin/runtime/jni_core.c @@ -14,67 +14,166 @@ (all lowercase `{{PREFIX}}_...`) and no JNI export (`Java_...`) can spell. */ +/* Helpers a given shim may not use (it only uses what its API needs). */ +#if defined(__GNUC__) +#define Jni_helper static inline __attribute__((unused)) +#else +#define Jni_helper static inline +#endif + static JavaVM* Jni_vm = NULL; static jclass Jni_bridge = NULL; static jmethodID Jni_error = NULL; -/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value - buffers all cross as one. */ -typedef struct { - jbyteArray array; - jbyte* elems; - const uint8_t* ptr; - size_t len; -} Jni_bytes; - -static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { - Jni_bytes b = {array, NULL, NULL, 0}; - if (array == NULL) { - return b; - } - b.len = (size_t)(*env)->GetArrayLength(env, array); - b.elems = (*env)->GetByteArrayElements(env, array, NULL); - b.ptr = (const uint8_t*)b.elems; - if (b.elems == NULL) { - b.len = 0; +/* The JNI primitive kinds: a direct value's carrier, a typed array's + element, and a box's payload. A string, bytes, or a value buffer crosses + as a byte array (`Jni_B`). */ +typedef enum { Jni_Z, Jni_B, Jni_S, Jni_I, Jni_J, Jni_F, Jni_D } Jni_kind; + +static const size_t Jni_kind_size[] = {1, 1, 2, 4, 8, 4, 8}; + +Jni_helper void Jni_throw_oom(JNIEnv* env, const char* what) { + jclass cls = (*env)->FindClass(env, "java/lang/OutOfMemoryError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, what); } - return b; } -static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { - if (b->elems != NULL) { - (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); +/* Copies all `n` elements of a Java primitive array into `dst`. */ +Jni_helper void Jni_get_region(JNIEnv* env, jarray array, Jni_kind kind, jsize n, void* dst) { + switch (kind) { + case Jni_Z: (*env)->GetBooleanArrayRegion(env, (jbooleanArray)array, 0, n, (jboolean*)dst); break; + case Jni_B: (*env)->GetByteArrayRegion(env, (jbyteArray)array, 0, n, (jbyte*)dst); break; + case Jni_S: (*env)->GetShortArrayRegion(env, (jshortArray)array, 0, n, (jshort*)dst); break; + case Jni_I: (*env)->GetIntArrayRegion(env, (jintArray)array, 0, n, (jint*)dst); break; + case Jni_J: (*env)->GetLongArrayRegion(env, (jlongArray)array, 0, n, (jlong*)dst); break; + case Jni_F: (*env)->GetFloatArrayRegion(env, (jfloatArray)array, 0, n, (jfloat*)dst); break; + case Jni_D: (*env)->GetDoubleArrayRegion(env, (jdoubleArray)array, 0, n, (jdouble*)dst); break; } } -/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid - input, so NULL + 0 is the empty array). */ -static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = (*env)->NewByteArray(env, (jsize)len); - if (out != NULL && len > 0 && ptr != NULL) { - (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); +/* A new Java primitive array holding a copy of the `count` elements at + `ptr` (never read when `count` is 0); NULL with an exception pending when + the JVM can't make it. */ +Jni_helper jarray Jni_new_array(JNIEnv* env, Jni_kind kind, const void* ptr, size_t count) { + if (count > (size_t)INT32_MAX) { + Jni_throw_oom(env, "native array is too large for a JVM array"); + return NULL; + } + jsize n = (jsize)count; + jarray out = NULL; + switch (kind) { + case Jni_Z: out = (*env)->NewBooleanArray(env, n); break; + case Jni_B: out = (*env)->NewByteArray(env, n); break; + case Jni_S: out = (*env)->NewShortArray(env, n); break; + case Jni_I: out = (*env)->NewIntArray(env, n); break; + case Jni_J: out = (*env)->NewLongArray(env, n); break; + case Jni_F: out = (*env)->NewFloatArray(env, n); break; + case Jni_D: out = (*env)->NewDoubleArray(env, n); break; + } + if (out == NULL || n == 0 || ptr == NULL) { + return out; + } + switch (kind) { + case Jni_Z: (*env)->SetBooleanArrayRegion(env, (jbooleanArray)out, 0, n, (const jboolean*)ptr); break; + case Jni_B: (*env)->SetByteArrayRegion(env, (jbyteArray)out, 0, n, (const jbyte*)ptr); break; + case Jni_S: (*env)->SetShortArrayRegion(env, (jshortArray)out, 0, n, (const jshort*)ptr); break; + case Jni_I: (*env)->SetIntArrayRegion(env, (jintArray)out, 0, n, (const jint*)ptr); break; + case Jni_J: (*env)->SetLongArrayRegion(env, (jlongArray)out, 0, n, (const jlong*)ptr); break; + case Jni_F: (*env)->SetFloatArrayRegion(env, (jfloatArray)out, 0, n, (const jfloat*)ptr); break; + case Jni_D: (*env)->SetDoubleArrayRegion(env, (jdoubleArray)out, 0, n, (const jdouble*)ptr); break; } return out; } -/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ -static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = Jni_new_bytes(env, ptr, len); +/* Jni_new_array over a run the producer handed over (a string, bytes, a + value buffer, or a typed array of `count` elements), which it then frees + whether or not the copy succeeded. */ +Jni_helper jarray Jni_take_array(JNIEnv* env, Jni_kind kind, const void* ptr, size_t count) { + jarray out = Jni_new_array(env, kind, ptr, count); if (ptr != NULL) { - {{PREFIX}}_free_bytes((uint8_t*)ptr, len); + {{PREFIX}}_free_bytes((uint8_t*)(uintptr_t)ptr, count * Jni_kind_size[kind]); } return out; } -/* Throws the Kotlin exception for `err` (mapped through error domain - `domain`, or 0 for a call that can't fail, which raises the trap) and - clears `err`. */ -static inline void Jni_throw(JNIEnv* env, {{PREFIX}}_error* err, jint domain) { - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; +/* A Java primitive array's elements copied out for one call (strings, + bytes, and value buffers as `ByteArray`s, typed arrays as `IntArray`s and + the like): one copy, into inline storage when it fits (8-aligned, as the + producer requires of typed arrays) and the heap otherwise. */ +typedef struct { + const void* ptr; /* NULL when empty */ + size_t len; /* element count */ + void* heap; + uint64_t inline_buf[32]; +} Jni_run; + +/* Fills `run` from `array` (NULL is empty); 0, with OutOfMemoryError + pending, when the heap copy can't be allocated. */ +Jni_helper int Jni_borrow(JNIEnv* env, jarray array, Jni_kind kind, Jni_run* run) { + run->ptr = NULL; + run->len = 0; + run->heap = NULL; + if (array == NULL) { + return 1; + } + jsize n = (*env)->GetArrayLength(env, array); + if (n <= 0) { + return 1; + } + size_t bytes = (size_t)n * Jni_kind_size[kind]; + void* dst = run->inline_buf; + if (bytes > sizeof run->inline_buf) { + run->heap = malloc(bytes); + if (run->heap == NULL) { + Jni_throw_oom(env, "could not copy an array argument"); + return 0; + } + dst = run->heap; + } + Jni_get_region(env, array, kind, n, dst); + run->ptr = dst; + run->len = (size_t)n; + return 1; +} + +Jni_helper void Jni_unborrow(Jni_run* run) { + free(run->heap); + run->heap = NULL; +} + +/* A global reference the producer holds on to (a callback implementation + or an async completion): NULL for NULL, and NULL with OutOfMemoryError + pending when the JVM can't make one. */ +Jni_helper void* Jni_pin(JNIEnv* env, jobject obj) { + if (obj == NULL) { + return NULL; + } + jobject ref = (*env)->NewGlobalRef(env, obj); + if (ref == NULL && !(*env)->ExceptionCheck(env)) { + Jni_throw_oom(env, "could not create a JNI global reference"); + } + return (void*)ref; +} + +Jni_helper void Jni_unpin(JNIEnv* env, void* ref) { + if (ref != NULL) { + (*env)->DeleteGlobalRef(env, (jobject)ref); + } +} + +/* Sets a failure with a C string message on `err`. */ +Jni_helper void Jni_set_error({{PREFIX}}_error* err, int32_t code, const char* message) { + {{PREFIX}}_error_set(err, code, (const uint8_t*)message, strlen(message)); +} + +/* Throws the Kotlin exception for `err` (mapped through `JniBridge.error` + domain `domain`) and clears `err`. */ +Jni_helper void Jni_throw(JNIEnv* env, {{PREFIX}}_error* err, jint domain) { + jarray text = Jni_new_array(env, Jni_B, err->message_ptr, err->message_len); + jarray payload = NULL; if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + payload = Jni_new_array(env, Jni_B, err->payload_ptr, err->payload_len); } jint code = (jint)err->code; {{PREFIX}}_error_clear(err); @@ -87,7 +186,7 @@ static inline void Jni_throw(JNIEnv* env, {{PREFIX}}_error* err, jint domain) { } } -static jint Jni_load_error(JNIEnv* env, const char* message) { +Jni_helper jint Jni_load_error(JNIEnv* env, const char* message) { jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); if (cls != NULL) { (*env)->ThrowNew(env, cls, message); @@ -107,7 +206,7 @@ typedef struct { has every expected entry with an equal hash; entries only the library has are fine. Fails loading with an error naming the first declaration that's missing or changed. */ -static inline jint Jni_check_contract(JNIEnv* env, const {{PREFIX}}_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { +Jni_helper jint Jni_check_contract(JNIEnv* env, const {{PREFIX}}_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { size_t len = 0; const {{PREFIX}}_contract_entry* table = contract(&len); if (table == NULL) { @@ -139,7 +238,8 @@ static inline jint Jni_check_contract(JNIEnv* env, const {{PREFIX}}_contract_ent return JNI_OK; } -/* Generated below: the contract checks and the method IDs the shim caches. */ +/* Generated below: the contract checks and the classes and method IDs the + shim caches. */ static jint Jni_load(JNIEnv* env); JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { @@ -156,7 +256,10 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { if (bridge == NULL) { return JNI_ERR; } - Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_bridge = (jclass)Jni_pin(env, bridge); + if (Jni_bridge == NULL) { + return JNI_ERR; + } Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ljava/lang/Throwable;"); if (Jni_error == NULL) { return JNI_ERR; diff --git a/crates/weaveffi-cli/src/targets/kotlin/tests.rs b/crates/weaveffi-cli/src/targets/kotlin/tests.rs index cddea207..42abde31 100644 --- a/crates/weaveffi-cli/src/targets/kotlin/tests.rs +++ b/crates/weaveffi-cli/src/targets/kotlin/tests.rs @@ -1,21 +1,19 @@ -//! Unit tests rendering a small IR through the generator: identity-driven -//! names and layout, configuration overrides, name escaping, the ABI 4 JNI -//! surface (ptr+len strings, the load-time contract check, cancellation), -//! unsigned types, the trap, composite codecs, object lifetime, callbacks -//! (every return family, `throws`, optional callbacks), thread detaching, -//! iterators, and packaging. +//! Unit tests of what's specific to the Kotlin target: identity-driven names +//! and layout, configuration overrides (package, flavor, SDK levels), name +//! escaping and module objects, JNI name mangling, and packaging. The +//! `kitchen_sink` snapshot pins the rest of the generated surface. -use camino::{Utf8Path, Utf8PathBuf}; +use camino::Utf8PathBuf; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; -use crate::backend::LanguageBackend; use crate::package::{FileContent, PackageContext}; use crate::platform::{BinarySet, NativeBinary, Platform}; use crate::targets::kotlin::{KotlinConfig, KotlinFlavor, KotlinGenerator}; +use crate::targets::Target; const FIXTURE: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: bus enums: @@ -46,21 +44,21 @@ modules: params: - { name: key, type: string } return: bytes - throws: true + throws: BusError - name: weight params: - { name: count, type: u32 } return: u64 errors: - name: BusError - codes: - - { name: Closed, code: 1, message: "Bus is closed" } - - name: Missing - code: 2 - message: "missing" - fields: - - { name: key, type: string } - - { name: tries, type: u16 } + - name: BusError + codes: + - { name: Closed, code: 1, message: "Bus is closed" } + - name: Missing + code: 2 + message: "missing" + fields: + - { name: key, type: string } + - { name: tries, type: u16 } interfaces: - name: Store constructors: @@ -72,7 +70,7 @@ modules: params: - { name: key, type: string } return: "string?" - throws: true + throws: BusError - name: larger params: - { name: other, type: "Store?" } @@ -97,7 +95,7 @@ modules: return: i64 async: true cancellable: true - throws: true + throws: BusError functions: - name: get params: @@ -140,8 +138,8 @@ fn api() -> Model { /// Render with `config` and return `(path, contents)` pairs, paths with `/`. fn render(config: &KotlinConfig) -> Vec<(String, String)> { let model = api(); - KotlinGenerator - .files(&model, Utf8Path::new("out"), config) + KotlinGenerator::from(config.clone()) + .render(&model) .into_iter() .map(|f| (f.path.as_str().replace('\\', "/"), f.contents)) .collect() @@ -165,19 +163,19 @@ fn layout_follows_the_identity() { assert_eq!( paths, [ - "out/kotlin/settings.gradle.kts", - "out/kotlin/build.gradle.kts", - "out/kotlin/consumer-rules.pro", - "out/kotlin/src/main/cpp/CMakeLists.txt", - "out/kotlin/src/main/cpp/event_bus.h", - "out/kotlin/src/main/cpp/event_bus_jni.c", - "out/kotlin/src/main/kotlin/event_bus/Runtime.kt", - "out/kotlin/src/main/kotlin/event_bus/Buffers.kt", - "out/kotlin/src/main/kotlin/event_bus/Async.kt", - "out/kotlin/src/main/kotlin/event_bus/Codecs.kt", - "out/kotlin/src/main/kotlin/event_bus/JniBridge.kt", - "out/kotlin/src/main/kotlin/event_bus/Bus.kt", - "out/kotlin/src/main/kotlin/event_bus/Units.kt", + "settings.gradle.kts", + "build.gradle.kts", + "consumer-rules.pro", + "src/main/cpp/CMakeLists.txt", + "src/main/cpp/event_bus.h", + "src/main/cpp/event_bus_jni.c", + "src/main/kotlin/event_bus/Runtime.kt", + "src/main/kotlin/event_bus/Buffers.kt", + "src/main/kotlin/event_bus/Async.kt", + "src/main/kotlin/event_bus/Codecs.kt", + "src/main/kotlin/event_bus/JniBridge.kt", + "src/main/kotlin/event_bus/Bus.kt", + "src/main/kotlin/event_bus/Units.kt", ] ); let runtime = file(&files, "Runtime.kt"); @@ -221,7 +219,7 @@ fn configuration_overrides_package_and_flavor() { .all(|(p, _)| !p.ends_with("consumer-rules.pro"))); assert!(files .iter() - .any(|(p, _)| p == "out/kotlin/src/main/kotlin/com/example/bus/JniBridge.kt")); + .any(|(p, _)| p == "src/main/kotlin/com/example/bus/JniBridge.kt")); let gradle = file(&files, "build.gradle.kts"); assert!(gradle.contains("kotlin(\"jvm\")")); assert!(gradle.contains("group = \"com.example.bus\"")); @@ -253,262 +251,6 @@ fn modules_are_objects_and_shadowing_names_are_escaped() { assert!(bridge.contains("@JvmStatic external fun bus_stats_summarize(store: Long): ByteArray")); } -#[test] -fn strings_cross_as_bytes_with_lengths() { - let files = render(&KotlinConfig::default()); - let bus = file(&files, "Bus.kt"); - assert!(bus.contains( - "fun get(key: String): String? = handle.borrow { _self ->\n decodeBuffer(JniBridge.bus_Store_get(_self, encodeUtf8(key))) { _r -> unpackOptionalOfString(_r) }" - )); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains("Jni_bytes p_key_b = Jni_borrow_bytes(env, p_key);")); - assert!(c.contains( - "event_bus_bus_Store_get((const event_bus_bus_Store*)(intptr_t)self, p_key_b.ptr, p_key_b.len, &out_len, &err);" - )); - assert!(c.contains("return Jni_take_bytes(env, rv, out_len);")); - assert!(c.contains("event_bus_free_bytes((uint8_t*)ptr, len);")); - assert!(!c.contains("free_string")); - assert!(!c.contains("GetStringUTFChars")); -} - -#[test] -fn load_checks_the_abi_revision_and_every_root_contract() { - let model = api(); - let files = render(&KotlinConfig::default()); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains("if (event_bus_abi_version() != 4u) {")); - assert!(!c.contains("checksum")); - for root in model.roots() { - let entries = weaveffi_model::contract::entries(&model, root); - let var = format!("Jni_contract_{}", root.name); - assert!(c.contains(&format!("static const Jni_contract_entry {var}[] = {{"))); - for e in &entries { - assert!(c.contains(&format!( - "{{UINT64_C(0x{:016x}), UINT64_C(0x{:016x}), \"{}\"}},", - e.id, e.hash, e.path - ))); - } - assert!(c.contains(&format!( - "if (Jni_check_contract(env, event_bus_{}_contract, {var}, sizeof {var} / sizeof {var}[0]) != JNI_OK) {{", - root.name - ))); - } - assert!(c.contains("problem = \"is missing from the library\";")); - assert!(c.contains("problem = \"changed since these bindings were generated\";")); -} - -#[test] -fn unsigned_integers_are_kotlin_unsigned_types() { - let files = render(&KotlinConfig::default()); - let bus = file(&files, "Bus.kt"); - // Public signatures use the unsigned types; the JNI natives carry the - // signed type of the same width, converted bit for bit. - assert!(bus.contains( - "fun scale(by: UByte, totals: Map): List = decodeBuffer(JniBridge.bus_scale(by.toByte(), encodeBuffer { _w -> packMapOfU64ToU16(_w, totals) })) { _r -> unpackListOfU32(_r) }" - )); - assert!(bus.contains( - "class Missing(val key: String, val tries: UShort, message: String = \"missing\")" - )); - assert!(bus.contains("fun weight(count: UInt): ULong")); - assert!(bus.contains( - "if (JniBridge.bus_Store_SizesIterator_next(_it, _slot)) _slot[0].toUInt() else NativeIterator.DONE" - )); - let bridge = file(&files, "JniBridge.kt"); - assert!(bridge - .contains("@JvmStatic external fun bus_scale(by: Byte, totals: ByteArray): ByteArray")); - assert!(bridge.contains("_impl.weight(count.toUInt()).toLong()")); - let buffers = file(&files, "Buffers.kt"); - assert!(buffers.contains("fun writeU32(v: UInt) = writeI32(v.toInt())")); - assert!(buffers.contains("fun readU64(): ULong = readI64().toULong()")); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains("JNICALL Java_event_1bus_JniBridge_bus_1scale(JNIEnv* env, jclass cls, jbyte p_by, jbyteArray p_totals)")); - assert!(c.contains("event_bus_bus_scale((uint8_t)p_by, ")); - assert!(c.contains("static uint64_t Jni_bus_Subscriber_weight(void* ctx, uint32_t p_count, event_bus_error* out_err)")); -} - -#[test] -fn composites_have_one_codec_each() { - let files = render(&KotlinConfig::default()); - let codecs = file(&files, "Codecs.kt"); - assert!(codecs.contains( - "internal fun packMapOfU64ToU16(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeU64(it) }, { _w.writeU16(it) })" - )); - assert!(codecs.contains( - "internal fun unpackOptionalOfString(_r: BufferReader): String? = _r.readOptional { _r.readString() }" - )); - assert_eq!(codecs.matches("internal fun packListOfU32(").count(), 1); - // Call sites call the codec instead of inlining a loop. - for (path, contents) in files.iter().filter(|(p, _)| p.ends_with(".kt")) { - if !path.ends_with("Codecs.kt") && !path.ends_with("Buffers.kt") { - assert!( - !contents.contains("writeList(") && !contents.contains("readOptional"), - "{path}" - ); - } - } -} - -#[test] -fn a_failed_call_that_cannot_fail_traps() { - let files = render(&KotlinConfig::default()); - let runtime = file(&files, "Runtime.kt"); - assert!(runtime.contains( - "class NativeBugException(val code: Int, message: String) :\n IllegalStateException(\"native call failed with code $code: $message\")" - )); - let bridge = file(&files, "JniBridge.kt"); - assert!(bridge.contains("1 -> BusException.fromCode(code, text, payload)")); - assert!(bridge.contains("else -> NativeBugException(code, text)")); - let c = file(&files, "event_bus_jni.c"); - // `get` throws (domain 1); `larger` can't fail, so its failure traps. - assert!(c.contains("\"(II[B[B)Ljava/lang/Throwable;\"")); - let larger = &c[c.find("bus_1Store_1larger(").unwrap()..]; - assert!(larger.contains("Jni_throw(env, &err, 0);")); - let get = &c[c.find("bus_1Store_1get(").unwrap()..]; - assert!(get.contains("Jni_throw(env, &err, 1);")); -} - -#[test] -fn objects_are_borrowed_for_every_call() { - let files = render(&KotlinConfig::default()); - let bus = file(&files, "Bus.kt"); - assert!(bus.contains("class Store private constructor(address: Long) : AutoCloseable {")); - assert!(bus.contains( - "internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::bus_Store_destroy))" - )); - assert!(bus.contains( - "fun larger(other: Store?): Store? = handle.borrow { _self ->\n other?.handle.borrowOrNull { _h0 ->\n Store.fromHandleOrNull(JniBridge.bus_Store_larger(_self, _h0))" - )); - // Objects inside buffers are cloned on the way in and released again if - // the encoding fails. - assert!(bus.contains("_w.writeObject(_v.source.cloneHandle(), JniBridge::bus_Store_destroy)")); - let runtime = file(&files, "Runtime.kt"); - assert!(runtime.contains("PhantomReference")); - assert!(!runtime.contains("java.lang.ref.Cleaner")); -} - -#[test] -fn cancellable_async_wires_the_token() { - let files = render(&KotlinConfig::default()); - let bus = file(&files, "Bus.kt"); - assert!(bus.contains( - "suspend fun drain(): Long = awaitNative(true, 1, { _raw -> _raw as Long }) { _token, _done ->" - )); - assert!(bus.contains("JniBridge.bus_Store_drain(_self, _token, _done)")); - let runtime = file(&files, "Async.kt"); - assert!(runtime.contains("cont.invokeOnCancellation { completion.cancel() }")); - assert!(runtime.contains("if (code == -5)")); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains( - "(event_bus_cancel_token*)(intptr_t)cancel_token, Jni_done_bus_Store_drain, context);" - )); - assert!(c.contains("CallVoidMethod(env, (jobject)context, Jni_on_long, (jlong)result);")); - assert!(c.contains("Jni_complete_end(env, context, detach);")); -} - -#[test] -fn callbacks_dispatch_through_cached_static_shims() { - let files = render(&KotlinConfig::default()); - let bridge = file(&files, "JniBridge.kt"); - assert!(bridge.contains( - "fun bus_Subscriber_on_event(_impl: Subscriber, _err: Long, event: ByteArray, note: ByteArray): Int = try {\n _impl.onEvent(decodeBuffer(event) { _r -> unpackEvent(_r) }, decodeUtf8(note)).value\n } catch (_e: Throwable) {\n fail(_err, _e, null)\n 0\n }" - )); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains( - "GetStaticMethodID(env, Jni_bridge, \"bus_Subscriber_on_event\", \"(Levent_bus/Subscriber;J[B[B)I\");" - )); - assert!(c.contains("AttachCurrentThreadAsDaemon")); - assert!(c.contains( - "static const event_bus_bus_Subscriber_vtable Jni_bus_Subscriber_vtable = {sizeof(event_bus_bus_Subscriber_vtable), 0, Jni_release_callback, Jni_bus_Subscriber_on_event, Jni_bus_Subscriber_label, Jni_bus_Subscriber_pick, Jni_bus_Subscriber_fetch, Jni_bus_Subscriber_weight};" - )); - // The shim reports failures itself through these two exports. - assert!(c.contains("Java_event_1bus_JniBridge_error_1set(JNIEnv* env, jclass cls, jlong err, jint code, jbyteArray message)")); - assert!( - c.contains("event_bus_error_set_payload((event_bus_error*)(intptr_t)err, p.ptr, p.len);") - ); - assert!(!c.contains("_dealloc")); -} - -#[test] -fn callback_methods_return_every_family() { - let files = render(&KotlinConfig::default()); - let bridge = file(&files, "JniBridge.kt"); - // A string return is encoded; the trampoline copies it into an alloc'd run. - assert!(bridge.contains( - "fun bus_Subscriber_label(_impl: Subscriber, _err: Long): ByteArray? = try {\n encodeUtf8(_impl.label())" - )); - // An object parameter is adopted before anything can fail; an optional - // object return is a fresh reference the producer adopts. - assert!(bridge.contains( - "fun bus_Subscriber_pick(_impl: Subscriber, _err: Long, home: Long): Long {\n val _home = Store.fromHandle(home)\n return try {\n _impl.pick(_home)?.cloneHandle() ?: 0L" - )); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains( - "static void Jni_bus_Subscriber_label(void* ctx, uint8_t** out_ptr, size_t* out_len, event_bus_error* out_err) {" - )); - assert!(c.contains("Jni_callback_return_bytes(env, rv, out_ptr, out_len, out_err);")); - assert!(c.contains("uint8_t* run = event_bus_alloc((size_t)len);")); - assert!(c.contains("return (event_bus_bus_Store*)(intptr_t)rv;")); - // A call that can't reach the JVM still releases the adopted object. - assert!(c.contains( - " event_bus_bus_Store_destroy(p_home);\n return (event_bus_bus_Store*)0;" - )); -} - -#[test] -fn throwing_callbacks_report_their_domain_error() { - let files = render(&KotlinConfig::default()); - let bridge = file(&files, "JniBridge.kt"); - assert!(bridge.contains("fail(_err, _e, _e as? BusException)")); - assert!(bridge.contains("error_set(err, typed.code, encodeUtf8(typed.message ?: \"\"))")); - assert!(bridge.contains("typed.encodePayload()?.let { error_set_payload(err, it) }")); - assert!(bridge.contains("error_set(err, -4, encodeUtf8(error.message ?: error.toString()))")); - let bus = file(&files, "Bus.kt"); - assert!(bus.contains( - "override fun encodePayload(): ByteArray = encodeBuffer { _w ->\n _w.writeString(key)\n _w.writeU16(tries)\n }" - )); - assert!(bus.contains("Throw a [BusException] to report a typed error to the library.")); -} - -#[test] -fn optional_callbacks_pass_a_null_vtable() { - let files = render(&KotlinConfig::default()); - let bus = file(&files, "Bus.kt"); - assert!(bus.contains("fun watch(sub: Subscriber?) {")); - assert!(bus.contains("JniBridge.bus_Store_watch(_self, sub)")); - let bridge = file(&files, "JniBridge.kt"); - assert!( - bridge.contains("@JvmStatic external fun bus_Store_watch(_self: Long, sub: Subscriber?)") - ); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains( - "p_sub != NULL ? (void*)(*env)->NewGlobalRef(env, p_sub) : NULL, p_sub != NULL ? &Jni_bus_Subscriber_vtable : NULL, &err);" - )); -} - -#[test] -fn producer_threads_detach_on_every_platform() { - let files = render(&KotlinConfig::default()); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains("pthread_key_create(&Jni_env_key, Jni_thread_exit) == 0;")); - assert!(c.contains("Jni_env_slot = FlsAlloc(Jni_thread_exit);")); - assert!(c.contains("*detach = !Jni_remember_attach();")); - assert!(c.contains("Jni_env_done(detach);")); -} - -#[test] -fn iterators_stream_through_native_iterator() { - let files = render(&KotlinConfig::default()); - let bus = file(&files, "Bus.kt"); - assert!(bus.contains( - "fun keys(): NativeIterator = handle.borrow { _self ->\n NativeIterator(JniBridge.bus_Store_keys(_self), JniBridge::bus_Store_KeysIterator_destroy) { _it ->" - )); - assert!(bus.contains( - "if (JniBridge.bus_Store_CountsIterator_next(_it, _slot)) _slot[0] else NativeIterator.DONE" - )); - let c = file(&files, "event_bus_jni.c"); - assert!(c.contains("(*env)->SetIntArrayRegion(env, out, 0, 1, &value);")); -} - #[test] fn package_bundles_prebuilt_binaries() { let model = api(); @@ -534,8 +276,8 @@ fn package_bundles_prebuilt_binaries() { "/prebuilt/wasm32/event_bus.wasm", )); let ctx = PackageContext::new(&binaries); - let artifacts = KotlinGenerator - .package(&model, &ctx, &KotlinConfig::default()) + let artifacts = KotlinGenerator::from(KotlinConfig::default()) + .package(&model, &ctx) .expect("kotlin supports packaging"); assert_eq!(artifacts.len(), 1); assert_eq!(artifacts[0].path, "kotlin/event-bus"); @@ -572,12 +314,8 @@ fn package_bundles_prebuilt_binaries() { "/prebuilt/android-x64/libevent_bus.so", )); partial.insert(binaries.get(Platform::AndroidArm64).unwrap().clone()); - let artifacts = KotlinGenerator - .package( - &model, - &PackageContext::new(&partial), - &KotlinConfig::default(), - ) + let artifacts = KotlinGenerator::from(KotlinConfig::default()) + .package(&model, &PackageContext::new(&partial)) .unwrap(); assert!(!artifacts[0] .files diff --git a/crates/weaveffi-cli/src/targets/mod.rs b/crates/weaveffi-cli/src/targets/mod.rs index 9a93101c..cb20b1a8 100644 --- a/crates/weaveffi-cli/src/targets/mod.rs +++ b/crates/weaveffi-cli/src/targets/mod.rs @@ -1,68 +1,337 @@ -//! The language generators, one module per target. Each implements -//! [`LanguageBackend`](crate::backend::LanguageBackend) and exposes its -//! generator and configuration types (for example -//! [`swift::SwiftGenerator`] and [`swift::SwiftConfig`]). - -pub mod c; -pub mod cpp; -pub mod dart; -pub mod dotnet; -pub mod go; +//! The language targets: the [`Target`] trait every generator implements and +//! the [`REGISTRY`] of every target the CLI drives. +//! +//! A target owns its configuration (its `[generators.]` table in +//! `weaveffi.toml`), renders its files from the validated [`Model`], and +//! declares the few things the commands need to know about it through +//! hooks: what `weaveffi package` builds from the per-platform libraries +//! ([`package`](Target::package)), which C glue `weaveffi build` prebuilds +//! ([`glue`](Target::glue)), and where `weaveffi dev` puts the library +//! ([`dev_bundle_dir`](Target::dev_bundle_dir), +//! [`linkage`](Target::linkage)). Nothing outside a target matches on its +//! name. + +pub(crate) mod c; +pub(crate) mod cpp; +pub(crate) mod dart; +pub(crate) mod dotnet; +pub(crate) mod go; pub(crate) mod js; -pub mod kotlin; -pub mod node; -pub mod python; -pub mod ruby; -pub mod swift; -pub mod wasm; +pub(crate) mod kotlin; +pub(crate) mod node; +pub(crate) mod python; +pub(crate) mod ruby; +pub(crate) mod swift; +pub(crate) mod wasm; + +use camino::{Utf8Path, Utf8PathBuf}; +use miette::{miette, Result}; +use serde::de::DeserializeOwned; +use weaveffi_model::model::Model; + +use crate::codegen::OutputFile; +use crate::package::{Artifact, PackageContext}; + +/// One language target: its name, its rendering, and the hooks the +/// `build`, `package`, and `dev` commands call. +/// +/// Rendering is pure: [`render`](Self::render) returns files in memory with +/// paths relative to the target's own output directory (`{out}/{name}/`), +/// and the [`Orchestrator`](crate::codegen::Orchestrator) does the I/O. +pub trait Target: Send + Sync { + /// The stable short name (`"swift"`, `"python"`, ...): the `--target` + /// token, the `[generators.]` table, and the output subdirectory. + fn name(&self) -> &'static str; + + /// Render every file this target produces for `model`, with paths + /// relative to the target's output directory. + fn render(&self, model: &Model) -> Vec; + + /// The installable artifacts that bundle the per-platform builds in + /// `ctx`, with paths relative to the dist directory. + /// + /// A target skips platforms its ecosystem has no slot for and returns + /// an empty list when none of the builds fits. A target that needs an + /// external tool which isn't installed records the artifact it skips + /// with [`PackageContext::skip`] instead of failing. The default + /// packages nothing. + /// + /// # Errors + /// + /// Returns an error when a library can't be read or an external tool + /// that is installed fails. + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let _ = (model, ctx); + Ok(Vec::new()) + } + + /// Finish packaging after [`package`](Self::package)'s artifacts are + /// written to `dist` (for example, `dotnet pack` over the written + /// project), returning any further files written, relative to `dist`. + /// The default does nothing. + /// + /// # Errors + /// + /// Returns an error when an installed external tool fails. + fn finish_package( + &self, + dist: &Utf8Path, + artifacts: &[Artifact], + ctx: &PackageContext<'_>, + ) -> Result> { + let _ = (dist, artifacts, ctx); + Ok(Vec::new()) + } + + /// The C glue library this target's bindings load next to the + /// producer's, which `weaveffi build` prebuilds per platform. The + /// default is none. + fn glue(&self, model: &Model) -> Option { + let _ = model; + None + } + + /// Where `weaveffi dev` copies the producer's library so the generated + /// package finds it without setup, relative to the target's output + /// directory, or `None` when the package doesn't bundle one. + fn dev_bundle_dir(&self, model: &Model) -> Option { + let _ = model; + None + } -use crate::codegen::{ConfiguredBackend, Target}; + /// How the bindings find the producer's library during development. + /// The default is [`Linkage::Runtime`]. + fn linkage(&self) -> Linkage { + Linkage::Runtime + } -/// Every target with its default configuration, in canonical order (the -/// order of the `--target` tokens). + /// The files (by file name, or by suffix for a pattern starting with + /// `*`) whose contents don't depend on the API beyond its names: fixed + /// runtimes, package manifests, and READMEs. + fn fixed_files(&self) -> &'static [&'static str] { + &[] + } +} + +/// How a target's bindings find the producer's library. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Linkage { + /// The bindings load the library at run time, from a bundled copy or + /// the path in `{LIBRARY}_LIBRARY`. + Runtime, + /// The bindings link the library when the consumer builds them. + Link, + /// The bindings load a `wasm32` module, which `weaveffi build + /// --platforms wasm32` produces. + Wasm, +} + +/// The kind of C glue library a target prebuilds. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GlueKind { + /// A Node.js N-API addon, built for desktop platforms. + NodeAddon, + /// A JNI shim, built for Android and desktop platforms. + JniShim, +} + +/// A C glue library a target's bindings load next to the producer's. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Glue { + /// What the library is. + pub kind: GlueKind, + /// The glue's C source, as the target renders it. + pub source: OutputFile, + /// The producer's C header the source includes. + pub header: OutputFile, +} + +impl Glue { + /// The glue of `kind` from a target's rendered `files`: the file named + /// `source` and the producer's header, `{library}.h`. + pub(crate) fn from_files( + kind: GlueKind, + files: &[OutputFile], + source: &str, + model: &Model, + ) -> Option { + let header = c::header_name(model); + let find = |name: &str| { + files + .iter() + .find(|f| f.path.file_name() == Some(name)) + .cloned() + }; + Some(Self { + kind, + source: find(source)?, + header: find(&header)?, + }) + } +} + +/// A registered target: its name, help text, and how to build it from its +/// `[generators.]` table. +#[derive(Clone, Copy)] +pub struct TargetDesc { + /// The target's name (equal to [`Target::name`]). + pub name: &'static str, + /// A one-line description for `--help`. + pub description: &'static str, + /// Whether `generate` renders the target when neither `--target` nor + /// `[project] targets` names any. + pub default: bool, + build: fn(toml::Table) -> Result, toml::de::Error>, +} + +impl std::fmt::Debug for TargetDesc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TargetDesc") + .field("name", &self.name) + .field("default", &self.default) + .finish_non_exhaustive() + } +} + +impl TargetDesc { + /// Build the target from its `[generators.]` table. + /// + /// # Errors + /// + /// Returns an error naming the table when it has an unknown key or a + /// value of the wrong type. + pub fn build(&self, table: toml::Table) -> Result> { + (self.build)(table).map_err(|e| miette!("[generators.{}]: {e}", self.name)) + } + + /// Build the target with its default configuration. + /// + /// # Panics + /// + /// Panics if the target's configuration has no default, which every + /// registered target's has. + #[must_use] + pub fn build_default(&self) -> Box { + (self.build)(toml::Table::new()).expect("every target's configuration has a default") + } +} + +/// Deserialize a target's config `C` from its table and build the target. +fn configured(table: toml::Table) -> Result, toml::de::Error> +where + C: DeserializeOwned, + T: Target + From + 'static, +{ + let config: C = toml::Value::Table(table).try_into()?; + Ok(Box::new(T::from(config))) +} + +/// Every target the CLI drives, in canonical order (the order `generate` +/// renders them and `--help` lists them). +pub static REGISTRY: &[TargetDesc] = &[ + TargetDesc { + name: "c", + description: "C header, plus value-buffer helpers", + default: true, + build: configured::, + }, + TargetDesc { + name: "cpp", + description: "Header-only C++ wrapper with a CMake package", + default: true, + build: configured::, + }, + TargetDesc { + name: "swift", + description: "SwiftPM package", + default: true, + build: configured::, + }, + TargetDesc { + name: "node", + description: "Node.js N-API package", + default: true, + build: configured::, + }, + TargetDesc { + name: "wasm", + description: "WebAssembly npm package", + default: true, + build: configured::, + }, + TargetDesc { + name: "dotnet", + description: ".NET (C#) project over P/Invoke", + default: true, + build: configured::, + }, + TargetDesc { + name: "dart", + description: "Dart package over dart:ffi", + default: true, + build: configured::, + }, + TargetDesc { + name: "python", + description: "Python package over ctypes", + default: true, + build: configured::, + }, + TargetDesc { + name: "go", + description: "Go module over cgo", + default: true, + build: configured::, + }, + TargetDesc { + name: "ruby", + description: "Ruby gem over FFI", + default: true, + build: configured::, + }, + TargetDesc { + name: "kotlin", + description: "Kotlin/JVM and Android Gradle project with a JNI shim", + default: true, + build: configured::, + }, +]; + +/// The registered target named `name`. #[must_use] -pub fn all_default() -> Vec> { - vec![ - Box::new(ConfiguredBackend::new(c::CGenerator, c::CConfig::default())), - Box::new(ConfiguredBackend::new( - cpp::CppGenerator, - cpp::CppConfig::default(), - )), - Box::new(ConfiguredBackend::new( - swift::SwiftGenerator, - swift::SwiftConfig::default(), - )), - Box::new(ConfiguredBackend::new( - kotlin::KotlinGenerator, - kotlin::KotlinConfig::default(), - )), - Box::new(ConfiguredBackend::new( - node::NodeGenerator, - node::NodeConfig::default(), - )), - Box::new(ConfiguredBackend::new( - wasm::WasmGenerator, - wasm::WasmConfig::default(), - )), - Box::new(ConfiguredBackend::new( - python::PythonGenerator, - python::PythonConfig::default(), - )), - Box::new(ConfiguredBackend::new( - dotnet::DotnetGenerator, - dotnet::DotnetConfig::default(), - )), - Box::new(ConfiguredBackend::new( - dart::DartGenerator, - dart::DartConfig::default(), - )), - Box::new(ConfiguredBackend::new( - go::GoGenerator, - go::GoConfig::default(), - )), - Box::new(ConfiguredBackend::new( - ruby::RubyGenerator, - ruby::RubyConfig::default(), - )), - ] +pub fn find(name: &str) -> Option<&'static TargetDesc> { + REGISTRY.iter().find(|d| d.name == name) +} + +/// Every registered target name, in registry order. +pub fn names() -> impl Iterator { + REGISTRY.iter().map(|d| d.name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registered_names_match_the_targets() { + for desc in REGISTRY { + assert_eq!(desc.build_default().name(), desc.name); + } + let mut names: Vec<&str> = names().collect(); + let count = names.len(); + names.sort_unstable(); + names.dedup(); + assert_eq!(names.len(), count, "duplicate target names"); + } + + #[test] + fn unknown_config_keys_name_the_table() { + let table: toml::Table = toml::from_str("module_name = \"X\"").unwrap(); + let err = find("c").unwrap().build(table).err().unwrap(); + let err = format!("{err}"); + assert!( + err.contains("[generators.c]") && err.contains("module_name"), + "{err}" + ); + } } diff --git a/crates/weaveffi-cli/src/targets/node/addon.rs b/crates/weaveffi-cli/src/targets/node/addon.rs index e03730b5..bbec2e21 100644 --- a/crates/weaveffi-cli/src/targets/node/addon.rs +++ b/crates/weaveffi-cli/src/targets/node/addon.rs @@ -1,86 +1,118 @@ //! The N-API addon (`{library}_node.c`): the Node.js transport. //! -//! The fixed part (value conversions, error reporting, the async and -//! callback-interface machinery, the JavaScript-thread bookkeeping, and the -//! runtime symbols) is `runtime/addon.c`. This module appends one exported -//! entry point per C symbol (callables, async launchers, iterator +//! The fixed part (value conversions and checks, error reporting, the async +//! and callback-interface machinery, the JavaScript-thread bookkeeping, and +//! the runtime symbols) is `runtime/addon.c`. This module appends one +//! exported entry point per C symbol (callables, async launchers, iterator //! `next`/`destroy`, interface `clone`/`destroy`, and module contract //! tables), each following the raw calling convention of the shared //! JavaScript layer, plus a static vtable with thread-hopping trampolines per //! callback interface. +//! +//! Every C call is built from the model's lowered signature ([`AbiFn`]): +//! each slot's argument is looked up by the slot's name, so the argument +//! order is the model's by construction. + +use std::collections::HashMap; -use weaveffi_model::abi::AbiParam; +use weaveffi_model::abi::{AbiParam, CType}; use weaveffi_model::model::{ - contract_symbol, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, Model, + contract_symbol, AbiFn, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, Model, }; -use weaveffi_model::plan::{ArgPass, RetPass}; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::Prim; use crate::codegen::CodeWriter; -use crate::targets::js::names::{callback_method_name, direct_prim}; +use crate::targets::js::names::callback_method_name; use crate::utils::{render_prelude, render_trailer, CommentStyle}; /// The fixed addon runtime, with `{{PREFIX}}` (the C prefix), `{{MACRO}}` /// (its uppercase form), and `{{HEADER}}` (the header file) placeholders. const ADDON_C: &str = include_str!("runtime/addon.c"); -/// The `js_arg_*` reader and C type of a direct primitive's temporary. -fn arg_reader(p: Prim) -> (&'static str, &'static str) { - match p { - Prim::Bool => ("js_arg_bool", "bool"), - Prim::I8 => ("js_arg_i8", "int8_t"), - Prim::I16 => ("js_arg_i16", "int16_t"), - Prim::I32 => ("js_arg_i32", "int32_t"), - Prim::I64 => ("js_arg_i64", "int64_t"), - Prim::U8 => ("js_arg_u8", "uint8_t"), - Prim::U16 => ("js_arg_u16", "uint16_t"), - Prim::U32 => ("js_arg_u32", "uint32_t"), - Prim::U64 => ("js_arg_u64", "uint64_t"), - Prim::F32 => ("js_arg_f32", "float"), - Prim::F64 => ("js_arg_f64", "double"), - Prim::String | Prim::Bytes => unreachable!("not a direct primitive"), +/// The `js_arg_*` reader of a scalar C slot type (C-style enums are +/// `int32_t` typedefs). +fn arg_reader(ty: &CType) -> &'static str { + match ty { + CType::Bool => "js_arg_bool", + CType::Int8 => "js_arg_i8", + CType::Int16 => "js_arg_i16", + CType::Int32 | CType::Enum { .. } => "js_arg_i32", + CType::Int64 => "js_arg_i64", + CType::Uint8 => "js_arg_u8", + CType::Uint16 => "js_arg_u16", + CType::Uint32 => "js_arg_u32", + CType::Uint64 => "js_arg_u64", + CType::Float => "js_arg_f32", + CType::Double => "js_arg_f64", + other => unreachable!("{other:?} is not a scalar slot"), } } -/// The expression creating a JavaScript value from the direct C value -/// `expr`. -fn new_direct(p: Prim, expr: &str) -> String { - match p { - Prim::Bool => format!("js_new_bool(env, {expr})"), - Prim::I8 | Prim::I16 | Prim::I32 => format!("js_new_i32(env, (int32_t){expr})"), - Prim::U8 | Prim::U16 | Prim::U32 => format!("js_new_u32(env, (uint32_t){expr})"), - Prim::I64 => format!("js_new_i64(env, {expr})"), - Prim::U64 => format!("js_new_u64(env, {expr})"), - Prim::F32 | Prim::F64 => format!("js_new_f64(env, (double){expr})"), - Prim::String | Prim::Bytes => unreachable!("not a direct primitive"), +/// The expression creating a JavaScript value from the scalar C value +/// `expr` of slot type `ty`. +fn new_scalar(ty: &CType, expr: &str) -> String { + match ty { + CType::Bool => format!("js_new_bool(env, {expr})"), + CType::Int8 | CType::Int16 | CType::Int32 | CType::Enum { .. } => { + format!("js_new_i32(env, (int32_t){expr})") + } + CType::Uint8 | CType::Uint16 | CType::Uint32 => { + format!("js_new_u32(env, (uint32_t){expr})") + } + CType::Int64 => format!("js_new_i64(env, {expr})"), + CType::Uint64 => format!("js_new_u64(env, {expr})"), + CType::Float | CType::Double => format!("js_new_f64(env, (double){expr})"), + other => unreachable!("{other:?} is not a scalar slot"), } } -/// The expression converting a value received from the producer: `slots` -/// are the C expressions of its ABI slots (one, or a pointer and a length). -/// `owned` values (returns, async results, iterator elements) are released -/// after conversion; borrowed ones (callback arguments) are copied. -fn receive(ty: &Ty, pass: &RetPass, slots: &[String], owned: bool) -> String { - match pass { - RetPass::Void => "js_undefined(env)".into(), - RetPass::Direct => new_direct(direct_prim(ty), &slots[0]), - RetPass::String => format!( - "{}(env, {}, {})", - if owned { "js_take_str" } else { "js_new_str" }, - slots[0], - slots[1] - ), - RetPass::Bytes | RetPass::Buffer => format!( - "{}(env, {}, {})", - if owned { - "js_take_bytes" - } else { - "js_new_bytes" - }, - slots[0], - slots[1] - ), - RetPass::Object { .. } => format!("js_new_handle(env, {})", slots[0]), +/// The N-API typed array type and C element type of a typed-array (Slice) +/// element. +fn typed_array(elem: Prim) -> (&'static str, &'static str) { + match elem { + Prim::I8 => ("napi_int8_array", "int8_t"), + Prim::I16 => ("napi_int16_array", "int16_t"), + Prim::I32 => ("napi_int32_array", "int32_t"), + Prim::I64 => ("napi_bigint64_array", "int64_t"), + Prim::U16 => ("napi_uint16_array", "uint16_t"), + Prim::U32 => ("napi_uint32_array", "uint32_t"), + Prim::U64 => ("napi_biguint64_array", "uint64_t"), + Prim::F32 => ("napi_float32_array", "float"), + Prim::F64 => ("napi_float64_array", "double"), + Prim::Bool | Prim::U8 | Prim::String | Prim::Bytes => { + unreachable!("{elem} never crosses as a typed array") + } + } +} + +/// `js_new_slice(...)` (borrowed) or `js_take_slice(...)` (owned) of a +/// typed array of `elem` at `ptr` with `count` elements. +fn new_slice(elem: Prim, ptr: &str, count: &str, owned: bool) -> String { + let (napi, c) = typed_array(elem); + let f = if owned { + "js_take_slice" + } else { + "js_new_slice" + }; + format!("{f}(env, {napi}, {ptr}, {count}, sizeof({c}))") +} + +/// The type an out slot (`T* out_x`) points to. +fn pointee(ty: &CType) -> &CType { + match ty { + CType::Ptr { pointee, .. } => pointee, + other => unreachable!("{other:?} is not an out slot"), + } +} + +/// The local zero of a C type: `NULL` for a pointer, `false` for a bool, +/// `0` otherwise. +fn zero(ty: &CType) -> &'static str { + match ty { + _ if ty.is_pointer() => "NULL", + CType::Bool => "false", + _ => "0", } } @@ -196,8 +228,8 @@ struct Args { decls: Vec, /// Argument reads (each jumps to `done` on failure). reads: Vec, - /// The C argument expressions, in slot order. - call: Vec, + /// The C argument expression of each slot, by slot name. + slots: HashMap, /// Statements run after the call (callback registrations the producer /// now owns). transfer: Vec, @@ -205,74 +237,119 @@ struct Args { cleanup: Vec, } +impl Args { + fn slot(&mut self, slot: &AbiParam, expr: impl Into) { + self.slots.insert(slot.name.clone(), expr.into()); + } + + /// The argument list of a call to `abi`, every slot in the model's + /// order. + fn call(&self, abi: &AbiFn) -> String { + let args: Vec<&str> = abi + .params + .iter() + .map(|p| { + self.slots + .get(&p.name) + .unwrap_or_else(|| { + panic!("no argument for slot `{}` of {}", p.name, abi.symbol) + }) + .as_str() + }) + .collect(); + format!("{}({})", abi.symbol, args.join(", ")) + } +} + /// Marshal the receiver, parameters, and cancel token of `f` from `argv`. -fn marshal(model: &Model, f: &FnBinding, launch: &[AbiParam]) -> Args { +fn marshal(model: &Model, f: &FnBinding) -> Args { let prefix = model.prefix(); let mut a = Args::default(); let mut idx = 0usize; - if f.has_self { - let ty = launch[0].ty.render_c(prefix); + if let Some(recv) = &f.receiver { a.decls.push("void* self_h = NULL;".into()); a.reads.push(format!( "if (!js_arg_handle(env, argv[{idx}], &self_h, false)) goto done;" )); - a.call.push(format!("({ty})self_h")); + a.slot(recv, format!("({})self_h", recv.ty.render_c(prefix))); idx += 1; } for (n, p) in f.params.iter().enumerate() { let v = format!("a{n}"); - match p.arg_pass() { + let arg = format!("argv[{idx}]"); + match &p.pass { ArgPass::Direct { slot } => { - let (reader, tmp) = arg_reader(direct_prim(&p.ty)); - a.decls.push(format!("{tmp} {v} = 0;")); - a.reads - .push(format!("if (!{reader}(env, argv[{idx}], &{v})) goto done;")); - if matches!(p.ty, Ty::Enum(_)) { - a.call.push(format!("({}){v}", slot.ty.render_c(prefix))); - } else { - a.call.push(v); - } + a.decls + .push(format!("{} {v} = 0;", slot.ty.render_c(prefix))); + a.reads.push(format!( + "if (!{}(env, {arg}, &{v})) goto done;", + arg_reader(&slot.ty) + )); + a.slot(slot, v); } - ArgPass::String { .. } => { - a.decls.push(format!("js_str {v} = JS_STR_INIT;")); + ArgPass::OptDirect { has, value, .. } => { + a.decls.push(format!("bool {v}_has = false;")); + a.decls + .push(format!("{} {v} = 0;", value.ty.render_c(prefix))); a.reads.push(format!( - "if (!js_arg_str(env, argv[{idx}], &{v})) goto done;" + "if (js_present(env, {arg}, &{v}_has) && !{}(env, {arg}, &{v})) goto done;", + arg_reader(&value.ty) )); - a.call.push(format!("JS_STR_PTR({v})")); - a.call.push(format!("{v}.len")); + a.slot(has, format!("{v}_has")); + a.slot(value, v); + } + ArgPass::Slice { ptr, len, elem } => { + let (napi, c) = typed_array(*elem); + a.decls.push(format!("const void* {v} = NULL;")); + a.decls.push(format!("size_t {v}_len = 0;")); + a.reads.push(format!( + "if (!js_arg_slice(env, {arg}, {napi}, &{v}, &{v}_len)) goto done;" + )); + a.slot(ptr, format!("(const {c}*){v}")); + a.slot(len, format!("{v}_len")); + } + ArgPass::String { ptr, len } => { + a.decls.push(format!("js_str {v} = JS_STR_INIT;")); + a.reads + .push(format!("if (!js_arg_str(env, {arg}, &{v})) goto done;")); + a.slot(ptr, format!("JS_STR_PTR({v})")); + a.slot(len, format!("{v}.len")); a.cleanup.push(format!("js_str_free(&{v});")); } - ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + ArgPass::Bytes { ptr, len } | ArgPass::Buffer { ptr, len } => { a.decls.push(format!("const uint8_t* {v} = NULL;")); a.decls.push(format!("size_t {v}_len = 0;")); a.reads.push(format!( - "if (!js_arg_bytes(env, argv[{idx}], &{v}, &{v}_len)) goto done;" + "if (!js_arg_bytes(env, {arg}, &{v}, &{v}_len)) goto done;" )); - a.call.push(v.clone()); - a.call.push(format!("{v}_len")); + a.slot(ptr, v.clone()); + a.slot(len, format!("{v}_len")); } - ArgPass::Object { slot, nullable } => { + ArgPass::Object { slot, nullable, .. } => { a.decls.push(format!("void* {v} = NULL;")); a.reads.push(format!( - "if (!js_arg_handle(env, argv[{idx}], &{v}, {nullable})) goto done;" + "if (!js_arg_handle(env, {arg}, &{v}, {nullable})) goto done;" )); - a.call.push(format!("({}){v}", slot.ty.render_c(prefix))); + a.slot(slot, format!("({}){v}", slot.ty.render_c(prefix))); } - ArgPass::Callback { nullable, .. } => { - let cb = model.callback_interface( - p.ty.callback_interface_name().expect("callback parameter"), - ); - let (dispatch, vtable) = cb_names(&cb.c_tag); + ArgPass::Callback { + ctx, + vtable, + nullable, + interface, + } => { + let cb = model.callback_interface(interface); + let (dispatch, table) = cb_names(&cb.c_tag); a.decls.push(format!("js_cb* {v} = NULL;")); a.reads.push(format!( - "if (!js_arg_cb(env, argv[{idx}], \"{}\", {dispatch}, {nullable}, &{v})) goto done;", + "if (!js_arg_cb(env, {arg}, \"{}\", {dispatch}, {nullable}, &{v})) goto done;", cb.c_tag )); - a.call.push(format!("(void*){v}")); - if nullable { - a.call.push(format!("{v} != NULL ? &{vtable} : NULL")); + a.slot(ctx, format!("(void*){v}")); + if *nullable { + a.slot(vtable, format!("{v} != NULL ? &{table} : NULL")); } else { - a.call.push(format!("&{vtable}")); + a.slot(vtable, format!("&{table}")); } a.transfer.push(format!("{v} = NULL;")); a.cleanup @@ -281,190 +358,164 @@ fn marshal(model: &Model, f: &FnBinding, launch: &[AbiParam]) -> Args { } idx += 1; } - if f.cancellable { + if let Some(token) = f.async_binding().and_then(|ab| ab.cancel_token.as_ref()) { a.decls.push("void* token = NULL;".into()); a.reads.push(format!( "if (!js_arg_handle(env, argv[{idx}], &token, true)) goto done;" )); - a.call.push(format!("({prefix}_cancel_token*)token")); + a.slot(token, format!("({})token", token.ty.render_c(prefix))); idx += 1; } a.argc = idx; a } +/// The async result kind of a scalar slot type and the `js_async` field +/// its value is stored in. +fn async_scalar(ty: &CType) -> (&'static str, &'static str) { + match ty { + CType::Bool => ("JS_R_BOOL", "b"), + CType::Int8 | CType::Int16 | CType::Int32 | CType::Enum { .. } => ("JS_R_I32", "i"), + CType::Int64 => ("JS_R_I64", "i"), + CType::Uint8 | CType::Uint16 | CType::Uint32 => ("JS_R_U32", "u"), + CType::Uint64 => ("JS_R_U64", "u"), + CType::Float | CType::Double => ("JS_R_F64", "f"), + other => unreachable!("{other:?} is not a scalar slot"), + } +} + /// The async result kind and the statements storing the completion's /// result slots into `a`. -fn async_result(f: &FnBinding) -> (&'static str, Vec) { - let pass = RetPass::of(f.ret.as_ref()); - match pass { - RetPass::Void => ("JS_R_VOID", vec![]), - RetPass::Direct => { - let ty = f.ret.as_ref().expect("direct result"); - let (kind, field) = match direct_prim(ty) { - Prim::Bool => ("JS_R_BOOL", "b"), - Prim::I8 | Prim::I16 | Prim::I32 => ("JS_R_I32", "i"), - Prim::I64 => ("JS_R_I64", "i"), - Prim::U8 | Prim::U16 | Prim::U32 => ("JS_R_U32", "u"), - Prim::U64 => ("JS_R_U64", "u"), - Prim::F32 | Prim::F64 => ("JS_R_F64", "f"), - Prim::String | Prim::Bytes => unreachable!("not direct"), - }; - (kind, vec![format!("a->v.{field} = result;")]) +fn async_result(result: &ResultPass) -> (&'static str, Vec) { + match result { + ResultPass::Void => ("JS_R_VOID", vec![]), + ResultPass::Direct { result } => { + let (kind, field) = async_scalar(&result.ty); + (kind, vec![format!("a->v.{field} = {};", result.name)]) } - RetPass::String | RetPass::Bytes | RetPass::Buffer => ( - if pass == RetPass::String { - "JS_R_STR" - } else { - "JS_R_BYTES" - }, - vec!["a->v.p = result_ptr;".into(), "a->len = result_len;".into()], + ResultPass::OptDirect { has, value } => { + let (kind, field) = async_scalar(&value.ty); + ( + kind, + vec![ + "a->opt = true;".into(), + format!("a->has = {};", has.name), + format!("a->v.{field} = {};", value.name), + ], + ) + } + ResultPass::Slice { ptr, len, elem } => { + let (napi, c) = typed_array(*elem); + ( + "JS_R_SLICE", + vec![ + format!("a->v.p = {};", ptr.name), + format!("a->len = {};", len.name), + format!("a->slice_type = {napi};"), + format!("a->slice_size = sizeof({c});"), + ], + ) + } + ResultPass::String { ptr, len } => ( + "JS_R_STR", + vec![ + format!("a->v.p = {};", ptr.name), + format!("a->len = {};", len.name), + ], + ), + ResultPass::Bytes { ptr, len } | ResultPass::Buffer { ptr, len } => ( + "JS_R_BYTES", + vec![ + format!("a->v.p = {};", ptr.name), + format!("a->len = {};", len.name), + ], ), - RetPass::Object { .. } => ("JS_R_HANDLE", vec!["a->v.p = result;".into()]), + ResultPass::Object { result, .. } => { + ("JS_R_HANDLE", vec![format!("a->v.p = {};", result.name)]) + } } } /// Emit one callable's entry point(s). fn emit_callable(w: &mut CodeWriter, model: &Model, f: &FnBinding, exports: &mut Vec) { let prefix = model.prefix(); - match &f.shape { - CallShape::Async(ab) => { - let symbol = &ab.launch.symbol; - let (kind, store) = async_result(f); - let params: Vec = ab - .callback_params - .iter() - .map(|p| format!("{} {}", p.ty.render_c(prefix), p.name)) - .collect(); - w.block( - format!("static void done_{symbol}({}) {{", params.join(", ")), - "}", - |w| { - w.line("js_async* a = (js_async*)context;"); - for s in &store { - w.line(s); - } - w.line("js_async_done(a, err);"); - }, - ); - w.blank(); - let mut args = marshal(model, f, &ab.launch.params); - args.call.push(format!("done_{symbol}")); - args.call.push("a".into()); - emit_entry(w, symbol, &args, |w| { - w.line(format!( - "js_async* a = js_async_begin(env, {kind}, \"{symbol}\", &ret);" - )); - w.line("js_sync_begin();"); - w.line(format!("{symbol}({});", args.call.join(", "))); - w.line("js_sync_end();"); - for t in &args.transfer { - w.line(t); + let symbol = &f.abi.symbol; + let mut args = marshal(model, f); + if let Some(ab) = f.async_binding() { + let (kind, store) = async_result(&ab.result); + let params: Vec = ab + .callback_params + .iter() + .map(|p| format!("{} {}", p.ty.render_c(prefix), p.name)) + .collect(); + w.block( + format!("static void done_{symbol}({}) {{", params.join(", ")), + "}", + |w| { + w.line("js_async* a = (js_async*)context;"); + for s in &store { + w.line(s); } - }); - exports.push(symbol.clone()); - } - CallShape::Sync(abi) => { - let args = marshal(model, f, &abi.params); - let pass = RetPass::of(f.ret.as_ref()); - let result = f.ret.as_ref().map(|ty| SyncResult { - c_type: abi.ret.render_c(prefix), - out_len: matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer), - value: receive(ty, &pass, &["r".into(), "out_len".into()], true), - }); - emit_sync(w, &abi.symbol, &args, result.as_ref()); - exports.push(abi.symbol.clone()); - } - CallShape::Iterator(it) => { - let args = marshal(model, f, &it.launch.params); - let handle = SyncResult { - c_type: it.launch.ret.render_c(prefix), - out_len: false, - value: "js_new_handle(env, r)".into(), - }; - emit_sync(w, &it.launch.symbol, &args, Some(&handle)); - exports.push(it.launch.symbol.clone()); - - let item = it.item_ctype().render_c(prefix); - let protocol = it.protocol(f); - let pair = matches!( - protocol.elem, - RetPass::String | RetPass::Bytes | RetPass::Buffer - ); - let symbol = &it.next.symbol; - w.block( - format!("static napi_value nx_{symbol}(napi_env env, napi_callback_info info) {{"), - "}", - |w| { - w.line("JS_ARGS(1);"); - w.line("void* h = NULL;"); - w.line("if (!js_arg_handle(env, argv[0], &h, false)) return NULL;"); - w.line(format!("{item} item;")); - w.line("memset(&item, 0, sizeof item);"); - w.line("size_t item_len = 0;"); - w.line("js_error err = {0};"); - let len = if pair { "&item_len, " } else { "" }; - w.line("js_sync_begin();"); - w.line(format!( - "int32_t has = {symbol}(({}*)h, &item, {len}&err);", - it.iter_tag - )); - w.line("js_sync_end();"); - if !pair { - w.line("(void)item_len;"); - } - w.line("if (err.code != 0) return js_throw(env, &err);"); - w.line("if (has == 0) return js_undefined(env);"); - w.line(format!( - "return {};", - receive( - &it.elem, - &protocol.elem, - &["item".into(), "item_len".into()], - true - ) - )); - }, - ); - w.blank(); - exports.push(symbol.clone()); - emit_handle_fn( - w, - &it.destroy_symbol, - &format!( - "js_sync_begin();\n{}(({}*)h);\njs_sync_end();\nreturn js_undefined(env);", - it.destroy_symbol, it.iter_tag - ), - ); - exports.push(it.destroy_symbol.clone()); - } + w.line("js_async_done(a, err);"); + }, + ); + w.blank(); + args.slots + .insert("callback".into(), format!("done_{symbol}")); + args.slots.insert("context".into(), "a".into()); + let call = args.call(&f.abi); + emit_entry(w, symbol, &args, |w| { + w.line(format!( + "js_async* a = js_async_begin(env, {kind}, \"{symbol}\", &ret);" + )); + w.line("js_sync_begin();"); + w.line(format!("{call};")); + w.line("js_sync_end();"); + for t in &args.transfer { + w.line(t); + } + }); + exports.push(symbol.clone()); + return; } -} - -/// The result of a synchronous entry point: the C type of `r`, whether an -/// `out_len` slot follows the inputs, and the JavaScript value made from `r` -/// (and `out_len`). -struct SyncResult { - c_type: String, - out_len: bool, - value: String, -} -/// Emit a synchronous entry point: marshal, call, check the error slot, and -/// convert the result (`None` for a void call). -fn emit_sync(w: &mut CodeWriter, symbol: &str, args: &Args, result: Option<&SyncResult>) { - emit_entry(w, symbol, args, |w| { + // A synchronous call (or an iterator launcher): the out slots its + // return needs, then the call, the error check, and the conversion. + let mut outs: Vec = Vec::new(); + for slot in f.ret_pass.out_slots() { + let ty = pointee(&slot.ty); + outs.push(format!( + "{} {} = {};", + ty.render_c(prefix), + slot.name, + zero(ty) + )); + args.slot(slot, format!("&{}", slot.name)); + } + args.slots.insert("out_err".into(), "&err".into()); + let value = match &f.ret_pass { + RetPass::Void => None, + RetPass::Direct => Some(new_scalar(&f.abi.ret, "r")), + RetPass::OptDirect { out_value } => Some(format!( + "r ? {} : js_null(env)", + new_scalar(pointee(&out_value.ty), &out_value.name) + )), + RetPass::Slice { out_len, elem } => Some(new_slice(*elem, "r", &out_len.name, true)), + RetPass::String { out_len } => Some(format!("js_take_str(env, r, {})", out_len.name)), + RetPass::Bytes { out_len } | RetPass::Buffer { out_len } => { + Some(format!("js_take_bytes(env, r, {})", out_len.name)) + } + RetPass::Object { .. } | RetPass::Iterator(_) => Some("js_new_handle(env, r)".into()), + }; + let call = args.call(&f.abi); + emit_entry(w, symbol, &args, |w| { w.line("js_error err = {0};"); - let mut call = args.call.clone(); - if result.is_some_and(|r| r.out_len) { - w.line("size_t out_len = 0;"); - call.push("&out_len".into()); + for o in &outs { + w.line(o); } - call.push("&err".into()); - let call = format!("{symbol}({})", call.join(", ")); w.line("js_sync_begin();"); - match result { - Some(r) => w.line(format!("{} r = {call};", r.c_type)), + match &value { + Some(_) => w.line(format!("{} r = {call};", f.abi.ret.render_c(prefix))), None => w.line(format!("{call};")), }; w.line("js_sync_end();"); @@ -475,9 +526,96 @@ fn emit_sync(w: &mut CodeWriter, symbol: &str, args: &Args, result: Option<&Sync w.line(" js_throw(env, &err);"); w.line(" goto done;"); w.line("}"); - let value = result.map_or("js_undefined(env)", |r| r.value.as_str()); - w.line(format!("ret = {value};")); + w.line(format!( + "ret = {};", + value.as_deref().unwrap_or("js_undefined(env)") + )); }); + exports.push(symbol.clone()); + + if let Some(it) = f.iterator() { + emit_iterator_next(w, prefix, it); + exports.push(it.next.symbol.clone()); + emit_handle_fn( + w, + &it.destroy_symbol, + &format!( + "js_sync_begin();\n{}(({}*)h);\njs_sync_end();\nreturn js_undefined(env);", + it.destroy_symbol, it.iter_tag + ), + ); + exports.push(it.destroy_symbol.clone()); + } +} + +/// Emit an iterator's `next` entry point: `undefined` once exhausted, else +/// the element (`null` for an absent optional one). +fn emit_iterator_next( + w: &mut CodeWriter, + prefix: &str, + it: &weaveffi_model::model::IteratorBinding, +) { + let symbol = &it.next.symbol; + let mut slots: HashMap = HashMap::new(); + let mut decls = Vec::new(); + for slot in it.item.slots() { + let ty = pointee(&slot.ty); + decls.push(format!( + "{} {} = {};", + ty.render_c(prefix), + slot.name, + zero(ty) + )); + slots.insert(slot.name.clone(), format!("&{}", slot.name)); + } + let iter = &it.next.params[0]; + slots.insert(iter.name.clone(), format!("({}*)h", it.iter_tag)); + slots.insert("out_err".into(), "&err".into()); + let call = Args { + slots, + ..Args::default() + } + .call(&it.next); + let value = match &it.item { + ItemPass::Direct { out_item } => new_scalar(pointee(&out_item.ty), &out_item.name), + ItemPass::OptDirect { out_has, out_item } => format!( + "{} ? {} : js_null(env)", + out_has.name, + new_scalar(pointee(&out_item.ty), &out_item.name) + ), + ItemPass::Slice { + out_item, + out_len, + elem, + } => new_slice(*elem, &out_item.name, &out_len.name, true), + ItemPass::String { out_item, out_len } => { + format!("js_take_str(env, {}, {})", out_item.name, out_len.name) + } + ItemPass::Bytes { out_item, out_len } | ItemPass::Buffer { out_item, out_len } => { + format!("js_take_bytes(env, {}, {})", out_item.name, out_len.name) + } + ItemPass::Object { out_item, .. } => format!("js_new_handle(env, {})", out_item.name), + }; + w.block( + format!("static napi_value nx_{symbol}(napi_env env, napi_callback_info info) {{"), + "}", + |w| { + w.line("JS_ARGS(1);"); + w.line("void* h = NULL;"); + w.line("if (!js_arg_handle(env, argv[0], &h, false)) return NULL;"); + for d in &decls { + w.line(d); + } + w.line("js_error err = {0};"); + w.line("js_sync_begin();"); + w.line(format!("int32_t has = {call};")); + w.line("js_sync_end();"); + w.line("if (err.code != 0) return js_throw(env, &err);"); + w.line("if (has == 0) return js_undefined(env);"); + w.line(format!("return {value};")); + }, + ); + w.blank(); } /// Emit an entry point's frame: the argument reads, `body` in its own @@ -504,139 +642,21 @@ fn emit_entry(w: &mut CodeWriter, symbol: &str, args: &Args, body: impl FnOnce(& w.blank(); } -/// The parts of one callback method's vtable entry: its C return type, the -/// parameter slots (declarations `p0`, `p1`, ...), and whether the return -/// travels through the `out_ptr`/`out_len` slots. -struct Entry<'a> { - ret_c: String, - slots: &'a [AbiParam], - run: bool, -} - -impl<'a> Entry<'a> { - fn of(method: &'a CallbackMethodBinding, pass: &RetPass, prefix: &str) -> Self { - let run = matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer); - let inputs: usize = method.params.iter().map(|p| p.abi.len()).sum(); - Entry { - ret_c: method.abi_ret.render_c(prefix), - slots: &method.abi_params[1..1 + inputs], - run, - } - } +/// The frame field (and trampoline parameter) of a callback method slot. +fn field(slot: &AbiParam) -> String { + format!("p_{}", slot.name) } /// Emit one callback interface: per method a frame, an invoker that runs on /// the JavaScript thread, and a trampoline (the vtable entry) that calls the /// invoker directly or hops to the JavaScript thread and waits; then the -/// dispatcher of hopped calls and the static vtable. +/// dispatcher of hopped calls and the static vtable (flags 0: methods may +/// be called from any thread). fn emit_callback_interface(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, prefix: &str) { let tag = &cb.c_tag; - let protocol = cb.protocol(); let (dispatch, vtable) = cb_names(tag); - for (idx, ((method, passes), ret)) in cb - .methods - .iter() - .zip(&protocol.method_args) - .zip(&protocol.method_returns) - .enumerate() - { - let entry = Entry::of(method, ret, prefix); - let frame = format!("frame_{tag}_{}", method.name); - w.block("typedef struct {", format!("}} {frame};"), |w| { - w.line("js_error* out_err;"); - for (i, s) in entry.slots.iter().enumerate() { - w.line(format!("{} p{i};", s.ty.render_c(prefix))); - } - if entry.run { - w.line("uint8_t** out_ptr;"); - w.line("size_t* out_len;"); - } else if method.ret.is_some() { - w.line(format!("{} result;", entry.ret_c)); - } - }); - w.blank(); - - let what = format!("{}.{}", cb.name, callback_method_name(&method.name)); - w.block( - format!( - "static void invoke_{tag}_{}(napi_env env, js_cb* cb, {frame}* f) {{", - method.name - ), - "}", - |w| { - w.line("napi_handle_scope scope;"); - w.line("napi_open_handle_scope(env, &scope);"); - let n = method.params.len(); - w.line(format!("napi_value argv[{}];", n + 1)); - let mut slot = 0usize; - for (i, (p, pass)) in method.params.iter().zip(passes).enumerate() { - let names: Vec = (slot..slot + p.abi.len()) - .map(|k| format!("f->p{k}")) - .collect(); - slot += p.abi.len(); - w.line(format!( - "argv[{i}] = {};", - receive(&p.ty, pass, &names, false) - )); - } - w.line("napi_value result;"); - w.line(format!( - "if (!js_cb_call(env, cb, \"{}\", {n}, argv, &result)) {{", - method.name - )); - w.line(format!( - " js_cb_report(env, f->out_err, \"{what} failed\");" - )); - if let Some(ty) = &method.ret { - w.line("} else {"); - w.scope(|w| emit_callback_return(w, ty, ret, &entry, &what)); - } - w.line("}"); - w.line("napi_close_handle_scope(env, scope);"); - }, - ); - w.blank(); - - let mut decls = vec!["void* ctx".to_string()]; - for (i, s) in entry.slots.iter().enumerate() { - decls.push(format!("{} p{i}", s.ty.render_c(prefix))); - } - if entry.run { - decls.push("uint8_t** out_ptr".into()); - decls.push("size_t* out_len".into()); - } - decls.push(format!("{prefix}_error* out_err")); - w.block( - format!( - "static {} tramp_{tag}_{}({}) {{", - entry.ret_c, - method.name, - decls.join(", ") - ), - "}", - |w| { - w.line(format!("{frame} f;")); - w.line("memset(&f, 0, sizeof f);"); - w.line("f.out_err = out_err;"); - for i in 0..entry.slots.len() { - w.line(format!("f.p{i} = p{i};")); - } - if entry.run { - w.line("f.out_ptr = out_ptr;"); - w.line("f.out_len = out_len;"); - } - w.line("js_cb* cb = (js_cb*)ctx;"); - w.line("if (js_cb_on_js_thread(cb)) {"); - w.line(format!(" invoke_{tag}_{}(cb->env, cb, &f);", method.name)); - w.line("} else {"); - w.line(format!(" js_cb_hop(cb, {idx}, &f, out_err, \"{what}\");")); - w.line("}"); - if method.ret.is_some() && !entry.run { - w.line("return f.result;"); - } - }, - ); - w.blank(); + for (idx, method) in cb.methods.iter().enumerate() { + emit_callback_method(w, cb, method, idx, prefix); } w.block( @@ -653,9 +673,7 @@ fn emit_callback_interface(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, pr w.line("}"); w.line("if (!js_cb_take(req)) return;"); w.line("if (env == NULL) {"); - w.line(format!( - " {prefix}_error_set(req->out_err, -4, \"the JavaScript environment is shutting down\");" - )); + w.line(" js_error_set(req->out_err, -4, \"the JavaScript environment is shutting down\");"); w.line("} else {"); w.line(" switch (req->method) {"); for (i, method) in cb.methods.iter().enumerate() { @@ -683,39 +701,201 @@ fn emit_callback_interface(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, pr w.blank(); } +/// Emit one callback method's frame, invoker, and trampoline. +fn emit_callback_method( + w: &mut CodeWriter, + cb: &CallbackInterfaceBinding, + method: &CallbackMethodBinding, + idx: usize, + prefix: &str, +) { + let tag = &cb.c_tag; + let frame = format!("frame_{tag}_{}", method.name); + // Every slot but the context: the inputs, the return's out slots, and + // `out_err`. + let slots = &method.abi.params[1..]; + let ret_c = method.abi.ret.render_c(prefix); + let returns = method.abi.ret != CType::Void; + w.block("typedef struct {", format!("}} {frame};"), |w| { + for s in slots { + w.line(format!("{} {};", s.ty.render_c(prefix), field(s))); + } + if returns { + w.line(format!("{ret_c} result;")); + } + }); + w.blank(); + + let what = format!("{}.{}", cb.name, callback_method_name(&method.name)); + w.block( + format!( + "static void invoke_{tag}_{}(napi_env env, js_cb* cb, {frame}* f) {{", + method.name + ), + "}", + |w| { + w.line("napi_handle_scope scope;"); + w.line("napi_open_handle_scope(env, &scope);"); + let n = method.params.len(); + w.line(format!("napi_value argv[{}];", n + 1)); + for (i, p) in method.params.iter().enumerate() { + let f = |s: &AbiParam| format!("f->{}", field(s)); + let value = match &p.pass { + ArgPass::Direct { slot } => new_scalar(&slot.ty, &f(slot)), + ArgPass::OptDirect { has, value, .. } => format!( + "{} ? {} : js_null(env)", + f(has), + new_scalar(&value.ty, &f(value)) + ), + ArgPass::Slice { ptr, len, elem } => new_slice(*elem, &f(ptr), &f(len), false), + ArgPass::String { ptr, len } => { + format!("js_new_str(env, {}, {})", f(ptr), f(len)) + } + ArgPass::Bytes { ptr, len } | ArgPass::Buffer { ptr, len } => { + format!("js_new_bytes(env, {}, {})", f(ptr), f(len)) + } + ArgPass::Object { slot, .. } => format!("js_new_handle(env, {})", f(slot)), + ArgPass::Callback { .. } => { + unreachable!("callback methods take value types only") + } + }; + w.line(format!("argv[{i}] = {value};")); + } + w.line("napi_value result;"); + w.line(format!( + "if (!js_cb_call(env, cb, \"{}\", {n}, argv, &result)) {{", + method.name + )); + w.line(format!( + " js_cb_report(env, f->p_out_err, \"{what} failed\");" + )); + if method.ret.is_some() { + w.line("} else {"); + w.scope(|w| emit_callback_return(w, method, &ret_c, &what, prefix)); + } + w.line("}"); + w.line("napi_close_handle_scope(env, scope);"); + }, + ); + w.blank(); + + let mut decls = vec!["void* ctx".to_string()]; + decls.extend( + slots + .iter() + .map(|s| format!("{} {}", s.ty.render_c(prefix), field(s))), + ); + w.block( + format!( + "static {ret_c} tramp_{tag}_{}({}) {{", + method.name, + decls.join(", ") + ), + "}", + |w| { + w.line(format!("{frame} f;")); + w.line("memset(&f, 0, sizeof f);"); + for s in slots { + w.line(format!("f.{0} = {0};", field(s))); + } + w.line("js_cb* cb = (js_cb*)ctx;"); + w.line("if (js_cb_on_js_thread(cb)) {"); + w.line(format!(" invoke_{tag}_{}(cb->env, cb, &f);", method.name)); + w.line("} else {"); + w.line(format!( + " js_cb_hop(cb, {idx}, &f, f.p_out_err, \"{what}\");" + )); + w.line("}"); + if returns { + w.line("return f.result;"); + } + }, + ); + w.blank(); +} + /// Emit the conversion of a callback method's JavaScript `result` into its -/// vtable return: the frame's `result`, or a run in the out slots. A value -/// of the wrong type is reported as a failure. -fn emit_callback_return(w: &mut CodeWriter, ty: &Ty, pass: &RetPass, entry: &Entry, what: &str) { +/// vtable return: the frame's `result`, a value behind `out_value`, or a run +/// in the `out_ptr`/`out_len` slots. A value of the wrong type is reported +/// as a failure. +fn emit_callback_return( + w: &mut CodeWriter, + method: &CallbackMethodBinding, + ret_c: &str, + what: &str, + prefix: &str, +) { let wrong = - format!("js_cb_report(env, f->out_err, \"{what} returned a value of the wrong type\");"); - match pass { - RetPass::Void => {} - RetPass::Direct => { - let (reader, tmp) = arg_reader(direct_prim(ty)); - w.line(format!("{tmp} v = 0;")); - w.line(format!("if ({reader}(env, result, &v)) {{")); - w.line(format!(" f->result = ({})v;", entry.ret_c)); + format!("js_cb_report(env, f->p_out_err, \"{what} returned a value of the wrong type\");"); + match &method.ret_pass { + CallbackRetPass::Void => {} + CallbackRetPass::Direct => { + w.line(format!("{ret_c} v = 0;")); + w.line(format!( + "if ({}(env, result, &v)) {{", + arg_reader(&method.abi.ret) + )); + w.line(" f->result = v;"); + w.line("} else {"); + w.line(format!(" {wrong}")); + w.line("}"); + } + CallbackRetPass::OptDirect { out_value } => { + let ty = pointee(&out_value.ty); + w.line("bool has = false;"); + w.line(format!("{} v = 0;", ty.render_c(prefix))); + w.line(format!( + "if (js_present(env, result, &has) && !{}(env, result, &v)) {{", + arg_reader(ty) + )); + w.line(format!(" {wrong}")); + w.line("} else if (has) {"); + w.line(format!(" *f->{} = v;", field(out_value))); + w.line(" f->result = true;"); + w.line("}"); + } + CallbackRetPass::Slice { + out_ptr, + out_len, + elem, + } => { + let (napi, c) = typed_array(*elem); + w.line("void* run = NULL;"); + w.line("size_t n = 0;"); + w.line(format!( + "if (js_ret_slice(env, result, {napi}, sizeof({c}), &run, &n)) {{" + )); + w.line(format!(" *f->{} = ({c}*)run;", field(out_ptr))); + w.line(format!(" *f->{} = n;", field(out_len))); w.line("} else {"); w.line(format!(" {wrong}")); w.line("}"); } - RetPass::String => { - w.line("if (!js_ret_str(env, result, f->out_ptr, f->out_len)) {"); + CallbackRetPass::String { out_ptr, out_len } => { + w.line(format!( + "if (!js_ret_str(env, result, f->{}, f->{})) {{", + field(out_ptr), + field(out_len) + )); w.line(format!(" {wrong}")); w.line("}"); } - RetPass::Bytes | RetPass::Buffer => { - w.line("if (!js_ret_bytes(env, result, f->out_ptr, f->out_len)) {"); + CallbackRetPass::Bytes { out_ptr, out_len } + | CallbackRetPass::Buffer { out_ptr, out_len } => { + w.line(format!( + "if (!js_ret_bytes(env, result, f->{}, f->{})) {{", + field(out_ptr), + field(out_len) + )); w.line(format!(" {wrong}")); w.line("}"); } - RetPass::Object { nullable } => { + CallbackRetPass::Object { nullable, .. } => { w.line("void* h = NULL;"); w.line(format!( "if (js_arg_handle(env, result, &h, {nullable})) {{" )); - w.line(format!(" f->result = ({})h;", entry.ret_c)); + w.line(format!(" f->result = ({ret_c})h;")); w.line("} else {"); w.line(format!(" {wrong}")); w.line("}"); diff --git a/crates/weaveffi-cli/src/targets/node/mod.rs b/crates/weaveffi-cli/src/targets/node/mod.rs index 5767a867..bc367da3 100644 --- a/crates/weaveffi-cli/src/targets/node/mod.rs +++ b/crates/weaveffi-cli/src/targets/node/mod.rs @@ -1,33 +1,33 @@ //! Node.js binding generator. //! //! Emits an npm package: the ES module (`index.js`, `index.d.ts`, -//! `runtime.js`) shared with the WebAssembly target through the shared -//! JavaScript layer, and its transport, a small N-API addon -//! (`{library}_node.c`) that calls the library's C ABI. `weaveffi build` -//! prebuilds the addon per platform and `weaveffi package` ships it in -//! per-platform packages; `npm install` compiles it with node-gyp (from -//! `binding.gyp`, against the bundled copy of the C header) only when no -//! prebuilt addon matches. Implements [`LanguageBackend`]; the shared driver -//! bridges it into the generator pipeline. +//! `runtime.js`, and the `./debug` export) shared with the WebAssembly +//! target through the [shared JavaScript layer](crate::targets::js), and +//! its transport, a small N-API addon (`{library}_node.c`) that calls the +//! library's C ABI. `weaveffi build` prebuilds the addon per platform and +//! `weaveffi package` ships it in per-platform packages; `npm install` +//! compiles it with node-gyp (from `binding.gyp`, against the bundled copy +//! of the C header) only when no prebuilt addon matches. mod addon; mod package; -use camino::Utf8Path; +use camino::Utf8PathBuf; use serde::{Deserialize, Serialize}; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; -use crate::backend::{LanguageBackend, OutputFile}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{Artifact, PackageContext, PackagedFile}; use crate::platform::node_addon_name; use crate::targets::c::render_c_header_from_model; -use crate::targets::js; +use crate::targets::js::{self, npm_tarball_name}; +use crate::targets::{Glue, GlueKind, Target}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use miette::Result; use self::addon::render_addon_c; -pub(crate) use self::package::npm_tarball_name; use self::package::{ node_platform_tokens, render_binding_gyp, render_package_json, render_platform_package_json, render_readme, NodeNames, @@ -68,7 +68,15 @@ impl NodeConfig { } /// Node.js backend: an ES module over an N-API addon. -pub struct NodeGenerator; +pub struct NodeGenerator { + config: NodeConfig, +} + +impl From for NodeGenerator { + fn from(config: NodeConfig) -> Self { + Self { config } + } +} /// The generated `index.js`: the imports, the addon loader, the load-time /// checks, and the shared API. @@ -84,7 +92,7 @@ fn render_index(model: &Model, names: &NodeNames<'_>) -> String { ); w.blank(); js::render_contract(&mut w, model); - w.line(js::verify_call(model, names.identity)); + w.line(js::verify_call(model)); w.blank(); js::render_api(&mut w, model); w.raw(render_trailer(CommentStyle::DoubleSlash, "index.js")); @@ -98,14 +106,16 @@ fn render_files( platforms: &[String], default_dir: &str, ) -> Vec<(String, String)> { - let modules: Vec = model.roots().map(js::names::module_name).collect(); + let modules: Vec = model + .roots() + .map(|m| js::names::module_name(&m.name)) + .collect(); vec![ ("index.js".into(), render_index(model, names)), - ( - "index.d.ts".into(), - js::render_dts(model, names.identity, ""), - ), + ("index.d.ts".into(), js::render_dts(model, "")), ("runtime.js".into(), js::render_runtime(names.identity)), + ("debug.js".into(), js::render_debug_js()), + ("debug.d.ts".into(), js::render_debug_dts()), ( format!("{}.c", names.addon), render_addon_c(model, &names.header, &format!("{}.c", names.addon)), @@ -123,15 +133,35 @@ fn render_files( ] } -impl LanguageBackend for NodeGenerator { - type Config = NodeConfig; - +impl Target for NodeGenerator { fn name(&self) -> &'static str { "node" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { - let dir = out_dir.join("node"); + fn glue(&self, model: &Model) -> Option { + let source = format!("{}.c", node_addon_name(&model.identity.library)); + Glue::from_files(GlueKind::NodeAddon, &self.render(model), &source, model) + } + + /// The package root, where `binding.gyp` links and loads the library. + fn dev_bundle_dir(&self, _model: &Model) -> Option { + Some(Utf8PathBuf::new()) + } + + fn fixed_files(&self) -> &'static [&'static str] { + &[ + "README.md", + "binding.gyp", + "debug.d.ts", + "debug.js", + "package.json", + "runtime.js", + ] + } + + fn render(&self, model: &Model) -> Vec { + let config = &self.config; + let dir = Utf8PathBuf::new(); let names = config.names(&model.identity); render_files(model, &names, &[], "path.resolve('.')") .into_iter() @@ -145,12 +175,8 @@ impl LanguageBackend for NodeGenerator { /// the addon, plus the main package, which lists them in /// `optionalDependencies` and compiles the addon at install time only /// when the installed platform package has none. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let names = config.names(&model.identity); let id = names.identity; let mut artifacts = Vec::new(); @@ -183,7 +209,7 @@ impl LanguageBackend for NodeGenerator { platform_names.push(name); } if platform_names.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } let default_dir = format!( "path.dirname(require.resolve('{}-' + process.platform + '-' + process.arch + '/package.json'))", @@ -198,7 +224,7 @@ impl LanguageBackend for NodeGenerator { "package", files, )); - Some(artifacts) + Ok(artifacts) } } diff --git a/crates/weaveffi-cli/src/targets/node/package.rs b/crates/weaveffi-cli/src/targets/node/package.rs index ca1a18c4..93b728b8 100644 --- a/crates/weaveffi-cli/src/targets/node/package.rs +++ b/crates/weaveffi-cli/src/targets/node/package.rs @@ -5,7 +5,7 @@ use weaveffi_model::pkg::Identity; use crate::manifest::{JsonObject, JsonValue}; use crate::platform::Platform; -use crate::targets::js::npm_metadata; +use crate::targets::js::{npm_metadata, PACKAGE_FILES}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; /// The names every file of the generated package derives from. @@ -25,14 +25,15 @@ pub(crate) struct NodeNames<'a> { impl NodeNames<'_> { /// The files `npm pack` publishes. fn files(&self) -> Vec { - vec![ - "index.js".into(), - "index.d.ts".into(), - "runtime.js".into(), - "binding.gyp".into(), - format!("{}.c", self.addon), - self.header.clone(), - ] + PACKAGE_FILES + .iter() + .map(|f| (*f).to_string()) + .chain([ + "binding.gyp".into(), + format!("{}.c", self.addon), + self.header.clone(), + ]) + .collect() } } @@ -50,15 +51,6 @@ fn install_script(names: &NodeNames<'_>) -> String { ) } -/// The npm tarball file name `npm pack` gives `name` at `version` -/// (`kvstore-1.2.0.tgz`, or `acme-kv-1.2.0.tgz` for `@acme/kv`). -pub(crate) fn npm_tarball_name(name: &str, version: &str) -> String { - format!( - "{}-{version}.tgz", - name.trim_start_matches('@').replace('/', "-") - ) -} - /// Render `package.json`. `platform_packages` lists the per-platform /// packages of the packaged layout (empty for plain generation). pub(crate) fn render_package_json(names: &NodeNames<'_>, platform_packages: &[String]) -> String { diff --git a/crates/weaveffi-cli/src/targets/node/runtime/addon.c b/crates/weaveffi-cli/src/targets/node/runtime/addon.c index 0f18e49c..ac002248 100644 --- a/crates/weaveffi-cli/src/targets/node/runtime/addon.c +++ b/crates/weaveffi-cli/src/targets/node/runtime/addon.c @@ -8,6 +8,7 @@ #endif #include #include +#include #include #include #include @@ -23,11 +24,13 @@ * The N-API transport of the JavaScript bindings. Every C symbol of the * library is exported to `index.js` under its own name and called with the * raw convention the shared JavaScript layer documents: direct values as - * numbers, bigints, and booleans; strings as strings; bytes and value - * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint - * handles (null when absent); callback interfaces as adapter objects (null - * for an absent optional one). A failure throws an instance of the - * runtime's `$Fault` class. + * numbers, bigints, and booleans (integers range-checked); optional scalars + * as the value or null; typed arrays (numeric lists) as the matching + * TypedArray; strings as strings; bytes and value buffers as Uint8Arrays; + * objects, iterators, and cancel tokens as bigint handles (null when + * absent); callback interfaces as adapter objects (null for an absent + * optional one). A failure throws an instance of the runtime's `$Fault` + * class. */ typedef {{PREFIX}}_error js_error; @@ -216,6 +219,12 @@ JS_HELPER napi_value js_new_bool(napi_env env, bool v) { return out; } +JS_HELPER napi_value js_null(napi_env env) { + napi_value out; + napi_get_null(env, &out); + return out; +} + /* An object, iterator, or cancel token handle: the pointer as a bigint, or * null for NULL. */ JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { @@ -241,6 +250,26 @@ JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) return out; } +/* A typed array (numeric list) of `count` elements of `size` bytes: a + * TypedArray of `type` holding a copy. A zero-length run is never read. */ +JS_HELPER napi_value js_new_slice(napi_env env, napi_typedarray_type type, const void* ptr, + size_t count, size_t size) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, count * size, &data, &ab); + if (count > 0) memcpy(data, ptr, count * size); + napi_create_typedarray(env, type, count, ab, 0, &out); + return out; +} + +/* A returned typed array: copy it, then release the producer's run. */ +JS_HELPER napi_value js_take_slice(napi_env env, napi_typedarray_type type, const void* ptr, + size_t count, size_t size) { + napi_value out = js_new_slice(env, type, ptr, count, size); + if (ptr != NULL) {{PREFIX}}_free_bytes((uint8_t*)ptr, count * size); + return out; +} + /* A module's contract table as a BigUint64Array of `id, hash` pairs. */ JS_HELPER napi_value js_new_contract(napi_env env, const {{PREFIX}}_contract_entry* table, size_t len) { @@ -256,11 +285,13 @@ JS_HELPER napi_value js_new_contract(napi_env env, const {{PREFIX}}_contract_ent return out; } -JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, - const uint8_t* payload, size_t payload_len) { +/* A `$Fault` for an error the library reported: its code, its message (a + * length-delimited UTF-8 run, NULL for none), and its payload. */ +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const uint8_t* message, + size_t message_len, const uint8_t* payload, size_t payload_len) { napi_value argv[3], ctor, out = NULL; napi_create_int32(env, code, &argv[0]); - napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + argv[1] = js_new_str(env, message, message != NULL ? message_len : 0); if (payload != NULL) { argv[2] = js_new_bytes(env, payload, payload_len); } else { @@ -276,7 +307,8 @@ JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, /* Throw the error a call reported, then release it. */ JS_HELPER napi_value js_throw(napi_env env, js_error* err) { - napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + napi_throw(env, js_fault(env, err->code, err->message_ptr, err->message_len, err->payload_ptr, + err->payload_len)); {{PREFIX}}_error_clear(err); return NULL; } @@ -288,59 +320,97 @@ JS_HELPER bool js_type_error(napi_env env, const char* expected) { return false; } -JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { - return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +JS_HELPER bool js_range_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_range_error(env, NULL, expected); + return false; } -JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { - return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +/* An integer argument of at most 32 bits: a number that is an integer in + * [lo, hi]. The range is checked on the double before any conversion, so + * nothing wraps or truncates, on every platform. */ +JS_HELPER bool js_arg_int(napi_env env, napi_value v, double lo, double hi, const char* expected, + double* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_number) return js_type_error(env, "expected a number"); + double d = 0; + napi_get_value_double(env, v, &d); + if (!(d >= lo && d <= hi) || d != floor(d)) return js_range_error(env, expected); + *out = d; + return true; } JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int8_t)t; + double d = 0; + if (!js_arg_int(env, v, -128.0, 127.0, "expected an integer in [-128, 127]", &d)) return false; + *out = (int8_t)d; return true; } JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int16_t)t; + double d = 0; + if (!js_arg_int(env, v, -32768.0, 32767.0, "expected an integer in [-32768, 32767]", &d)) { + return false; + } + *out = (int16_t)d; + return true; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + double d = 0; + if (!js_arg_int(env, v, -2147483648.0, 2147483647.0, + "expected an integer in [-2147483648, 2147483647]", &d)) { + return false; + } + *out = (int32_t)d; return true; } JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint8_t)t; + double d = 0; + if (!js_arg_int(env, v, 0.0, 255.0, "expected an integer in [0, 255]", &d)) return false; + *out = (uint8_t)d; return true; } JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint16_t)t; + double d = 0; + if (!js_arg_int(env, v, 0.0, 65535.0, "expected an integer in [0, 65535]", &d)) return false; + *out = (uint16_t)d; return true; } -/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + double d = 0; + if (!js_arg_int(env, v, 0.0, 4294967295.0, "expected an integer in [0, 4294967295]", &d)) { + return false; + } + *out = (uint32_t)d; + return true; +} + +/* 64-bit integers: a bigint that fits, or a number that is an integer in + * range. The double is range-checked before it is converted (converting + * an out-of-range double is undefined behavior in C); 2^63 and 2^64 are + * exact doubles, so the bounds are exclusive. */ JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { napi_valuetype t; napi_typeof(env, v, &t); if (t == napi_bigint) { bool lossless = false; napi_get_value_bigint_int64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return false; + return lossless || js_range_error(env, "expected a signed 64-bit integer"); } + if (t != napi_number) return js_type_error(env, "expected a bigint"); double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { - *out = (int64_t)d; - return true; + napi_get_value_double(env, v, &d); + if (!(d >= -9223372036854775808.0 && d < 9223372036854775808.0) || d != floor(d)) { + return js_range_error(env, "expected a signed 64-bit integer"); } - return js_type_error(env, "expected a bigint"); + *out = (int64_t)d; + return true; } JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { @@ -349,27 +419,42 @@ JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { if (t == napi_bigint) { bool lossless = false; napi_get_value_bigint_uint64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return false; + return lossless || js_range_error(env, "expected an unsigned 64-bit integer"); } + if (t != napi_number) return js_type_error(env, "expected a bigint"); double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && - d == (double)(uint64_t)d) { - *out = (uint64_t)d; - return true; + napi_get_value_double(env, v, &d); + if (!(d >= 0.0 && d < 18446744073709551616.0) || d != floor(d)) { + return js_range_error(env, "expected an unsigned 64-bit integer"); } - return js_type_error(env, "expected a bigint"); + *out = (uint64_t)d; + return true; } JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); } +/* f32: any number, rounded to the nearest float as Float32Array rounds it. + * Converting a double outside the float range is undefined behavior in C, + * so the overflow cases are explicit: at or past FLT_MAX plus half an ulp a + * value rounds to an infinity, and below that to FLT_MAX. */ JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + const double max = 3.4028234663852886e38; /* FLT_MAX */ + const double overflow = 3.4028235677973366e38; /* FLT_MAX + half an ulp */ double d = 0; if (!js_arg_f64(env, v, &d)) return false; - *out = (float)d; + if (d >= overflow) { + *out = HUGE_VALF; + } else if (d <= -overflow) { + *out = -HUGE_VALF; + } else if (d > max) { + *out = (float)max; + } else if (d < -max) { + *out = (float)-max; + } else { + *out = (float)d; + } return true; } @@ -377,6 +462,15 @@ JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); } +/* Whether an optional argument is present (neither null nor undefined), + * also stored in `*has`. */ +JS_HELPER bool js_present(napi_env env, napi_value v, bool* has) { + napi_valuetype t; + napi_typeof(env, v, &t); + *has = t != napi_null && t != napi_undefined; + return *has; +} + /* A handle argument; null or undefined is NULL when `nullable`. */ JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { napi_valuetype t; @@ -411,6 +505,24 @@ JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, siz return js_type_error(env, "expected a Uint8Array"); } +/* A borrowed typed array (numeric list) of `type`: its data and element + * count. */ +JS_HELPER bool js_arg_slice(napi_env env, napi_value v, napi_typedarray_type type, const void** ptr, + size_t* count) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type actual; + void* data = NULL; + napi_get_typedarray_info(env, v, &actual, count, &data, NULL, NULL); + if (actual == type) { + *ptr = data; + return true; + } + } + return js_type_error(env, "expected a typed array of the parameter's element type"); +} + /* A string argument encoded as UTF-8: short strings live in the inline * buffer, longer ones on the heap until js_str_free. */ typedef struct { @@ -444,6 +556,11 @@ JS_HELPER void js_str_free(js_str* s) { napi_value argv[(n) + 1]; \ napi_get_cb_info(env, info, &argc, argv, NULL, NULL) +/* Set `err` to `code` with a NUL-terminated message. */ +JS_HELPER void js_error_set(js_error* err, int32_t code, const char* message) { + {{PREFIX}}_error_set(err, code, (const uint8_t*)message, strlen(message)); +} + /* --------------------------------------------------------------------------- * Async calls: the completion may run on any thread, so it only records the * result and queues the settlement on the JavaScript thread through a @@ -460,9 +577,13 @@ typedef enum { JS_R_BOOL, JS_R_STR, JS_R_BYTES, - JS_R_HANDLE + JS_R_HANDLE, + JS_R_SLICE } js_kind; +/* One async call. The completion records its result here: the value (`v`, + * plus `len` for a run), whether an optional scalar is present (`opt` and + * `has`), and a typed array's type and element size. */ typedef struct { napi_deferred deferred; napi_threadsafe_function tsfn; @@ -476,6 +597,10 @@ typedef struct { const void* p; } v; size_t len; + bool opt; + bool has; + napi_typedarray_type slice_type; + size_t slice_size; } js_async; JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { @@ -484,8 +609,11 @@ JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* js_async* a = (js_async*)data; bool failed = a->err != NULL && a->err->code != 0; if (env != NULL && failed) { - napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, - a->err->payload_ptr, a->err->payload_len)); + napi_reject_deferred(env, a->deferred, + js_fault(env, a->err->code, a->err->message_ptr, a->err->message_len, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL && a->opt && !a->has) { + napi_resolve_deferred(env, a->deferred, js_null(env)); } else if (env != NULL) { napi_value v = NULL; switch (a->kind) { @@ -499,11 +627,16 @@ JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + case JS_R_SLICE: + v = js_take_slice(env, a->slice_type, a->v.p, a->len, a->slice_size); + break; } napi_resolve_deferred(env, a->deferred, v); } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { /* The environment is shutting down: just release the result. */ {{PREFIX}}_free_bytes((uint8_t*)a->v.p, a->len); + } else if (!failed && a->kind == JS_R_SLICE && a->v.p != NULL) { + {{PREFIX}}_free_bytes((uint8_t*)a->v.p, a->len * a->slice_size); } {{PREFIX}}_error_free(a->err); napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); @@ -653,7 +786,7 @@ JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, uv_mutex_unlock(&t->mu); free(req); js_thread_release(t); - {{PREFIX}}_error_set(out_err, -4, "the callback implementation is no longer reachable"); + js_error_set(out_err, -4, "the callback implementation is no longer reachable"); return; } const uint64_t limit = (uint64_t)JS_CB_DEADLOCK_MS * 1000000u; @@ -679,7 +812,7 @@ JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, "callback, so waiting longer would deadlock (call it asynchronously, or call " "back on the calling thread)", what, JS_CB_DEADLOCK_MS); - {{PREFIX}}_error_set(out_err, -4, msg); + js_error_set(out_err, -4, msg); return; } uv_cond_timedwait(&t->cv, &t->mu, limit - (now - since)); @@ -732,9 +865,11 @@ JS_HELPER char* js_cstring(napi_env env, napi_value v) { } /* Report the pending JavaScript exception (or `fallback`) through - * `out_err`; nothing unwinds through the C frame. A `$Fault` (a domain error - * a throwing method raised) reports its code, message, and payload; any - * other exception is a foreign failure, code -4, with its message. */ + * `out_err`; nothing unwinds through the C frame. A `$Fault` (the adapter's + * report of a method declared `throws`: a domain code with its payload, or + * -1) reports its code, message, and payload; any other exception (from a + * method that doesn't throw, or a return of the wrong type) is a callback + * failure, code -4, with its message. */ JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { int32_t code = -4; char* msg = NULL; @@ -770,7 +905,7 @@ JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallbac napi_is_exception_pending(env, &pending); if (pending) napi_get_and_clear_last_exception(env, &exc); } - {{PREFIX}}_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); + js_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); if (payload != NULL) {{PREFIX}}_error_set_payload(out_err, payload, payload_len); free(msg); } @@ -812,6 +947,21 @@ JS_HELPER bool js_ret_bytes(napi_env env, napi_value v, uint8_t** out_ptr, size_ return true; } +/* A callback's typed-array return: a TypedArray of `type`, copied into a + * {p}_alloc run of `count * size` bytes the producer adopts. `*run` and + * `*count` are the run and its element count (NULL and 0 when empty). */ +JS_HELPER bool js_ret_slice(napi_env env, napi_value v, napi_typedarray_type type, size_t size, + void** run, size_t* count) { + const void* data = NULL; + size_t n = 0; + if (!js_arg_slice(env, v, type, &data, &n)) return false; + uint8_t* out = {{PREFIX}}_alloc(n * size); + if (n > 0) memcpy(out, data, n * size); + *run = out; + *count = n; + return true; +} + /* --------------------------------------------------------------------------- * The runtime symbols every library exports. * ------------------------------------------------------------------------- */ diff --git a/crates/weaveffi-cli/src/targets/node/tests.rs b/crates/weaveffi-cli/src/targets/node/tests.rs index 2a1d929b..9cb79e3c 100644 --- a/crates/weaveffi-cli/src/targets/node/tests.rs +++ b/crates/weaveffi-cli/src/targets/node/tests.rs @@ -5,7 +5,7 @@ use crate::targets::js::test_api; fn files(name: &str, config: &NodeConfig) -> Vec { let model = test_api(name); - NodeGenerator.files(&model, Utf8Path::new("out"), config) + NodeGenerator::from(config.clone()).render(&model) } fn file(files: &[OutputFile], name: &str) -> String { @@ -31,6 +31,8 @@ fn the_package_is_named_after_the_identity() { "index.js", "index.d.ts", "runtime.js", + "debug.js", + "debug.d.ts", "acme_kv_node.c", "acme_kv.h", "binding.gyp", @@ -38,17 +40,34 @@ fn the_package_is_named_after_the_identity() { "README.md" ] ); - has(&file(&out, "package.json"), "\"name\": \"acme-kv\","); - has( - &file(&out, "binding.gyp"), - "\"target_name\": \"acme_kv_node\",", + let manifest: serde_json::Value = + serde_json::from_str(&file(&out, "package.json")).expect("valid package.json"); + assert_eq!(manifest["name"], "acme-kv"); + assert_eq!( + manifest["exports"]["./debug"], + serde_json::json!({ "types": "./debug.d.ts", "default": "./debug.js" }) + ); + assert_eq!( + manifest["files"], + serde_json::json!([ + "index.js", + "index.d.ts", + "runtime.js", + "debug.js", + "debug.d.ts", + "binding.gyp", + "acme_kv_node.c", + "acme_kv.h" + ]) ); - has(&file(&out, "binding.gyp"), "\"-lacme_kv\""); + let gyp = file(&out, "binding.gyp"); + has(&gyp, "\"target_name\": \"acme_kv_node\","); + has(&gyp, "\"-lacme_kv\""); has( - &file(&out, "binding.gyp"), + &gyp, "process.env.ACME_KV_LIBRARY || process.env.npm_config_acme_kv_library", ); - has(&file(&out, "binding.gyp"), "-Wl,-rpath,@loader_path"); + has(&gyp, "-Wl,-rpath,@loader_path"); has( &file(&out, "index.js"), "const $raw = $loadAddon('acme_kv_node.node');", @@ -70,124 +89,52 @@ fn the_package_is_named_after_the_identity() { } #[test] -fn index_checks_the_contract_at_load() { - let js = file(&files("kv", &NodeConfig::default()), "index.js"); - has(&js, "$raw.$setup($Fault);"); - has(&js, "const $contract = [\n ['kv', [\n"); - has(&js, "'kv.Store.get'],\n"); - has(&js, "\n$verify($raw, 'kv', 'kv', 4, $contract);\n"); +fn the_leak_counters_are_not_public_api() { + let out = files("kv", &NodeConfig::default()); + let index = file(&out, "index.js"); + assert!(!index.contains("export function __debugLive"), "{index}"); + has(&index, "$setLive((kind) => $raw.kv_debug_live(kind));"); + assert!(!file(&out, "index.d.ts").contains("debugLive")); has( - &js, - "export { KvError, CancelledError } from './runtime.js';", + &file(&out, "debug.d.ts"), + "export declare function debugLive(kind: number): bigint;", ); - assert!(!js.contains("checksum"), "{js}"); } #[test] -fn addon_exports_every_symbol_with_the_raw_convention() { +fn addon_symbols_and_slots_follow_the_lowered_signatures() { let c = file(&files("kv", &NodeConfig::default()), "kv_node.c"); assert!(!c.contains("{{"), "unsubstituted placeholder"); - assert!(!c.contains("checksum") && !c.contains("dealloc"), "{c}"); has(&c, "#include \"kv.h\""); - has(&c, "typedef kv_error js_error;"); - for symbol in [ - "kv_kv_contract", - "kv_kv_Store_new", - "kv_kv_Store_clone", - "kv_kv_Store_destroy", - "kv_kv_Store_ScanIterator_next", - "kv_kv_Store_ScanIterator_destroy", - "kv_kv_Store_wait", - "kv_kv_stats_count", - ] { - has( - &c, - &format!("{{\"{symbol}\", NULL, nx_{symbol}, NULL, NULL, NULL, napi_default, NULL}},"), - ); - } + // Optional scalars cross as a flag and a value; their absence is null. has( &c, - " const kv_contract_entry* table = kv_kv_contract(&len);\n return js_new_contract(env, table, len);", + "if (js_present(env, argv[0], &a0_has) && !js_arg_u16(env, argv[0], &a0)) goto done;", ); - // Strings cross as (ptr, len); a returned string is freed after copying. - // Every call into the library is bracketed for the callback hop. has( &c, - "js_sync_begin();\n const uint8_t* r = kv_kv_Store_get((const kv_kv_Store*)self_h, JS_STR_PTR(a0), a0.len, &out_len, &err);\n js_sync_end();", + "bool r = kv_kv_maybe(a0_has, a0, a1_has, a1, &out_value, &err);", ); - has(&c, "ret = js_take_bytes(env, r, out_len);"); - // A cancellable launcher takes the token handle after its inputs. has( &c, - "if (!js_arg_handle(env, argv[2], &token, true)) goto done;", + "ret = r ? js_new_f64(env, (double)out_value) : js_null(env);", ); - has(&c, "kv_kv_Store_wait((const kv_kv_Store*)self_h, a0, (kv_cancel_token*)token, done_kv_kv_Store_wait, a);"); + // Typed arrays cross as the matching TypedArray, freed by byte length. has( &c, - "static void done_kv_kv_Store_wait(void* context, kv_error* err, int64_t result) {", + "if (!js_arg_slice(env, argv[1], napi_biguint64_array, &a1, &a1_len)) goto done;", ); has( &c, - "js_async* a = js_async_begin(env, JS_R_HANDLE, \"kv_kv_Store_fetch\", &ret);", + "ret = js_take_slice(env, napi_float64_array, r, out_len, sizeof(double));", ); - // Every declaration precedes the first read that can fail. + // Callback trampolines take every slot of the method's signature. + has(&c, "static bool tramp_kv_kv_Policy_limit(void* ctx, bool p_has_hint, int32_t p_hint, int16_t* p_out_value, kv_error* p_out_err) {"); has( &c, - " js_cb* a0 = NULL;\n int32_t a1 = 0;\n if (!js_arg_cb(", + "if (js_ret_slice(env, result, napi_float32_array, sizeof(float), &run, &n)) {", ); - // An optional callback passes a null vtable for none. - has( - &c, - "if (!js_arg_cb(env, argv[0], \"kv_kv_Policy\", dispatch_kv_kv_Policy, true, &a0)) goto done;", - ); - has( - &c, - "kv_kv_install((void*)a0, a0 != NULL ? &vtable_kv_kv_Policy : NULL, &err);", - ); - has(&c, "NAPI_MODULE_INIT() {\n js_env_init(env);"); -} - -#[test] -fn callback_vtables_hop_to_the_javascript_thread() { - let c = file(&files("kv", &NodeConfig::default()), "kv_node.c"); - has(&c, "static bool tramp_kv_kv_Listener_on_message(void* ctx, const uint8_t* p0, size_t p1, uint64_t p2, kv_error* out_err) {"); - has(&c, " if (js_cb_on_js_thread(cb)) {"); - has( - &c, - " js_cb_hop(cb, 0, &f, out_err, \"Listener.onMessage\");", - ); - has(&c, "argv[0] = js_new_str(env, f->p0, f->p1);"); - has(&c, "argv[1] = js_new_u64(env, f->p2);"); - has(&c, "argv[2] = js_new_handle(env, f->p3);"); has(&c, "static const kv_kv_Listener_vtable vtable_kv_kv_Listener = {sizeof(kv_kv_Listener_vtable), 0, js_cb_free, tramp_kv_kv_Listener_on_message, tramp_kv_kv_Listener_on_bundle};"); - has(&c, " if (!js_cb_take(req)) return;"); -} - -#[test] -fn callback_returns_of_every_family_reach_the_producer() { - let c = file(&files("kv", &NodeConfig::default()), "kv_node.c"); - // A buffer return travels as a {p}_alloc run in the out slots. - has(&c, "static void tramp_kv_kv_Policy_admit(void* ctx, const uint8_t* p0, size_t p1, uint8_t** out_ptr, size_t* out_len, kv_error* out_err) {"); - has( - &c, - " if (!js_ret_bytes(env, result, f->out_ptr, f->out_len)) {\n js_cb_report(env, f->out_err, \"Policy.admit returned a value of the wrong type\");", - ); - has( - &c, - " if (!js_ret_str(env, result, f->out_ptr, f->out_len)) {", - ); - // Objects are returned by value; `I` must be a handle, `I?` may be null. - has(&c, "static kv_kv_Store* tramp_kv_kv_Policy_pick(void* ctx, const uint8_t* p0, size_t p1, kv_error* out_err) {"); - has( - &c, - " if (js_arg_handle(env, result, &h, false)) {\n f->result = (kv_kv_Store*)h;", - ); - has(&c, " if (js_arg_handle(env, result, &h, true)) {"); - has( - &c, - "static kv_kv_Mode tramp_kv_kv_Policy_mode(void* ctx, kv_error* out_err) {", - ); - has(&c, " f->result = (kv_kv_Mode)v;"); } #[test] @@ -200,8 +147,8 @@ fn package_ships_one_npm_package_per_node_platform() { binaries.insert(NativeBinary::new(Platform::LinuxX64, "lib/libkv.so")); binaries.insert(NativeBinary::new(Platform::Wasm32, "lib/kv.wasm")); let ctx = PackageContext::new(&binaries); - let artifacts = NodeGenerator - .package(&model, &ctx, &NodeConfig::default()) + let artifacts = NodeGenerator::from(NodeConfig::default()) + .package(&model, &ctx) .expect("node packages"); let paths: Vec<&str> = artifacts.iter().map(|a| a.path.as_str()).collect(); assert_eq!( @@ -252,12 +199,3 @@ fn package_ships_one_npm_package_per_node_platform() { ); assert_eq!(node_platform_tokens(Platform::AndroidArm64), None); } - -#[test] -fn npm_tarball_names_follow_npm_pack() { - assert_eq!(super::npm_tarball_name("kv", "1.0.0"), "kv-1.0.0.tgz"); - assert_eq!( - super::npm_tarball_name("@acme/kv", "1.0.0"), - "acme-kv-1.0.0.tgz" - ); -} diff --git a/crates/weaveffi-cli/src/targets/python/callbacks.rs b/crates/weaveffi-cli/src/targets/python/callbacks.rs index fc13625c..bcac9235 100644 --- a/crates/weaveffi-cli/src/targets/python/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/python/callbacks.rs @@ -1,19 +1,21 @@ //! Callback interfaces: the abstract base class the consumer subclasses, -//! and the ABI side the producer calls through, satisfying -//! [`weaveffi_model::plan::CallbackProtocol`]: one `CFUNCTYPE` per method, +//! and the ABI side the producer calls through: one `CFUNCTYPE` per method, //! the `ctypes.Structure` mirroring the C vtable (its `size`, `flags`, and //! `free` header first), one trampoline per method, and the single static //! vtable whose function objects are pinned at module scope. use crate::codegen::CodeWriter; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding}; -use weaveffi_model::ty::{Family, Prim, Ty}; +use weaveffi_model::abi::CType; +use weaveffi_model::model::{ + CallbackInterfaceBinding, CallbackMethodBinding, CallbackParamBinding, +}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; -use crate::targets::python::calls::{adopt_expr, cfunctype, slot_params}; +use crate::targets::python::calls::{adopt_expr, cfunctype, direct_value, slot_params}; use crate::targets::python::codec::{decode_expr, encode_stmts}; -use crate::targets::python::docs::{fn_docstring, with_deprecation}; +use crate::targets::python::docs::{fn_docstring, ParamDoc}; use crate::targets::python::types::{ - py_local, py_member_name, py_name, py_return_hint, py_slot_hint, py_type_hint, + int_checker, prim_kind, py_local, py_member_name, py_name, py_slot_hint, py_type_hint, Dir, }; use crate::targets::python::Gen; @@ -22,74 +24,66 @@ use crate::targets::python::Gen; /// /// Each trampoline looks its implementation up by the integer `ctx`, /// converts the arguments, calls the method, and hands the return back -/// through the C return or the `out_ptr`/`out_len` slots. An exception is -/// reported through `out_err` (as a domain code when the method declares -/// errors and raised one, else `-4`) and nothing unwinds through the C -/// frame. `ctypes` acquires the GIL on entry, so the producer may call from -/// any thread. +/// through the C return or the out slots. An exception is reported through +/// `out_err` (see [`fail_call`]) and nothing unwinds through the C frame. +/// `ctypes` acquires the GIL on entry, so the producer may call from any +/// thread; the vtable's `flags` are 0 (not thread-affine). pub(crate) fn render_callback_interface( w: &mut CodeWriter, g: &Gen<'_>, cb: &CallbackInterfaceBinding, - domain: Option<&str>, ) { let name = &cb.name; w.blank().blank(); w.line(format!("class {name}(abc.ABC):")); w.scope(|w| { - let mut usage = format!( + let usage = format!( "Subclass it and implement every method to pass an instance where the\n\ API takes a `{name}`. The producer may call the methods from any\n\ - thread until it releases the instance." + thread until it releases the instance. An exception a method raises\n\ + reaches the producer as a failure of that call (a method that throws\n\ + an error domain reports that domain's codes with their fields)." ); - if let Some(domain) = domain.filter(|_| cb.methods.iter().any(|m| m.throws)) { - usage.push_str(&format!( - " A method that declares errors may\n\ - raise a `{domain}` code, which reaches the producer with its fields;\n\ - any other exception reaches it as {}.FOREIGN_ERROR_CODE (-4).", - g.root_error - )); - } else { - usage.push_str(&format!( - " An exception a method raises\n\ - reaches the producer as {}.FOREIGN_ERROR_CODE (-4).", - g.root_error - )); - } - let doc = with_deprecation(cb.doc.as_deref(), cb.deprecated.as_deref()); - let doc = match doc { + let doc = match g.doc(&cb.doc, &cb.deprecated) { Some(d) => format!("{d}\n\n{usage}"), None => usage, }; - fn_docstring(w, Some(&doc), &[], None); + fn_docstring(w, Some(&doc), &[], &[]); for m in &cb.methods { let mut sig: Vec = vec!["self".into()]; sig.extend( m.params .iter() - .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty))), + .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty, Dir::Out))), ); + let ret = m + .ret + .as_ref() + .map_or_else(|| "None".into(), |t| py_type_hint(t, Dir::In)); w.blank(); w.line("@abc.abstractmethod"); w.line(format!( - "def {}({}) -> {}:", + "def {}({}) -> {ret}:", py_member_name(&m.name), sig.join(", "), - py_return_hint(m.ret.as_ref()) )); w.scope(|w| { - let doc = with_deprecation(m.doc.as_deref(), m.deprecated.as_deref()); - let raises = domain.filter(|_| m.throws).map(|d| { - ( - d, - "To report one of the domain's error codes to the producer.", - ) - }); - if doc.is_none() && raises.is_none() && m.params.iter().all(|p| p.doc.is_none()) { + let doc = g.doc(&m.doc, &m.deprecated); + let raises = callback_raises(g, &m.error); + let params: Vec = m + .params + .iter() + .map(|p| ParamDoc { + name: py_name(&p.name), + hint: py_type_hint(&p.ty, Dir::Out), + doc: g.text(&p.doc), + }) + .collect(); + if doc.is_none() && raises.is_empty() && params.iter().all(|p| p.doc.is_none()) { w.line("..."); } else { - fn_docstring(w, doc.as_deref(), &m.params, raises); + fn_docstring(w, doc.as_deref(), ¶ms, &raises); } }); } @@ -103,7 +97,7 @@ pub(crate) fn render_callback_interface( w.line(format!( "_{name}_{}_t = {}", m.name, - cfunctype(&m.abi_ret, &m.abi_params) + cfunctype(&m.abi.ret, &m.abi.params) )); } w.blank().blank(); @@ -124,7 +118,7 @@ pub(crate) fn render_callback_interface( }); for m in &cb.methods { - render_trampoline(w, cb, m, domain.filter(|_| m.throws)); + render_trampoline(w, g, cb, m); } // The one static vtable. Each field keeps its function object alive, and @@ -133,7 +127,7 @@ pub(crate) fn render_callback_interface( w.line(format!("_{name}_vtable = _{name}Vtable(")); w.scope(|w| { w.line(format!("ctypes.sizeof(_{name}Vtable),")); - w.line("0,"); + w.line("0, # flags: callable from any thread"); w.line("_callback_free_fn,"); for m in &cb.methods { w.line(format!("_{name}_{0}_t(_{name}_{0}),", m.name)); @@ -145,63 +139,104 @@ pub(crate) fn render_callback_interface( )); } +/// The `Raises` entries documenting what an implementation may raise to +/// report a typed failure. +fn callback_raises(g: &Gen<'_>, error: &ErrorStrategy) -> Vec<(String, String)> { + match error { + ErrorStrategy::Domain(name) => vec![( + g.domain_class(name).to_string(), + "To report one of the domain's codes, with its fields, to the producer.".into(), + )], + ErrorStrategy::Untyped | ErrorStrategy::Trap => vec![], + } +} + /// The expression converting one trampoline parameter's C slots into the -/// value handed to the implementation. Strings, bytes, and buffers are -/// borrowed for the call and copied; an object transfers one strong -/// reference, which a new wrapper adopts. -fn trampoline_arg(p: &ParamBinding) -> String { - let data = || { - format!( - "_peek_bytes({}, {})", - py_local(&format!("{}_ptr", p.name)), - py_local(&format!("{}_len", p.name)) - ) - }; - let n = py_local(&p.name); - match p.ty.family() { - Family::String => format!("{}.decode(\"utf-8\")", data()), - Family::Bytes => data(), - Family::Buffer => decode_expr(&data(), &p.ty), - Family::Object { nullable } => adopt_expr(&p.ty, &n, nullable), - Family::Direct => match &p.ty { - Ty::Enum(name) => format!("{name}({n})"), - _ => n, - }, - Family::Callback { .. } | Family::Iterator => { - unreachable!("callback interfaces and iterators are never callback arguments") +/// value handed to the implementation. Strings, bytes, buffers, and typed +/// arrays are borrowed for the call and copied; an object transfers one +/// strong reference, which a new wrapper adopts. +fn trampoline_arg(p: &CallbackParamBinding) -> String { + let local = |slot: &weaveffi_model::abi::AbiParam| py_local(&slot.name); + match &p.pass { + ArgPass::Direct { slot } => direct_value(&local(slot), &slot.ty), + ArgPass::OptDirect { has, value, .. } => format!( + "{} if {} else None", + direct_value(&local(value), &value.ty), + local(has) + ), + ArgPass::Slice { ptr, len, elem } => format!( + "_peek_array({}, {}, \"{}\")", + local(ptr), + local(len), + prim_kind(*elem) + ), + ArgPass::String { ptr, len } => { + format!( + "_peek_bytes({}, {}).decode(\"utf-8\")", + local(ptr), + local(len) + ) + } + ArgPass::Bytes { ptr, len } => format!("_peek_bytes({}, {})", local(ptr), local(len)), + ArgPass::Buffer { ptr, len } => decode_expr( + &format!("_peek_bytes({}, {})", local(ptr), local(len)), + &p.ty, + ), + ArgPass::Object { + slot, + nullable, + interface, + } => adopt_expr(interface, &local(slot), *nullable), + ArgPass::Callback { .. } => { + unreachable!("validation rejects a callback as a callback method parameter") } } } -/// `(coercion, default)` for a direct return: the conversion applied to the -/// implementation's result (inside the `try`, so a wrong type is reported -/// like any other failure), and the value returned after a failure. -fn direct_return(ty: &Ty) -> (&'static str, &'static str) { - match ty { - Ty::Prim(Prim::Bool) => ("bool", "False"), - Ty::Prim(Prim::F32 | Prim::F64) => ("float", "0.0"), - // Integers and C-style enums (an `IntEnum` is an `int`). - _ => ("int", "0"), +/// The statement reporting the caught exception `exc` through `out_err`: +/// a method that throws a domain reports an exception of that domain as its +/// code with its fields, and anything else as code -1 with the message +/// (`throws` a domain or `any`) or -4 (no `throws`). +fn fail_call(g: &Gen<'_>, error: &ErrorStrategy) -> String { + match error { + ErrorStrategy::Trap => "_callback_fail(out_err, exc, _FOREIGN)".into(), + ErrorStrategy::Untyped => "_callback_fail(out_err, exc, _GENERIC)".into(), + ErrorStrategy::Domain(name) => format!( + "_callback_fail(out_err, exc, _GENERIC, {})", + g.domain_class(name) + ), + } +} + +/// The expression checking a direct value `expr` an implementation returned +/// for a slot of C type `ty`, inside the trampoline's `try` (so a wrong type +/// or an out-of-range integer is reported like any other failure, instead +/// of `ctypes` truncating it or failing after the trampoline returned). +fn returned_scalar(expr: &str, ty: &CType, what: &str) -> String { + match (int_checker(ty), ty) { + (Some(check), _) => format!("{check}({expr}, \"{what}\")"), + (None, CType::Bool) => format!("bool({expr})"), + (None, _) => format!("_float({expr}, \"{what}\")"), } } /// Render the trampoline for one callback method: a `def` whose parameters /// are the vtable entry's C slots (`ctx`, the parameter slots, the return's -/// out slots, `out_err`). `domain` names the error domain whose codes the -/// method may report (only for a method that declares errors). +/// out slots, `out_err`). fn render_trampoline( w: &mut CodeWriter, + g: &Gen<'_>, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding, - domain: Option<&str>, ) { + let what = format!("{}.{}() result", cb.name, py_member_name(&m.name)); w.blank().blank(); w.line(format!( "def _{}_{}({}) -> {}:", cb.name, m.name, - slot_params(&m.abi_params), - py_slot_hint(&m.abi_ret) + slot_params(&m.abi.params), + py_slot_hint(&m.abi.ret) )); w.scope(|w| { w.line("try:"); @@ -227,57 +262,98 @@ fn render_trampoline( py_member_name(&m.name), args.join(", ") ); - let Some(ty) = &m.ret else { - w.line(call); - return; - }; - if ty.family() == Family::Direct { - w.line(format!("return {}({call})", direct_return(ty).0)); - return; - } - w.line(format!("_ret = {call}")); - match ty.family() { - Family::Object { nullable } => { - let class = ty.interface_name().expect("object names an interface"); - let helper = if nullable { - "_callback_return_object_opt" - } else { - "_callback_return_object" + match &m.ret_pass { + CallbackRetPass::Void => { + w.line(call); + } + CallbackRetPass::Direct => { + w.line(format!( + "return {}", + returned_scalar(&call, &m.abi.ret, &what) + )); + } + CallbackRetPass::OptDirect { out_value } => { + let pointee = match &out_value.ty { + CType::Ptr { pointee, .. } => pointee.as_ref(), + other => other, }; - w.line(format!("return {helper}(_ret, {class})")); + w.line(format!("_ret = {call}")); + w.line("if _ret is None:"); + w.scope(|w| { + w.line("return False"); + }); + w.line(format!( + "{}[0] = {}", + py_local(&out_value.name), + returned_scalar("_ret", pointee, &what) + )); + w.line("return True"); + } + CallbackRetPass::Slice { + out_ptr, + out_len, + elem, + } => { + w.line(format!( + "_callback_return_array({}, {}, {call}, \"{}\", \"{what}\")", + py_local(&out_ptr.name), + py_local(&out_len.name), + prim_kind(*elem) + )); } - Family::String => { - w.line("_callback_return_bytes(out_ptr, out_len, _ret.encode(\"utf-8\"))"); + CallbackRetPass::String { out_ptr, out_len } => { + w.line(format!( + "_callback_return_bytes({}, {}, {call}.encode(\"utf-8\"))", + py_local(&out_ptr.name), + py_local(&out_len.name) + )); } - Family::Bytes => { - w.line("_callback_return_bytes(out_ptr, out_len, bytes(_ret))"); + CallbackRetPass::Bytes { out_ptr, out_len } => { + w.line(format!( + "_callback_return_bytes({}, {}, bytes({call}))", + py_local(&out_ptr.name), + py_local(&out_len.name) + )); } - Family::Buffer => { + CallbackRetPass::Buffer { out_ptr, out_len } => { + let ty = m.ret.as_ref().expect("a buffered return has a type"); + w.line(format!("_ret = {call}")); for line in encode_stmts("_w", "_ret", ty) { w.line(line); } - w.line("_callback_return_bytes(out_ptr, out_len, _w.finish())"); + w.line(format!( + "_callback_return_bytes({}, {}, _w.finish())", + py_local(&out_ptr.name), + py_local(&out_len.name) + )); } - Family::Direct => unreachable!("returned above"), - Family::Callback { .. } | Family::Iterator => { - unreachable!("validation rejects {ty} as a callback return") + CallbackRetPass::Object { + nullable, + interface, + .. + } => { + let helper = if *nullable { + "_callback_return_object_opt" + } else { + "_callback_return_object" + }; + w.line(format!("return {helper}({call}, {interface})")); } } }); w.line("except BaseException as exc:"); w.scope(|w| { - match domain { - Some(domain) => w.line(format!("_callback_fail(out_err, exc, {domain})")), - None => w.line("_callback_fail(out_err, exc)"), + w.line(fail_call(g, &m.error)); + // The C return after a failure: the producer ignores it. + let fallback = match &m.abi.ret { + CType::Void => None, + CType::Bool => Some("False"), + CType::Float | CType::Double => Some("0.0"), + ty if py_slot_hint(ty) == "int" => Some("0"), + _ => Some("None"), }; - match m.ret.as_ref().map(|ty| (ty, ty.family())) { - Some((ty, Family::Direct)) => { - w.line(format!("return {}", direct_return(ty).1)); - } - Some((_, Family::Object { .. })) => { - w.line("return None"); - } - _ => {} + if let Some(value) = fallback { + w.line(format!("return {value}")); } }); }); diff --git a/crates/weaveffi-cli/src/targets/python/calls.rs b/crates/weaveffi-cli/src/targets/python/calls.rs index f0a6f3b8..0e8039e4 100644 --- a/crates/weaveffi-cli/src/targets/python/calls.rs +++ b/crates/weaveffi-cli/src/targets/python/calls.rs @@ -3,24 +3,24 @@ //! //! Every C prototype is bound once, at import time, from the model's lowered //! [`AbiFn`] (`argtypes` and `restype` come straight from its slots). -//! Marshalling dispatch is driven by the shared plans: [`ArgPass`] decides -//! how each parameter crosses and [`Family`] how a result is received, so -//! this module never re-derives those shapes. +//! Marshalling is driven by the stored passing contracts: [`ArgPass`] +//! decides how each parameter crosses, [`RetPass`], [`ResultPass`], and +//! [`ItemPass`] how a sync return, an async result, and an iterator item are +//! received, and [`ErrorStrategy`](weaveffi_model::plan::ErrorStrategy) +//! what a failure raises, so this module never re-derives those shapes. use crate::codegen::CodeWriter; use weaveffi_model::abi::{AbiParam, CType}; -use weaveffi_model::model::{ - AbiFn, CallShape, ErrorBinding, FnBinding, IteratorBinding, ModuleBinding, ParamBinding, -}; -use weaveffi_model::plan::ArgPass; -use weaveffi_model::ty::{Family, Ty}; +use weaveffi_model::model::{AbiFn, AsyncBinding, FnBinding, IteratorBinding}; +use weaveffi_model::plan::{ArgPass, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{Prim, RetTy, Ty}; use crate::targets::python::codec::{decode_expr, encode_stmts}; -use crate::targets::python::docs::{fn_docstring, with_deprecation}; -use crate::targets::python::entities::py_raise_factory; +use crate::targets::python::docs::{fn_docstring, ParamDoc}; use crate::targets::python::types::{ - py_binding_name, py_ctype, py_local, py_member_name, py_name, py_object_member, py_return_hint, - py_slot_hint, py_str_literal, py_type_hint, Slot, + enum_class, int_checker, prim_kind, py_binding_name, py_ctype, py_local, py_member_name, + py_name, py_object_member, py_out_local, py_param_hint, py_restype, py_return_hint, + py_slot_hint, py_str_literal, py_type_hint, Dir, }; use crate::targets::python::Gen; @@ -40,26 +40,14 @@ pub(crate) enum FnScope { Init, } -/// The lowered C function a callable's wrapper invokes. -fn launcher(f: &FnBinding) -> &AbiFn { - match &f.shape { - CallShape::Sync(abi) => abi, - CallShape::Async(a) => &a.launch, - CallShape::Iterator(it) => &it.launch, - } -} - /// `_bind("symbol", restype, argtypes...)` for one lowered function. The /// async launcher's `callback` slot is typed with the completion's /// `CFUNCTYPE` (`callback_type`). fn bind_line(g: &Gen<'_>, abi: &AbiFn, callback_type: Option<&str>) -> String { - let mut parts = vec![ - format!("\"{}\"", abi.symbol), - py_ctype(&abi.ret, Slot::Recv), - ]; - parts.extend(abi.params.iter().map(|p| match (&p.ty, callback_type) { - (CType::Named(_), Some(cb)) if p.name == "callback" => cb.to_string(), - _ => py_ctype(&p.ty, Slot::Arg), + let mut parts = vec![format!("\"{}\"", abi.symbol), py_restype(&abi.ret)]; + parts.extend(abi.params.iter().map(|p| match callback_type { + Some(cb) if p.name == "callback" => cb.to_string(), + _ => py_ctype(&p.ty, true), })); format!( "{} = _bind({})", @@ -68,10 +56,11 @@ fn bind_line(g: &Gen<'_>, abi: &AbiFn, callback_type: Option<&str>) -> String { ) } -/// The `CFUNCTYPE(...)` spelling for a C function-pointer signature. +/// The `CFUNCTYPE(...)` spelling for a C function-pointer signature the +/// producer calls (a vtable method or an async completion). pub(crate) fn cfunctype(ret: &CType, params: &[AbiParam]) -> String { - let parts: Vec = std::iter::once(py_ctype(ret, Slot::Recv)) - .chain(params.iter().map(|p| py_ctype(&p.ty, Slot::Recv))) + let parts: Vec = std::iter::once(py_restype(ret)) + .chain(params.iter().map(|p| py_ctype(&p.ty, false))) .collect(); format!("ctypes.CFUNCTYPE({})", parts.join(", ")) } @@ -89,40 +78,85 @@ pub(crate) fn slot_params(params: &[AbiParam]) -> String { /// The module-level stem of an async function's completion objects: the /// launcher's binding name without `_c`. fn completion_stem(g: &Gen<'_>, f: &FnBinding) -> String { - let binding = py_binding_name(&launcher(f).symbol, g.prefix); + let binding = py_binding_name(&f.abi.symbol, g.prefix); binding.strip_prefix("_c").unwrap_or(&binding).to_string() } -/// The module-level iterator class for an iterator-returning callable: -/// `_` plus the iterator tag without its prefix. -fn iterator_class(g: &Gen<'_>, it: &IteratorBinding) -> String { - py_binding_name(&it.iter_tag, g.prefix).replacen("_c_", "_", 1) +/// The module-level function pulling one element of an iterator: +/// `_pull_` plus the iterator tag without its prefix. +fn pull_fn(g: &Gen<'_>, it: &IteratorBinding) -> String { + py_binding_name(&it.iter_tag, g.prefix).replacen("_c_", "_pull_", 1) } -/// The expression receiving one producer-owned value of `ty` (a sync -/// return, an async result, or an iterator element) from its pointer (or -/// direct value) expression `p` and length expression `len`, releasing what -/// it owes. -fn recv_expr(ty: &Ty, p: &str, len: &str) -> String { - match ty.family() { - Family::String => format!("_take_str({p}, {len})"), - Family::Bytes => format!("_take_bytes({p}, {len})"), - Family::Buffer => decode_expr(&format!("_take_bytes({p}, {len})"), ty), - Family::Object { nullable } => adopt_expr(ty, p, nullable), - Family::Direct => match ty { - Ty::Enum(name) => format!("{name}({p})"), - _ => p.to_string(), - }, - Family::Callback { .. } | Family::Iterator => { - unreachable!("callback interfaces and iterators are never received as values") +/// The value expression `expr` of a direct slot of C type `ty` as Python +/// hands it out: re-wrapped in its `IntEnum` for a C-style enum. +pub(crate) fn direct_value(expr: &str, ty: &CType) -> String { + match enum_class(ty) { + Some(class) => format!("{class}({expr})"), + None => expr.to_string(), + } +} + +/// One value the producer hands over (a sync return, an async result, or +/// an iterator item) as the expressions that hold its slots. +pub(crate) enum Recv<'a> { + /// A number, `bool`, or enum, by value. + Direct { value: String, ty: &'a CType }, + /// A present flag and a value (ignored when absent). + OptDirect { + has: String, + value: String, + ty: &'a CType, + }, + /// An owned typed-array run: address and element count. + Slice { + ptr: String, + len: String, + elem: Prim, + }, + /// An owned UTF-8 run. + String { ptr: String, len: String }, + /// An owned byte run. + Bytes { ptr: String, len: String }, + /// An owned value buffer of type `ty`. + Buffer { + ptr: String, + len: String, + ty: &'a Ty, + }, + /// One strong reference to an object. + Object { + ptr: String, + nullable: bool, + interface: &'a str, + }, +} + +/// The expression taking ownership of a received value: copying it into a +/// Python value and releasing what the producer handed over. +pub(crate) fn recv_expr(r: &Recv<'_>) -> String { + match r { + Recv::Direct { value, ty } => direct_value(value, ty), + Recv::OptDirect { has, value, ty } => { + format!("{} if {has} else None", direct_value(value, ty)) + } + Recv::Slice { ptr, len, elem } => { + format!("_take_array({ptr}, {len}, \"{}\")", prim_kind(*elem)) } + Recv::String { ptr, len } => format!("_take_str({ptr}, {len})"), + Recv::Bytes { ptr, len } => format!("_take_bytes({ptr}, {len})"), + Recv::Buffer { ptr, len, ty } => decode_expr(&format!("_take_bytes({ptr}, {len})"), ty), + Recv::Object { + ptr, + nullable, + interface, + } => adopt_expr(interface, ptr, *nullable), } } /// The expression adopting the object pointer `p` (one strong reference) -/// into a new wrapper of `ty`'s interface; `None` for a null `I?`. -pub(crate) fn adopt_expr(ty: &Ty, p: &str, nullable: bool) -> String { - let class = ty.interface_name().expect("object names an interface"); +/// into a new wrapper of `class`; `None` for a null `I?`. +pub(crate) fn adopt_expr(class: &str, p: &str, nullable: bool) -> String { if nullable { format!("{class}._adopt({p}) if {p} else None") } else { @@ -130,151 +164,276 @@ pub(crate) fn adopt_expr(ty: &Ty, p: &str, nullable: bool) -> String { } } +/// The local holding an out slot's value: `_` plus the slot name +/// (`_out_len`, `_out_value`, `_out_item`). +fn out_local(slot: &AbiParam) -> String { + format!("_{}", slot.name) +} + +/// How an async completion's result slots become the awaited value. +fn result_recv<'a>(result: &'a ResultPass, elem: Option<&'a Ty>) -> Option> { + let local = |slot: &AbiParam| py_local(&slot.name); + Some(match result { + ResultPass::Void => return None, + ResultPass::Direct { result } => Recv::Direct { + value: local(result), + ty: &result.ty, + }, + ResultPass::OptDirect { has, value } => Recv::OptDirect { + has: local(has), + value: local(value), + ty: &value.ty, + }, + ResultPass::Slice { ptr, len, elem } => Recv::Slice { + ptr: local(ptr), + len: local(len), + elem: *elem, + }, + ResultPass::String { ptr, len } => Recv::String { + ptr: local(ptr), + len: local(len), + }, + ResultPass::Bytes { ptr, len } => Recv::Bytes { + ptr: local(ptr), + len: local(len), + }, + ResultPass::Buffer { ptr, len } => Recv::Buffer { + ptr: local(ptr), + len: local(len), + ty: elem.expect("a buffered result has a value type"), + }, + ResultPass::Object { + result, + nullable, + interface, + .. + } => Recv::Object { + ptr: local(result), + nullable: *nullable, + interface, + }, + }) +} + +/// How an iterator's `_next` out slots become the yielded item. The slots +/// are locals named by [`out_local`], read through `.value`. +fn item_recv<'a>(item: &'a ItemPass, elem: &'a Ty) -> Recv<'a> { + let value = |slot: &AbiParam| format!("{}.value", out_local(slot)); + let pointee = |slot: &'a AbiParam| match &slot.ty { + CType::Ptr { pointee, .. } => pointee.as_ref(), + other => other, + }; + match item { + ItemPass::Direct { out_item } => Recv::Direct { + value: value(out_item), + ty: pointee(out_item), + }, + ItemPass::OptDirect { out_has, out_item } => Recv::OptDirect { + has: value(out_has), + value: value(out_item), + ty: pointee(out_item), + }, + ItemPass::Slice { + out_item, + out_len, + elem, + } => Recv::Slice { + ptr: value(out_item), + len: value(out_len), + elem: *elem, + }, + ItemPass::String { out_item, out_len } => Recv::String { + ptr: value(out_item), + len: value(out_len), + }, + ItemPass::Bytes { out_item, out_len } => Recv::Bytes { + ptr: value(out_item), + len: value(out_len), + }, + ItemPass::Buffer { out_item, out_len } => Recv::Buffer { + ptr: value(out_item), + len: value(out_len), + ty: elem, + }, + ItemPass::Object { + out_item, + nullable, + interface, + .. + } => Recv::Object { + ptr: value(out_item), + nullable: *nullable, + interface, + }, + } +} + +/// How a sync call's C return (`_ret`) and out slots become the returned +/// value; `None` for a void call or an iterator (handled by the caller). +fn ret_recv<'a>(ret_pass: &'a RetPass, ret: Option<&'a RetTy>, abi: &'a AbiFn) -> Option> { + let value = |slot: &AbiParam| format!("{}.value", out_local(slot)); + let ret_ptr = || "_ret".to_string(); + Some(match ret_pass { + RetPass::Void | RetPass::Iterator(_) => return None, + RetPass::Direct => Recv::Direct { + value: ret_ptr(), + ty: &abi.ret, + }, + RetPass::OptDirect { out_value } => Recv::OptDirect { + has: ret_ptr(), + value: value(out_value), + ty: match &out_value.ty { + CType::Ptr { pointee, .. } => pointee, + other => other, + }, + }, + RetPass::Slice { out_len, elem } => Recv::Slice { + ptr: ret_ptr(), + len: value(out_len), + elem: *elem, + }, + RetPass::String { out_len } => Recv::String { + ptr: ret_ptr(), + len: value(out_len), + }, + RetPass::Bytes { out_len } => Recv::Bytes { + ptr: ret_ptr(), + len: value(out_len), + }, + RetPass::Buffer { out_len } => Recv::Buffer { + ptr: ret_ptr(), + len: value(out_len), + ty: ret + .and_then(RetTy::value) + .expect("a buffered return has a value type"), + }, + RetPass::Object { + nullable, + interface, + .. + } => Recv::Object { + ptr: ret_ptr(), + nullable: *nullable, + interface, + }, + }) +} + /// Emit every module-level piece a callable needs ahead of its wrapper: the -/// bound launcher, plus the iterator class (iterator shape) or the static -/// completion trampoline (async shape). `owner` names the callable in docs -/// (`Store.get` for a member); `packed` appends a sync member's binding to -/// the block the previous member started, with no blank lines between. -pub(crate) fn render_bindings( - w: &mut CodeWriter, - g: &Gen<'_>, - f: &FnBinding, - error: Option<&ErrorBinding>, - owner: &str, - packed: bool, -) { - match &f.shape { - CallShape::Sync(abi) => { - if !packed { - w.blank().blank(); - } - w.line(bind_line(g, abi, None)); - } - CallShape::Iterator(it) => { +/// bound entry point, plus the iterator's bound `_next`/`_destroy` and its +/// pull function (iterator returns) or the static completion trampoline +/// (async calls). `packed` appends a plain sync member's binding to the +/// block the previous member started, with no blank lines between. +pub(crate) fn render_bindings(w: &mut CodeWriter, g: &Gen<'_>, f: &FnBinding, packed: bool) { + if let Some(a) = f.async_binding() { + render_completion(w, g, f, a); + return; + } + let Some(it) = f.iterator() else { + if !packed { w.blank().blank(); - w.line(bind_line(g, &it.launch, None)); - render_iterator_class(w, g, f, it, error, owner); } - CallShape::Async(a) => { - let stem = completion_stem(g, f); - let cb_type = py_binding_name(&a.callback_type, g.prefix); - let factory = py_raise_factory(f, error); - let value = match &f.ret { - None => "None".to_string(), - Some(ty) => match ty.family() { - Family::String | Family::Bytes | Family::Buffer => { - recv_expr(ty, "result_ptr", "result_len") - } - _ => recv_expr(ty, "result", ""), - }, - }; - w.blank().blank(); + w.line(bind_line(g, &f.abi, None)); + return; + }; + w.blank().blank(); + w.line(bind_line(g, &f.abi, None)); + w.line(bind_line(g, &it.next, None)); + w.line(format!( + "{} = _bind(\"{}\", None, ctypes.c_void_p)", + py_binding_name(&it.destroy_symbol, g.prefix), + it.destroy_symbol + )); + render_pull(w, g, f, it); +} + +/// Render the module-level function pulling one item of an iterator: one +/// producer `_next` call, the item copied out and released, and +/// `StopIteration` once the stream ends. The runtime's `NativeIterator` +/// calls it for each step and destroys the handle exactly once. +fn render_pull(w: &mut CodeWriter, g: &Gen<'_>, f: &FnBinding, it: &IteratorBinding) { + let next = py_binding_name(&it.next.symbol, g.prefix); + w.blank().blank(); + w.line(format!( + "def {}(_p: int) -> {}:", + pull_fn(g, it), + py_type_hint(&it.elem, Dir::Out) + )); + w.scope(|w| { + let mut args = vec!["_p".to_string()]; + for slot in it.item.slots() { w.line(format!( - "{cb_type} = {}", - cfunctype(&CType::Void, &a.callback_params) + "{} = {}()", + out_local(slot), + py_out_local(&slot.ty) )); - w.line(bind_line(g, &a.launch, Some(&cb_type))); - w.blank().blank(); + args.push(format!("ctypes.byref({})", out_local(slot))); + } + w.line("_err = _ErrorStruct()"); + args.push("ctypes.byref(_err)".into()); + w.line(format!("_more = {next}({})", args.join(", "))); + w.line("if _err.code:"); + w.scope(|w| { w.line(format!( - "def {stem}_complete({}) -> None:", - slot_params(&a.callback_params) + "raise {}(*_read_error(_err))", + g.raise_factory(&f.error) )); - w.scope(|w| { - w.line("# Runs once, on a producer thread: take ownership of the result"); - w.line("# here, then hand it to the awaiting event loop."); - w.line("try:"); - w.scope(|w| { - w.line("if err:"); - w.scope(|w| { - w.line(format!( - "_async_settle(context, _async_error(err, {factory}), None)" - )); - }); - w.line("else:"); - w.scope(|w| { - w.line(format!("_async_settle(context, None, {value})")); - }); - }); - w.line("except BaseException as exc:"); - w.scope(|w| { - w.line("_async_settle(context, exc, None)"); - }); - }); - w.blank().blank(); - w.line(format!("{stem}_completion = {cb_type}({stem}_complete)")); - } - } + }); + w.line("if not _more:"); + w.scope(|w| { + w.line("raise StopIteration"); + }); + w.line(format!( + "return {}", + recv_expr(&item_recv(&it.item, &it.elem)) + )); + }); } -/// Render the module-level class for one iterator-returning callable, -/// satisfying the pull contract of -/// [`weaveffi_model::plan::IteratorProtocol`]: one producer `next` call per -/// `__next__`, per-element releases after copying, and exactly one -/// `destroy` (on exhaustion, `close()`, or garbage collection). -fn render_iterator_class( - w: &mut CodeWriter, - g: &Gen<'_>, - f: &FnBinding, - it: &IteratorBinding, - error: Option<&ErrorBinding>, - owner: &str, -) { - let class = iterator_class(g, it); - let next = py_binding_name(&it.next.symbol, g.prefix); - let destroy = py_binding_name(&it.destroy_symbol, g.prefix); - let factory = py_raise_factory(f, error); - let item_ty = py_ctype(it.item_ctype(), Slot::Recv); - let has_len = it.next.params.iter().any(|p| p.name == "out_len"); - - w.line(bind_line(g, &it.next, None)); +/// Render an async call's bindings: the completion's `CFUNCTYPE`, the bound +/// launcher, and the one static completion trampoline, which takes +/// ownership of the result on the producer's thread and settles the +/// awaiting future. +fn render_completion(w: &mut CodeWriter, g: &Gen<'_>, f: &FnBinding, a: &AsyncBinding) { + let stem = completion_stem(g, f); + let cb_type = py_binding_name(&a.callback_type, g.prefix); + let elem = f.ret.as_ref().and_then(RetTy::value); + let value = result_recv(&a.result, elem).map_or_else(|| "None".into(), |r| recv_expr(&r)); + w.blank().blank(); w.line(format!( - "{destroy} = _bind(\"{}\", None, ctypes.c_void_p)", - it.destroy_symbol + "{cb_type} = {}", + cfunctype(&CType::Void, &a.callback_params) )); + w.line(bind_line(g, &f.abi, Some(&cb_type))); w.blank().blank(); - w.line(format!("class {class}(_Iterator):")); + w.line(format!( + "def {stem}_complete({}) -> None:", + slot_params(&a.callback_params) + )); w.scope(|w| { - w.line(format!( - "\"\"\"The lazy iterator `{owner}` returns: each step pulls one element" - )); - w.line("from the producer.\"\"\""); - w.blank(); - w.line(format!("_destroy = staticmethod({destroy})")); - w.blank(); - w.line(format!("def __next__(self) -> {}:", py_type_hint(&it.elem))); + w.line("# Runs once, on a producer thread: take ownership of the result"); + w.line("# here, then hand it to the awaiting event loop."); + w.line("try:"); w.scope(|w| { - w.line(format!("_item = {item_ty}()")); - let mut args = vec!["_p".to_string(), "ctypes.byref(_item)".into()]; - if has_len { - w.line("_len = ctypes.c_size_t()"); - args.push("ctypes.byref(_len)".into()); - } - args.push("ctypes.byref(_err)".into()); - w.line("_err = _ErrorStruct()"); - w.line("_p = self._step()"); - w.line("try:"); - w.scope(|w| { - w.line(format!("_more = {next}({})", args.join(", "))); - }); - w.line("finally:"); + w.line("if err:"); w.scope(|w| { - w.line("self._release()"); - }); - w.line("if _err.code:"); - w.scope(|w| { - w.line(format!("raise {factory}(*_read_error(_err))")); + w.line(format!( + "_async_settle(context, _async_error(err, {}), None)", + g.raise_factory(&f.error) + )); }); - w.line("if not _more:"); + w.line("else:"); w.scope(|w| { - w.line("self.close()"); - w.line("raise StopIteration"); + w.line(format!("_async_settle(context, None, {value})")); }); - w.line(format!( - "return {}", - recv_expr(&it.elem, "_item.value", "_len.value") - )); + }); + w.line("except BaseException as exc:"); + w.scope(|w| { + w.line("_async_settle(context, exc, None)"); }); }); + w.blank().blank(); + w.line(format!("{stem}_completion = {cb_type}({stem}_complete)")); } /// The emitted Python name for a callable in `scope`. @@ -296,9 +455,14 @@ struct Lend { } /// One parameter's local preparation and its C argument expressions. +#[derive(Default)] struct ParamPlan { - /// Statements run before any object is lent (encoding). + /// Statements run before any object is lent (checking and encoding). prep: Vec, + /// Range checks: `(local, expression)`, inlined into the argument list + /// when nothing is lent or registered before the call, else bound to the + /// local in `prep` (a failing check must not strand a registration). + checks: Vec<(String, String)>, /// Statements run inside the innermost borrow scope, just before the /// call (finishing value buffers, registering callbacks). late: Vec, @@ -308,16 +472,52 @@ struct ParamPlan { args: Vec, } -fn plan_param(p: &ParamBinding) -> ParamPlan { - let n = py_name(&p.name); - let mut plan = ParamPlan { - prep: vec![], - late: vec![], - lend: None, - args: vec![], - }; - match p.arg_pass() { - ArgPass::Direct { .. } => plan.args.push(n), +/// The checked argument for a direct value `n` of C type `ty`: the +/// runtime's range check for an integer or enum, the value itself for a +/// float or `bool`. +fn checked(plan: &mut ParamPlan, n: &str, local: String, ty: &CType) -> String { + match int_checker(ty) { + Some(check) => { + plan.checks + .push((local.clone(), format!("{check}({n}, \"{n}\")"))); + local + } + None => n.to_string(), + } +} + +/// Plan how parameter `n` crosses per its passing contract. +fn plan_param(n: &str, pass: &ArgPass) -> ParamPlan { + let mut plan = ParamPlan::default(); + match pass { + ArgPass::Direct { slot } => { + let arg = checked(&mut plan, n, format!("_{n}_v"), &slot.ty); + plan.args.push(arg); + } + ArgPass::OptDirect { value, .. } => { + let zero = match value.ty { + CType::Float | CType::Double => "0.0", + CType::Bool => "False", + _ => "0", + }; + let local = format!("_{n}_v"); + let present = match int_checker(&value.ty) { + Some(check) => format!("{check}({n}, \"{n}\")"), + None => n.to_string(), + }; + plan.checks.push(( + local.clone(), + format!("{present} if {n} is not None else {zero}"), + )); + plan.args = vec![format!("{n} is not None"), local]; + } + ArgPass::Slice { elem, .. } => { + plan.prep.push(format!( + "_{n}_a = _array({n}, \"{}\", \"{n}\")", + prim_kind(*elem) + )); + plan.args = vec![format!("_{n}_a.buffer_info()[0]"), format!("len(_{n}_a)")]; + } ArgPass::String { .. } => { plan.prep.push(format!("_{n}_b = {n}.encode(\"utf-8\")")); plan.args = vec![format!("_{n}_b"), format!("len(_{n}_b)")]; @@ -326,27 +526,26 @@ fn plan_param(p: &ParamBinding) -> ParamPlan { plan.prep.push(format!("_{n}_b = bytes({n})")); plan.args = vec![format!("_{n}_b"), format!("len(_{n}_b)")]; } - // Encode now, mint object tokens later: `finish()` runs once every - // parameter has encoded and every lent object is held, so a failure - // anywhere before the call leaks no strong reference. + // Encoded with every other parameter; buffers are finished (object + // tokens minted) later. ArgPass::Buffer { .. } => { - plan.prep - .extend(encode_stmts(&format!("_{n}_w"), &n, &p.ty)); - plan.late.push(format!("_{n}_b = _{n}_w.finish()")); plan.args = vec![format!("_{n}_b"), format!("len(_{n}_b)")]; } - ArgPass::Object { nullable, .. } => { - let class = p.ty.interface_name().expect("object names an interface"); - plan.lend = Some(if nullable { + ArgPass::Object { + nullable, + interface, + .. + } => { + plan.lend = Some(if *nullable { Lend { local: format!("_{n}_p"), - acquire: format!("_lend_opt({n}, {class})"), + acquire: format!("_lend_opt({n}, {interface})"), release: format!("_release_opt({n})"), } } else { Lend { local: format!("_{n}_p"), - acquire: format!("_lend({n}, {class})"), + acquire: format!("_lend({n}, {interface})"), release: format!("{n}._release()"), } }); @@ -354,21 +553,23 @@ fn plan_param(p: &ParamBinding) -> ParamPlan { } // Registered last, so nothing between registration and the call can // fail and strand the entry; the producer releases it with `free`. - ArgPass::Callback { nullable, .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback names a callback interface"); - let vtable = format!("_{cb}_vtable_ptr"); - if nullable { - plan.late - .push(format!("_{n}_ctx = _callback_register_opt({n}, {cb})")); + ArgPass::Callback { + nullable, + interface, + .. + } => { + let vtable = format!("_{interface}_vtable_ptr"); + if *nullable { + plan.late.push(format!( + "_{n}_ctx = _callback_register_opt({n}, {interface})" + )); plan.args = vec![ format!("_{n}_ctx"), format!("{vtable} if {n} is not None else None"), ]; } else { plan.late - .push(format!("_{n}_ctx = _callback_register({n}, {cb})")); + .push(format!("_{n}_ctx = _callback_register({n}, {interface})")); plan.args = vec![format!("_{n}_ctx"), vtable]; } } @@ -377,24 +578,11 @@ fn plan_param(p: &ParamBinding) -> ParamPlan { } /// Render one callable's wrapper `def` (its bindings are emitted separately -/// by [`render_bindings`]). A throwing callable raises the error domain in -/// scope for `module`; any other failure raises the unchecked trap. -pub(crate) fn render_callable( - w: &mut CodeWriter, - g: &Gen<'_>, - module: &ModuleBinding, - f: &FnBinding, - scope: FnScope, -) { - let error = g.model.error_domain(module); - let raises = error.filter(|_| f.throws).map(|eb| { - ( - eb.type_name.as_str(), - "If the call reports one of the domain's error codes.", - ) - }); - let abi = launcher(f); - let binding = py_binding_name(&abi.symbol, g.prefix); +/// by [`render_bindings`]). A callable that throws raises its error domain +/// (or the root error for `throws: any`); any other failure raises the +/// unchecked trap. +pub(crate) fn render_callable(w: &mut CodeWriter, g: &Gen<'_>, f: &FnBinding, scope: FnScope) { + let binding = py_binding_name(&f.abi.symbol, g.prefix); let mut sig: Vec = match scope { FnScope::Method | FnScope::Init => vec!["self".into()], @@ -404,7 +592,7 @@ pub(crate) fn render_callable( sig.extend( f.params .iter() - .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty))), + .map(|p| format!("{}: {}", py_name(&p.name), py_param_hint(&p.ty))), ); let ret_hint = match scope { FnScope::Init => "None".to_string(), @@ -431,56 +619,45 @@ pub(crate) fn render_callable( sig.join(", "), )); w.indent(); - let mut doc = with_deprecation(f.doc.as_deref(), f.deprecated.as_deref()); - if let CallShape::Iterator(_) = f.shape { - let streaming = "Returns a lazy iterator: each step pulls one element from the\n\ - producer. Exhaust or close() the iterator to release its native\n\ - handle (garbage collection also releases it)."; + let mut doc = g.doc(&f.doc, &f.deprecated); + if f.iterator().is_some() { + let streaming = "Returns a lazy `NativeIterator`: each step pulls one element from\n\ + the producer. Exhaust it, `close()` it, or use it in a `with`\n\ + statement to release its native handle (garbage collection also\n\ + releases it)."; doc = Some(match doc { Some(d) => format!("{d}\n\n{streaming}"), None => streaming.to_string(), }); } - fn_docstring(w, doc.as_deref(), &f.params, raises); + let params: Vec = f + .params + .iter() + .map(|p| ParamDoc { + name: py_name(&p.name), + hint: py_param_hint(&p.ty), + doc: g.text(&p.doc), + }) + .collect(); + fn_docstring(w, doc.as_deref(), ¶ms, &g.raises(&f.error)); - if let Some(msg) = &f.deprecated { + if let Some(msg) = g.deprecation(&f.deprecated) { w.line(format!( "warnings.warn(\"{}\", DeprecationWarning, stacklevel=2)", - py_str_literal(msg) + py_str_literal(&msg) )); } - let plans: Vec = f.params.iter().map(plan_param).collect(); - for line in plans.iter().flat_map(|p| &p.prep) { - w.line(line); - } - - // The C arguments: `self`, each parameter's slots, then the trailing - // slots the lowering appended, named by the model. - let mut args: Vec = Vec::new(); - let mut consumed = 0; - if f.has_self { - args.push("_self_p".into()); - consumed = 1; - } - for (p, plan) in f.params.iter().zip(&plans) { - args.extend(plan.args.iter().cloned()); - consumed += p.abi.len(); - } - for slot in &abi.params[consumed..] { - args.push(match slot.name.as_str() { - "out_len" => "ctypes.byref(_out_len)".to_string(), - "out_err" => "ctypes.byref(_err)".to_string(), - "cancel_token" => "_token".to_string(), - "callback" => format!("{}_completion", completion_stem(g, f)), - "context" => "_call".to_string(), - other => unreachable!("unexpected trailing slot `{other}`"), - }); - } - let call = format!("{binding}({})", args.join(", ")); + let mut plans: Vec = f + .params + .iter() + .map(|p| plan_param(&py_name(&p.name), &p.pass)) + .collect(); + // Lent objects (the receiver first), and the late statements: finishing + // value buffers and registering callbacks. let mut lends: Vec = Vec::new(); - if f.has_self { + if f.has_self() { lends.push(Lend { local: "_self_p".into(), acquire: "self._acquire()".into(), @@ -488,11 +665,63 @@ pub(crate) fn render_callable( }); } lends.extend(plans.iter().filter_map(|p| p.lend.clone())); - let late: Vec = plans.iter().flat_map(|p| p.late.clone()).collect(); + let mut late: Vec = Vec::new(); + for (p, plan) in f.params.iter().zip(&plans) { + if let ArgPass::Buffer { .. } = p.pass { + let n = py_name(&p.name); + late.push(format!("_{n}_b = _{n}_w.finish()")); + } + late.extend(plan.late.iter().cloned()); + } + // Range checks run in the argument list unless something is lent or + // registered first. + let inline = lends.is_empty() && late.is_empty(); + for plan in &mut plans { + let checks = std::mem::take(&mut plan.checks); + for (local, expr) in checks { + if inline { + for arg in &mut plan.args { + if *arg == local { + arg.clone_from(&expr); + } + } + } else { + plan.prep.push(format!("{local} = {expr}")); + } + } + } + for (p, plan) in f.params.iter().zip(&plans) { + for line in &plan.prep { + w.line(line); + } + if let (ArgPass::Buffer { .. }, Some(ty)) = (&p.pass, p.ty.value()) { + let n = py_name(&p.name); + for line in encode_stmts(&format!("_{n}_w"), &n, ty) { + w.line(line); + } + } + } + + // The C arguments: `self`, each parameter's slots, then the trailing + // slots of the shape. + let mut args: Vec = Vec::new(); + if f.has_self() { + args.push("_self_p".into()); + } + for plan in &plans { + args.extend(plan.args.iter().cloned()); + } - if let CallShape::Async(_) = f.shape { + if let Some(a) = f.async_binding() { + if a.cancellable() { + args.push("_token".into()); + } + args.push(format!("{}_completion", completion_stem(g, f))); + args.push("_call".into()); + debug_assert_eq!(args.len(), f.abi.params.len(), "{}", f.abi.symbol); + let call = format!("{binding}({})", args.join(", ")); w.line("_call, _future = _async_begin()"); - if f.cancellable { + if a.cancellable() { w.line("_token = _cancel_token_create()"); } w.line("try:"); @@ -500,21 +729,21 @@ pub(crate) fn render_callable( w.line("except BaseException:"); w.scope(|w| { w.line("_async_abandon(_call)"); - if f.cancellable { + if a.cancellable() { w.line("_cancel_token_destroy(_token)"); } w.line("raise"); }); - let wait = if f.cancellable { + let wait = if a.cancellable() { "await _async_wait_cancellable(_future, _token)" } else { "await _future" }; // The completion built the value; the annotation hands its type to // checkers. - match &f.ret { + match f.ret.as_ref().and_then(RetTy::value) { Some(ty) => { - w.line(format!("_result: {} = {wait}", py_type_hint(ty))); + w.line(format!("_result: {} = {wait}", py_type_hint(ty, Dir::Out))); w.line("return _result"); } None => { @@ -525,45 +754,52 @@ pub(crate) fn render_callable( return; } - w.line("_err = _ErrorStruct()"); - if abi.params.iter().any(|p| p.name == "out_len") { - w.line("_out_len = ctypes.c_size_t()"); + for slot in f.ret_pass.out_slots() { + w.line(format!( + "{} = {}()", + out_local(slot), + py_out_local(&slot.ty) + )); + args.push(format!("ctypes.byref({})", out_local(slot))); } - // A direct result is used as returned, so its annotation tells checkers - // what the untyped `ctypes` call produced. - let call = match &f.ret { - Some(ty @ Ty::Prim(_)) if ty.family() == Family::Direct => { - format!("_ret: {} = {call}", py_type_hint(ty)) + w.line("_err = _ErrorStruct()"); + args.push("ctypes.byref(_err)".into()); + debug_assert_eq!(args.len(), f.abi.params.len(), "{}", f.abi.symbol); + let call = format!("{binding}({})", args.join(", ")); + // A direct scalar result is used as returned, so its annotation tells + // checkers what the untyped `ctypes` call produced. + let call = match (&f.ret_pass, f.ret.as_ref().and_then(RetTy::value)) { + (RetPass::Void, _) if !matches!(scope, FnScope::Init | FnScope::Factory) => call, + (RetPass::Direct, Some(ty @ Ty::Prim(_))) => { + format!("_ret: {} = {call}", py_type_hint(ty, Dir::Out)) } - Some(_) => format!("_ret = {call}"), - None if matches!(scope, FnScope::Init | FnScope::Factory) => format!("_ret = {call}"), - None => call, + _ => format!("_ret = {call}"), }; emit_lent_call(w, &lends, &late, &call); w.line("if _err.code:"); w.scope(|w| { w.line(format!( "raise {}(*_read_error(_err))", - py_raise_factory(f, error) + g.raise_factory(&f.error) )); }); - match (scope, &f.shape) { - (FnScope::Init, _) => { + match scope { + FnScope::Init => { w.line("self._init_handle(_required(_ret))"); } - (FnScope::Factory, _) => { + FnScope::Factory => { w.line("return cls._adopt(_required(_ret))"); } - (_, CallShape::Iterator(it)) => { - w.line(format!("return {}._adopt(_ret)", iterator_class(g, it))); - } _ => { - if let Some(ty) = &f.ret { + if let RetPass::Iterator(it) = &f.ret_pass { w.line(format!( - "return {}", - recv_expr(ty, "_ret", "_out_len.value") + "return _iterate(_required(_ret), {}, {})", + pull_fn(g, it), + py_binding_name(&it.destroy_symbol, g.prefix) )); + } else if let Some(r) = ret_recv(&f.ret_pass, f.ret.as_ref(), &f.abi) { + w.line(format!("return {}", recv_expr(&r))); } } } diff --git a/crates/weaveffi-cli/src/targets/python/codec.rs b/crates/weaveffi-cli/src/targets/python/codec.rs index 2d9ff661..18afb6fb 100644 --- a/crates/weaveffi-cli/src/targets/python/codec.rs +++ b/crates/weaveffi-cli/src/targets/python/codec.rs @@ -6,40 +6,25 @@ //! runtime call); everything else delegates to its named function, so no //! loop is ever inlined at a call site. Every dispatch goes through //! [`Ty::wire`], and primitives dispatch on [`Prim::snake`] (`write_i32`, -//! `read_string`). - -use std::collections::HashSet; +//! `read_string`). The composites and their names come from the shared +//! [`codecs`](crate::codegen::codecs) emitter (`_read_list_i32`, +//! `_write_map_string_Item`). +use crate::codegen::codecs::{composites, stem}; use crate::codegen::CodeWriter; use weaveffi_model::model::{EnumBinding, Model, StructBinding}; use weaveffi_model::ty::{Prim, Ty, WireType}; -use crate::targets::python::types::{py_field, py_type_hint, py_variant}; - -/// The identifier fragment naming `ty` in its codec functions: a -/// primitive's spelling (`i64`), a user type's name (`Entry`), or the -/// composite's shape (`opt_i64`, `list_string`, `map_string_Store`). -fn mangle(ty: &Ty) -> String { - match ty { - Ty::Optional(inner) => format!("opt_{}", mangle(inner)), - Ty::List(inner) => format!("list_{}", mangle(inner)), - Ty::Map(k, v) => format!("map_{}_{}", mangle(k), mangle(v)), - Ty::Prim(p) => p.snake().to_string(), - _ => ty - .user_name() - .expect("only optionals, lists, maps, primitives, and user types are mangled") - .to_string(), - } -} +use crate::targets::python::types::{py_field, py_type_hint, py_variant, Dir}; /// `_write_{stem}`: the writer for a record, rich enum, or composite. fn write_fn(ty: &Ty) -> String { - format!("_write_{}", mangle(ty)) + format!("_write_{}", stem(ty)) } /// `_read_{stem}`: the reader for a record, rich enum, or composite. fn read_fn(ty: &Ty) -> String { - format!("_read_{}", mangle(ty)) + format!("_read_{}", stem(ty)) } /// The `struct` format character and byte width of a fixed-width numeric @@ -194,70 +179,6 @@ pub(crate) fn render_rich_enum_codecs(w: &mut CodeWriter, e: &EnumBinding) { }); } -/// Record every optional, list, and map shape inside `ty`, innermost first. -fn collect(ty: &Ty, out: &mut Vec, seen: &mut HashSet) { - match ty { - Ty::Optional(inner) | Ty::List(inner) => collect(inner, out, seen), - Ty::Map(k, v) => { - collect(k, out, seen); - collect(v, out, seen); - } - _ => return, - } - if seen.insert(ty.clone()) { - out.push(ty.clone()); - } -} - -/// Every distinct composite type a value buffer of the API carries, in -/// first-use order: inside records, rich-enum variants, and error payloads, -/// and at every buffered call boundary (parameters, returns, iterator -/// elements, and callback parameters and returns). -fn composites(model: &Model) -> Vec { - let mut out = Vec::new(); - let mut seen = HashSet::new(); - for m in &model.modules { - let fields = m - .structs - .iter() - .flat_map(|s| &s.fields) - .chain( - m.enums - .iter() - .flat_map(|e| &e.variants) - .flat_map(|v| &v.fields), - ) - .chain( - m.errors - .iter() - .flat_map(|e| &e.codes) - .flat_map(|c| &c.fields), - ); - for f in fields { - collect(&f.ty, &mut out, &mut seen); - } - } - let mut boundary = |ty: &Ty| { - let ty = ty.iterator_elem().unwrap_or(ty); - if ty.is_buffered() { - collect(ty, &mut out, &mut seen); - } - }; - for m in &model.modules { - for f in m.callables() { - f.params.iter().for_each(|p| boundary(&p.ty)); - f.ret.iter().for_each(&mut boundary); - } - for cb in &m.callback_interfaces { - for meth in &cb.methods { - meth.params.iter().for_each(|p| boundary(&p.ty)); - meth.ret.iter().for_each(&mut boundary); - } - } - } - out -} - /// Append the writer and reader of every composite type the API uses, after /// every module (a function body resolves the codecs it calls when it runs, /// so definition order doesn't matter). @@ -275,11 +196,11 @@ pub(crate) fn render_composite_codecs(w: &mut CodeWriter, model: &Model) { /// Append one composite type's writer and reader. fn render_composite(w: &mut CodeWriter, ty: &Ty) { - let hint = py_type_hint(ty); w.blank().blank(); w.line(format!( - "def {}(_w: _Writer, value: {hint}) -> None:", - write_fn(ty) + "def {}(_w: _Writer, value: {}) -> None:", + write_fn(ty), + py_type_hint(ty, Dir::In) )); w.scope(|w| match ty { Ty::Optional(inner) => { @@ -292,7 +213,10 @@ fn render_composite(w: &mut CodeWriter, ty: &Ty) { Ty::List(inner) => match inner.wire() { WireType::Prim(p) if numeric_format(p).is_some() => { let (code, _) = numeric_format(p).expect("numeric"); - w.line(format!("_w.write_numbers(\"{code}\", value)")); + w.line(format!( + "_w.write_numbers(\"{code}\", \"{}\", value)", + p.snake() + )); } _ => { w.line("_w.write_count(len(value))"); @@ -314,7 +238,11 @@ fn render_composite(w: &mut CodeWriter, ty: &Ty) { }); w.blank().blank(); - w.line(format!("def {}(_r: _Reader) -> {hint}:", read_fn(ty))); + w.line(format!( + "def {}(_r: _Reader) -> {}:", + read_fn(ty), + py_type_hint(ty, Dir::Out) + )); w.scope(|w| match ty { Ty::Optional(inner) => { w.line(format!( diff --git a/crates/weaveffi-cli/src/targets/python/docs.rs b/crates/weaveffi-cli/src/targets/python/docs.rs index faa31c9d..3c712442 100644 --- a/crates/weaveffi-cli/src/targets/python/docs.rs +++ b/crates/weaveffi-cli/src/targets/python/docs.rs @@ -1,14 +1,12 @@ //! Doc emission for the generated Python: `#` comments, PEP 257 docstrings, //! and the NumPy-style callable docstring with `Parameters` and `Raises` //! sections. Everything writes into the one [`CodeWriter`] at its current -//! depth. +//! depth. IDL text reaches here already rewritten to Python spellings (see +//! [`Gen::doc`](crate::targets::python::Gen::doc)). use crate::codegen::CodeWriter; -use weaveffi_model::model::ParamBinding; -use crate::targets::python::types::{py_name, py_type_hint}; - -/// `doc` trimmed, or `None` when it is absent or blank. +/// `doc` trimmed, or `None` when it's absent or blank. fn trimmed(doc: Option<&str>) -> Option<&str> { doc.map(str::trim).filter(|d| !d.is_empty()) } @@ -24,17 +22,6 @@ fn escape(text: &str) -> String { out } -/// Join a summary with an optional deprecation note into one docstring -/// body. -pub(crate) fn with_deprecation(doc: Option<&str>, deprecated: Option<&str>) -> Option { - let note = deprecated.map(|msg| format!("Deprecated: {}", msg.trim())); - match (trimmed(doc), note) { - (Some(doc), Some(note)) => Some(format!("{doc}\n\n{note}")), - (Some(doc), None) => Some(doc.to_string()), - (None, note) => note, - } -} - /// Emit `doc` as a PEP 257 docstring, the first statement of a class or /// function body: one line when it fits on one, else the summary on the /// opening line and the closing quotes on their own. No-op when `doc` is @@ -67,22 +54,32 @@ pub(crate) fn comment(w: &mut CodeWriter, doc: Option<&str>) { } } +/// One documented parameter of a callable: its Python name, annotation, and +/// doc text. +pub(crate) struct ParamDoc { + /// The parameter's Python spelling. + pub name: String, + /// Its annotation. + pub hint: String, + /// Its doc text, already rewritten. + pub doc: Option, +} + /// Emit a callable's NumPy-style docstring: the summary, a `Parameters` -/// section for the parameters that carry docs, and a `Raises` section from -/// `raises`, a `(domain, when)` pair naming the module's error domain (for a -/// callable that declares errors) and when it's raised. Falls back to a -/// plain docstring when there are no sections. +/// section for the parameters that carry docs, and a `Raises` section of +/// `(exception, when)` pairs. Falls back to a plain docstring when there +/// are no sections. pub(crate) fn fn_docstring( w: &mut CodeWriter, doc: Option<&str>, - params: &[ParamBinding], - raises: Option<(&str, &str)>, + params: &[ParamDoc], + raises: &[(String, String)], ) { - let documented: Vec<(&ParamBinding, &str)> = params + let documented: Vec<(&ParamDoc, &str)> = params .iter() .filter_map(|p| trimmed(p.doc.as_deref()).map(|d| (p, d))) .collect(); - if documented.is_empty() && raises.is_none() { + if documented.is_empty() && raises.is_empty() { docstring(w, doc); return; } @@ -93,7 +90,7 @@ pub(crate) fn fn_docstring( if !documented.is_empty() { let mut s = String::from("Parameters\n----------"); for (p, d) in documented { - s.push_str(&format!("\n{} : {}", py_name(&p.name), py_type_hint(&p.ty))); + s.push_str(&format!("\n{} : {}", p.name, p.hint)); for line in d.lines() { s.push('\n'); if !line.trim().is_empty() { @@ -104,8 +101,12 @@ pub(crate) fn fn_docstring( } sections.push(s); } - if let Some((domain, when)) = raises { - sections.push(format!("Raises\n------\n{domain}\n {when}")); + if !raises.is_empty() { + let mut s = String::from("Raises\n------"); + for (exception, when) in raises { + s.push_str(&format!("\n{exception}\n {when}")); + } + sections.push(s); } let body = sections.join("\n\n"); if trimmed(doc).is_some() { diff --git a/crates/weaveffi-cli/src/targets/python/entities.rs b/crates/weaveffi-cli/src/targets/python/entities.rs index e098832f..fcd7b599 100644 --- a/crates/weaveffi-cli/src/targets/python/entities.rs +++ b/crates/weaveffi-cli/src/targets/python/entities.rs @@ -1,18 +1,15 @@ //! Entity rendering: error domains, plain and rich enums, record //! dataclasses, and interface wrapper classes. +use crate::codegen::errors::ErrorTable; use crate::codegen::CodeWriter; -use heck::ToSnakeCase; -use weaveffi_model::model::{ - CallShape, EnumBinding, ErrorBinding, FieldBinding, FnBinding, InterfaceBinding, Model, - ModuleBinding, StructBinding, -}; +use weaveffi_model::model::{EnumBinding, FieldBinding, InterfaceBinding, StructBinding}; use crate::targets::python::calls::{render_bindings, render_callable, FnScope}; use crate::targets::python::codec::{ read_expr, render_record_codecs, render_rich_enum_codecs, write_stmt, }; -use crate::targets::python::docs::{comment, docstring, with_deprecation}; +use crate::targets::python::docs::{comment, docstring}; use crate::targets::python::types::{ py_binding_name, py_error_field, py_field, py_field_hint, py_str_literal, py_variant, }; @@ -20,95 +17,31 @@ use crate::targets::python::Gen; // ── Errors ── -/// The first of `candidates` that no declaration of the API (a type, or an -/// error code's class) already names. -fn free_name(model: &Model, candidates: [String; 3]) -> String { - let taken = |name: &str| { - model.modules.iter().any(|m| { - m.enums.iter().any(|e| e.name == name) - || m.structs.iter().any(|s| s.name == name) - || m.interfaces.iter().any(|i| i.name == name) - || m.callback_interfaces.iter().any(|c| c.name == name) - || m.errors.as_ref().is_some_and(|e| { - e.type_name == name - || e.codes.iter().any(|c| py_code_class_name(&c.name) == name) - }) - }) - }; - let fallback = format!("{}Root", candidates[2]); - candidates - .into_iter() - .find(|n| !taken(n)) - .unwrap_or(fallback) -} - -/// The root exception class every declared error derives from: `Error` (so -/// consumers write `except kvstore.Error`), unless the API declares a type -/// or error code with that name, in which case `{PascalName}Error`, then -/// `{PascalName}BaseError`. -pub(crate) fn root_error_name(model: &Model, pascal_name: &str) -> String { - free_name( - model, - [ - "Error".to_string(), - format!("{pascal_name}Error"), - format!("{pascal_name}BaseError"), - ], - ) -} - -/// The unchecked `RuntimeError` subclass a failed call that declares no -/// errors raises: `InternalError`, unless the API declares that name. -pub(crate) fn trap_error_name(model: &Model, pascal_name: &str) -> String { - free_name( - model, - [ - "InternalError".to_string(), - format!("{pascal_name}InternalError"), - format!("{pascal_name}RuntimeError"), - ], - ) -} - -/// `_{stem}_from`: builds the domain exception matching a code, message, -/// and payload. -pub(crate) fn py_factory_name(eb: &ErrorBinding) -> String { - format!("_{}_from", eb.type_name.to_snake_case()) -} - -/// The factory an out-err slot of `f` is raised through: the module -/// domain's typed factory when `f` declares errors, the unchecked trap -/// otherwise. -pub(crate) fn py_raise_factory(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match error { - Some(eb) if f.throws => py_factory_name(eb), - _ => "_trap_from".to_string(), - } -} - -/// The Python class name for one error code: plain PascalCase with no forced -/// suffix (`KeyNotFound`, not `KeyNotFoundError`). Each class is also -/// attached to its domain class (`KvError.KeyNotFound`). -pub(crate) fn py_code_class_name(name: &str) -> String { - weaveffi_model::errors::pascal(name) -} - -/// Render one module's declared error domain: a base exception named after -/// the domain (subclassing the root exception), one subclass per code -/// carrying its stable `CODE`, its payload fields as constructor arguments -/// and attributes, and their value-buffer encoding (a callback method that -/// declares errors raises them back to the producer), then the factory that -/// builds the exception for a code, message, and payload. Each code class -/// is also attached to the domain class, so consumers can catch +/// Render one error domain: a base exception named after the domain +/// (subclassing the root exception), one subclass per code carrying its +/// stable `CODE`, its payload fields as constructor arguments and +/// attributes, and their value-buffer encoding (a callback method that +/// throws the domain raises them back to the producer), then the factory +/// that builds the exception for a code, message, and payload. Each code +/// class is also attached to the domain class, so consumers can catch /// `KvError.KeyNotFound`. -pub(crate) fn render_error( - w: &mut CodeWriter, - g: &Gen<'_>, - module: &ModuleBinding, - eb: &ErrorBinding, -) { - let domain = &eb.type_name; +/// +/// Domains are open: the factory maps a positive code these bindings don't +/// know (from a newer library) to the domain's base class itself, with the +/// code and message preserved. +pub(crate) fn render_error(w: &mut CodeWriter, g: &Gen<'_>, t: &ErrorTable<'_>) { + let domain = &t.type_name; let root = &g.root_error; + let codes: Vec<(String, String)> = t + .codes + .iter() + .map(|row| { + ( + weaveffi_model::errors::pascal(&row.code.name), + g.code_class(&row.code.name).to_string(), + ) + }) + .collect(); w.blank().blank(); w.line(format!("class {domain}({root}):")); @@ -116,29 +49,34 @@ pub(crate) fn render_error( docstring( w, Some(&format!( - "Base exception for the `{}` module's error domain.", - module.dot_path + "Base exception of the `{}` error domain (module `{}`).\n\n\ + Each declared code raises its own subclass. A positive code these\n\ + bindings don't know (from a newer library) raises this class itself,\n\ + with `code` and `message` preserved.", + t.domain.name, t.module.dot_path )), ); - // The per-code classes, attached below once they exist. - w.blank(); - for c in &eb.codes { - let class = py_code_class_name(&c.name); - w.line(format!("{class}: Type[{class}]")); + if !codes.is_empty() { + // The per-code classes, attached below once they exist. + w.blank(); + for (alias, class) in &codes { + w.line(format!("{alias}: ClassVar[type[{class}]]")); + } } }); - for c in &eb.codes { - let class = py_code_class_name(&c.name); + for (row, (_, class)) in t.codes.iter().zip(&codes) { + let c = row.code; w.blank().blank(); w.line(format!("class {class}({domain}):")); w.scope(|w| { - docstring(w, Some(c.doc.as_deref().unwrap_or(&c.message))); + let doc = g.text(&c.doc); + docstring(w, Some(doc.as_deref().unwrap_or(&c.message))); w.blank(); w.line(format!("CODE = {}", c.value)); if !c.fields.is_empty() { w.blank(); - render_dataclass_fields(w, &c.fields, py_error_field); + render_fields(w, g, &c.fields, py_error_field); } let mut params: Vec = c .fields @@ -183,27 +121,27 @@ pub(crate) fn render_error( // Scoped aliases: `except KvError.KeyNotFound` names the code through its // domain. - w.blank().blank(); - for c in &eb.codes { - let class = py_code_class_name(&c.name); - w.line(format!("{domain}.{class} = {class}")); + if !codes.is_empty() { + w.blank().blank(); + for (alias, class) in &codes { + w.line(format!("{domain}.{alias} = {class}")); + } } w.blank().blank(); w.line(format!( "def {}(code: int, message: str, payload: bytes = b\"\") -> {root}:", - py_factory_name(eb) + g.domain_factory(&t.domain.name) )); w.scope(|w| { docstring( w, Some(&format!( - "The {domain} subclass for `code` with its payload fields, or the\n\ - plain {root} for a runtime code (a panic, a marshalling failure)." + "The exception for a failure of a call that throws {domain}." )), ); - for c in &eb.codes { - let class = py_code_class_name(&c.name); + for (row, (_, class)) in t.codes.iter().zip(&codes) { + let c = row.code; w.line(format!("if code == {}:", c.value)); w.scope(|w| { if c.fields.is_empty() { @@ -222,7 +160,7 @@ pub(crate) fn render_error( w.line("))"); }); } - w.line(format!("return {root}(code, message)")); + w.line(format!("return _unknown_code({domain}, code, message)")); }); } @@ -230,21 +168,21 @@ pub(crate) fn render_error( /// Render one enum: a plain `IntEnum` for a C-style enum, or the dataclass /// sum-type hierarchy for a rich enum. -pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { +pub(crate) fn render_enum(w: &mut CodeWriter, g: &Gen<'_>, e: &EnumBinding) { if e.is_rich() { - render_rich_enum(w, e); + render_rich_enum(w, g, e); return; } w.blank().blank(); w.line(format!("class {}(IntEnum):", e.name)); w.scope(|w| { - let doc = with_deprecation(e.doc.as_deref(), e.deprecated.as_deref()); + let doc = g.doc(&e.doc, &e.deprecated); if doc.is_some() { docstring(w, doc.as_deref()); w.blank(); } for v in &e.variants { - comment(w, v.doc.as_deref()); + comment(w, g.text(&v.doc).as_deref()); w.line(format!("{} = {}", py_variant(&v.name), v.value)); } }); @@ -252,33 +190,38 @@ pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { /// Render a rich (algebraic) enum as an idiomatic Python sum type: a base /// class holding the nested `Tag` discriminant enum and a `tag` property, -/// one module-level `@dataclass` subclass per variant carrying its fields, -/// scoped aliases (`Shape.Circle` is `ShapeCircle`), and the buffer codec. -/// Consumers construct variants directly and discriminate with `isinstance` -/// (or the `tag` property). -fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { +/// one module-level frozen dataclass subclass per variant carrying its +/// fields, scoped aliases (`Shape.Circle` is `ShapeCircle`), and the buffer +/// codec. Consumers construct variants directly and discriminate with +/// `isinstance`, `match`, or the `tag` property. +fn render_rich_enum(w: &mut CodeWriter, g: &Gen<'_>, e: &EnumBinding) { let name = &e.name; w.blank().blank(); w.line(format!("class {name}:")); w.scope(|w| { - let doc = with_deprecation(e.doc.as_deref(), e.deprecated.as_deref()); + let doc = g.doc(&e.doc, &e.deprecated); if doc.is_some() { docstring(w, doc.as_deref()); w.blank(); } + w.line("__slots__ = ()"); + w.blank(); w.line("class Tag(IntEnum):"); w.scope(|w| { for v in &e.variants { - comment(w, v.doc.as_deref()); + comment(w, g.text(&v.doc).as_deref()); w.line(format!("{} = {}", py_variant(&v.name), v.value)); } }); w.blank(); // The variant classes, attached below once they exist. for v in &e.variants { - w.line(format!("{0}: Type[{name}{0}]", py_variant(&v.name))); + w.line(format!( + "{0}: ClassVar[type[{name}{0}]]", + py_variant(&v.name) + )); } - w.line(format!("TAG: {name}.Tag")); + w.line(format!("TAG: ClassVar[{name}.Tag]")); w.blank(); w.line("@property"); w.line(format!("def tag(self) -> {name}.Tag:")); @@ -291,17 +234,18 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { for v in &e.variants { let class = format!("{name}{}", py_variant(&v.name)); w.blank().blank(); - w.line("@dataclass"); + w.line("@dataclass(frozen=True, slots=True)"); w.line(format!("class {class}({name}):")); w.scope(|w| { - if v.doc.is_some() { - docstring(w, v.doc.as_deref()); + let doc = g.text(&v.doc); + if doc.is_some() { + docstring(w, doc.as_deref()); w.blank(); } w.line(format!("TAG = {name}.Tag.{}", py_variant(&v.name))); if !v.fields.is_empty() { w.blank(); - render_dataclass_fields(w, &v.fields, py_field); + render_fields(w, g, &v.fields, py_field); } }); } @@ -318,16 +262,16 @@ fn render_rich_enum(w: &mut CodeWriter, e: &EnumBinding) { // ── Records ── -/// Render a record as a plain `@dataclass` value class plus its buffer -/// codec. Records have no C symbols: construction, equality, and repr all +/// Render a record as a frozen, slotted dataclass plus its buffer codec. +/// Records have no C symbols: construction, equality, hashing, and repr all /// come from the dataclass, and instances cross the ABI serialized in value -/// buffers. -pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { +/// buffers. Derive a changed copy with `dataclasses.replace`. +pub(crate) fn render_struct(w: &mut CodeWriter, g: &Gen<'_>, s: &StructBinding) { w.blank().blank(); - w.line("@dataclass"); + w.line("@dataclass(frozen=True, slots=True)"); w.line(format!("class {}:", s.name)); w.scope(|w| { - let doc = with_deprecation(s.doc.as_deref(), s.deprecated.as_deref()); + let doc = g.doc(&s.doc, &s.deprecated); docstring(w, doc.as_deref()); if s.fields.is_empty() { if doc.is_none() { @@ -338,16 +282,21 @@ pub(crate) fn render_struct(w: &mut CodeWriter, s: &StructBinding) { if doc.is_some() { w.blank(); } - render_dataclass_fields(w, &s.fields, py_field); + render_fields(w, g, &s.fields, py_field); }); render_record_codecs(w, s); } /// Emit one annotated attribute line (`name: hint`) per field, with field /// docs as leading comments; `spell` names each field. -fn render_dataclass_fields(w: &mut CodeWriter, fields: &[FieldBinding], spell: fn(&str) -> String) { +fn render_fields( + w: &mut CodeWriter, + g: &Gen<'_>, + fields: &[FieldBinding], + spell: fn(&str) -> String, +) { for f in fields { - comment(w, f.doc.as_deref()); + comment(w, g.text(&f.doc).as_deref()); w.line(format!( "{}: {}", spell(&f.name), @@ -367,19 +316,13 @@ fn render_dataclass_fields(w: &mut CodeWriter, fields: &[FieldBinding], spell: f /// constructor named `new` becomes `__init__`; every other constructor /// becomes a `@classmethod` factory; methods lend `self` as the leading C /// argument; statics are `@staticmethod`s. -pub(crate) fn render_interface( - w: &mut CodeWriter, - g: &Gen<'_>, - module: &ModuleBinding, - i: &InterfaceBinding, -) { - let error = g.model.error_domain(module); +pub(crate) fn render_interface(w: &mut CodeWriter, g: &Gen<'_>, i: &InterfaceBinding) { let name = &i.name; - // Consecutive sync member bindings are packed into one block. + // Consecutive plain sync member bindings are packed into one block. let mut packed = false; - for m in i.constructors.iter().chain(&i.methods).chain(&i.statics) { - render_bindings(w, g, m, error, &format!("{name}.{}", m.name), packed); - packed = matches!(m.shape, CallShape::Sync(_)); + for m in i.members() { + render_bindings(w, g, m, packed); + packed = !m.is_async() && m.iterator().is_none(); } let clone = py_binding_name(&i.clone_symbol, g.prefix); let destroy = py_binding_name(&i.destroy_symbol, g.prefix); @@ -398,7 +341,7 @@ pub(crate) fn render_interface( w.blank().blank(); w.line(format!("class {name}(_Object):")); w.scope(|w| { - let doc = with_deprecation(i.doc.as_deref(), i.deprecated.as_deref()); + let doc = g.doc(&i.doc, &i.deprecated); if doc.is_some() { docstring(w, doc.as_deref()); w.blank(); @@ -407,7 +350,7 @@ pub(crate) fn render_interface( w.line(format!("_clone = staticmethod({clone})")); match i.constructors.iter().find(|c| c.name == "new") { - Some(c) => render_callable(w, g, module, c, FnScope::Init), + Some(c) => render_callable(w, g, c, FnScope::Init), None => { // No canonical constructor: instances only come from // factories and producer returns. @@ -421,13 +364,13 @@ pub(crate) fn render_interface( } } for c in i.constructors.iter().filter(|c| c.name != "new") { - render_callable(w, g, module, c, FnScope::Factory); + render_callable(w, g, c, FnScope::Factory); } for m in &i.methods { - render_callable(w, g, module, m, FnScope::Method); + render_callable(w, g, m, FnScope::Method); } for s in &i.statics { - render_callable(w, g, module, s, FnScope::Static); + render_callable(w, g, s, FnScope::Static); } }); } diff --git a/crates/weaveffi-cli/src/targets/python/mod.rs b/crates/weaveffi-cli/src/targets/python/mod.rs index 588998b7..3aac6db1 100644 --- a/crates/weaveffi-cli/src/targets/python/mod.rs +++ b/crates/weaveffi-cli/src/targets/python/mod.rs @@ -1,16 +1,18 @@ //! Python (`ctypes`) binding generator. //! //! Emits a pip-installable package of `ctypes` bindings over the C ABI -//! (revision 4): distribution `{name}`, import package `{prefix}`, and one +//! (revision 5): distribution `{name}`, import package `{prefix}`, and one //! fully annotated implementation module `{prefix}/{prefix}.py` with a -//! `py.typed` marker. Every C prototype is bound once at import time, after -//! the load-time ABI revision and contract-table check. Records and rich -//! enums are dataclasses crossing the boundary as value buffers; interfaces -//! are reference-counted wrapper classes with `close()` and a `__del__` +//! `py.typed` marker (Python 3.10 or later). Every C prototype is bound once +//! at import time, after the load-time ABI revision and contract-table +//! check. Records and rich enums are frozen, slotted dataclasses crossing +//! the boundary as value buffers; optional scalars and numeric lists cross +//! directly (`bool` flag plus value, and typed arrays); interfaces are +//! reference-counted wrapper classes with `close()` and a `__del__` //! backstop; callback interfaces are abstract base classes backed by one //! static vtable of `ctypes` trampolines; async functions are coroutines //! whose cancellation cancels the native call; `iter` returns are lazy -//! Python iterators. +//! `NativeIterator[T]` objects. mod callbacks; mod calls; @@ -23,28 +25,38 @@ mod runtime; mod tests; mod types; -use crate::backend::{LanguageBackend, OutputFile}; +use std::collections::{HashMap, HashSet}; + +use crate::codegen::docs::{ApiNames, Doc, IdentKind}; +use crate::codegen::errors::{self, ErrorTable}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{Artifact, ArtifactKind, PackageContext, PackagedFile, WheelMeta}; use crate::platform::{glibc_requirement, Os}; +use crate::targets::Target; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use camino::Utf8Path; +use camino::Utf8PathBuf; +use heck::ToSnakeCase; +use miette::Result; use serde::{Deserialize, Serialize}; +use weaveffi_model::errors::type_name; use weaveffi_model::model::{Model, ModuleBinding}; use weaveffi_model::pkg::Identity; +use weaveffi_model::plan::ErrorStrategy; use crate::targets::python::callbacks::render_callback_interface; use crate::targets::python::calls::{render_bindings, render_callable, FnScope}; use crate::targets::python::codec::render_composite_codecs; use crate::targets::python::entities::{ - py_code_class_name, render_enum, render_error, render_interface, render_struct, - root_error_name, trap_error_name, + render_enum, render_error, render_interface, render_struct, }; use crate::targets::python::package::{ render_py_typed, render_pyproject_toml, render_readme, render_wheel_readme, }; use crate::targets::python::runtime::render_runtime; -use crate::targets::python::types::{py_member_name, py_variant}; +use crate::targets::python::types::{ + py_field, py_member_name, py_name, py_object_member, py_variant, +}; /// Per-target configuration for [`PythonGenerator`]. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -58,7 +70,7 @@ pub struct PythonConfig { /// and the implementation module inside it. pub import_name: Option, /// The `requires-python` specifier of the project and its wheels - /// (default `>=3.9`). + /// (default `>=3.10`, the oldest Python the generated code runs on). pub requires_python: String, } @@ -67,7 +79,7 @@ impl Default for PythonConfig { Self { name: None, import_name: None, - requires_python: ">=3.9".into(), + requires_python: ">=3.10".into(), } } } @@ -115,33 +127,233 @@ fn wheel_meta(id: &Identity, names: &Names, config: &PythonConfig, tag: String) } } -/// The shared context every rendering function receives. +/// Python's builtin exception names ending in `Error`. A derived code class +/// name (`Timeout` -> `TimeoutError`) never takes one, since a star import +/// of the package would shadow the builtin. +const PY_BUILTIN_ERRORS: &[&str] = &[ + "ArithmeticError", + "AssertionError", + "AttributeError", + "BlockingIOError", + "BrokenPipeError", + "BufferError", + "ChildProcessError", + "ConnectionAbortedError", + "ConnectionError", + "ConnectionRefusedError", + "ConnectionResetError", + "EOFError", + "EnvironmentError", + "FileExistsError", + "FileNotFoundError", + "FloatingPointError", + "IOError", + "ImportError", + "IndentationError", + "IndexError", + "InterruptedError", + "IsADirectoryError", + "KeyError", + "LookupError", + "MemoryError", + "ModuleNotFoundError", + "NameError", + "NotADirectoryError", + "NotImplementedError", + "OSError", + "OverflowError", + "PermissionError", + "ProcessLookupError", + "PythonFinalizationError", + "RecursionError", + "ReferenceError", + "RuntimeError", + "SyntaxError", + "SystemError", + "TabError", + "TimeoutError", + "TypeError", + "UnboundLocalError", + "UnicodeDecodeError", + "UnicodeEncodeError", + "UnicodeError", + "UnicodeTranslateError", + "ValueError", + "ZeroDivisionError", +]; + +/// The public classes the runtime itself defines. +const RUNTIME_CLASSES: &[&str] = &["LibraryLoadError", "NativeIterator"]; + +/// The shared context every rendering function receives: the model, the +/// resolved exception class names, and the identifier index docs are +/// rewritten through. pub(crate) struct Gen<'a> { /// The model being rendered. pub model: &'a Model, /// The C symbol prefix (stripped from binding names). pub prefix: &'a str, - /// The root exception class name (see [`root_error_name`]). + /// The root exception class every declared error derives from: `Error` + /// (so consumers write `except kvstore.Error`), unless the API declares + /// that name, then `{PascalName}Error`, then `{PascalName}BaseError`. pub root_error: String, - /// The unchecked trap exception class name (see [`trap_error_name`]). + /// The unchecked `RuntimeError` subclass a failed call that declares no + /// errors raises: `InternalError`, unless the API declares that name. pub trap_error: String, + /// Every error domain with its shared type names, by declaring module. + errors: Vec>, + /// Each error code's module-level class name, by code name (code names + /// are globally unique). + code_classes: HashMap, + /// Every identifier the API declares, for rewriting doc text. + names: ApiNames, } impl<'a> Gen<'a> { fn new(model: &'a Model) -> Self { + let errors = errors::tables(model, "Error"); + let mut taken: HashSet = RUNTIME_CLASSES.iter().map(|s| s.to_string()).collect(); + for m in &model.modules { + taken.extend(m.enums.iter().map(|e| e.name.clone())); + taken.extend(m.structs.iter().map(|s| s.name.clone())); + taken.extend(m.interfaces.iter().map(|i| i.name.clone())); + taken.extend(m.callback_interfaces.iter().map(|c| c.name.clone())); + } + taken.extend(errors.iter().map(|t| t.type_name.clone())); + // A code class is the shared `{Code}Error` name, or, when that's a + // builtin or another declaration, `{DomainStem}{Code}Error`. + let mut code_classes = HashMap::new(); + for t in &errors { + for row in &t.codes { + let mut class = row.type_name.clone(); + if PY_BUILTIN_ERRORS.contains(&class.as_str()) || taken.contains(&class) { + class = format!("{}{class}", type_name(&t.domain.name, "")); + } + taken.insert(class.clone()); + code_classes.insert(row.code.name.clone(), class); + } + } let pascal = model.identity.pascal_name(); + let free = |candidates: [String; 3]| { + let fallback = format!("{}Root", candidates[2]); + candidates + .into_iter() + .find(|n| !taken.contains(n)) + .unwrap_or(fallback) + }; + let root_error = free([ + "Error".into(), + format!("{pascal}Error"), + format!("{pascal}BaseError"), + ]); + let trap_error = free([ + "InternalError".into(), + format!("{pascal}InternalError"), + format!("{pascal}RuntimeError"), + ]); Self { model, prefix: model.prefix(), - root_error: root_error_name(model, &pascal), - trap_error: trap_error_name(model, &pascal), + root_error, + trap_error, + errors, + code_classes, + names: ApiNames::new(model), + } + } + + /// The error domains `m` declares. + fn errors_of(&self, m: &ModuleBinding) -> impl Iterator> { + let index = m.index; + self.errors.iter().filter(move |t| t.module.index == index) + } + + /// The class of the error domain named `name` (the shared type name). + pub(crate) fn domain_class(&self, name: &str) -> &str { + self.errors + .iter() + .find(|t| t.domain.name == name) + .map(|t| t.type_name.as_str()) + .expect("a validated model declares every domain a callable throws") + } + + /// The module-level class of the error code named `code`. + pub(crate) fn code_class(&self, code: &str) -> &str { + &self.code_classes[code] + } + + /// The factory building the exception of a failed call that throws the + /// domain `name`: `_{snake class}_from`. + pub(crate) fn domain_factory(&self, name: &str) -> String { + format!("_{}_from", self.domain_class(name).to_snake_case()) + } + + /// The factory an out-err slot of a callable with error strategy `error` + /// is raised through: the domain's typed factory, the root error's + /// (`throws: any`), or the unchecked trap's. + pub(crate) fn raise_factory(&self, error: &ErrorStrategy) -> String { + match error { + ErrorStrategy::Domain(name) => self.domain_factory(name), + ErrorStrategy::Untyped => format!("_{}_from", self.root_error.to_snake_case()), + ErrorStrategy::Trap => "_trap_from".into(), } } + + /// The `Raises` entries of a callable with error strategy `error`. + pub(crate) fn raises(&self, error: &ErrorStrategy) -> Vec<(String, String)> { + match error { + ErrorStrategy::Domain(name) => vec![( + self.domain_class(name).to_string(), + "If the call fails with one of the domain's codes.".into(), + )], + ErrorStrategy::Untyped => vec![(self.root_error.clone(), "If the call fails.".into())], + ErrorStrategy::Trap => vec![], + } + } + + /// The Python spelling of an API identifier named in backticks in doc + /// text, or `None` to keep it as written. + fn spell(&self, ident: &str) -> Option { + Some(match self.names.kind(ident)? { + IdentKind::Function | IdentKind::CallbackMethod => py_member_name(ident), + IdentKind::Member => py_object_member(ident), + IdentKind::Param => py_name(ident), + IdentKind::Field => py_field(ident), + IdentKind::ErrorDomain => type_name(ident, "Error"), + IdentKind::ErrorCode => self.code_classes.get(ident)?.clone(), + IdentKind::Module | IdentKind::Type | IdentKind::Variant => return None, + }) + } + + /// Doc text in Python spellings, or `None` when there's none. + pub(crate) fn text(&self, doc: &Option) -> Option { + Doc::new(doc, &None).text(|i| self.spell(i)) + } + + /// Doc text followed by a `Deprecated: ...` paragraph, in Python + /// spellings. + pub(crate) fn doc(&self, doc: &Option, deprecated: &Option) -> Option { + Doc::new(doc, deprecated).with_deprecation(|i| self.spell(i)) + } + + /// The deprecation message in Python spellings, for the + /// `DeprecationWarning` a deprecated callable issues. + pub(crate) fn deprecation(&self, deprecated: &Option) -> Option { + Doc::new(&None, deprecated).deprecation(|i| self.spell(i)) + } } /// Python backend: emits a pip-installable package of fully annotated /// `ctypes` bindings over the C ABI exposed by the underlying cdylib. -pub struct PythonGenerator; +pub struct PythonGenerator { + config: PythonConfig, +} + +impl From for PythonGenerator { + fn from(config: PythonConfig) -> Self { + Self { config } + } +} impl PythonGenerator { /// Render the implementation module: the runtime, every module's members @@ -187,28 +399,28 @@ impl PythonGenerator { } } -/// Emit every member of `m` in canonical order. Callback interfaces precede -/// interfaces because interface members take them as parameters. +/// Emit every member of `m` in canonical order: error domains, enums, +/// records, callback interfaces (which interface members take as +/// parameters), interfaces, and functions. fn emit_module(w: &mut CodeWriter, g: &Gen<'_>, m: &ModuleBinding) { - let error = g.model.error_domain(m); - if let Some(e) = m.errors.as_ref() { - render_error(w, g, m, e); + for t in g.errors_of(m) { + render_error(w, g, t); } for e in &m.enums { - render_enum(w, e); + render_enum(w, g, e); } for s in &m.structs { - render_struct(w, s); + render_struct(w, g, s); } for cb in &m.callback_interfaces { - render_callback_interface(w, g, cb, error.map(|e| e.type_name.as_str())); + render_callback_interface(w, g, cb); } for i in &m.interfaces { - render_interface(w, g, m, i); + render_interface(w, g, i); } for f in &m.functions { - render_bindings(w, g, f, error, &f.name, false); - render_callable(w, g, m, f, FnScope::Free); + render_bindings(w, g, f, false); + render_callable(w, g, f, FnScope::Free); } } @@ -216,10 +428,15 @@ fn emit_module(w: &mut CodeWriter, g: &Gen<'_>, m: &ModuleBinding) { /// (the package's `__init__.py`) re-exports the API and nothing else. fn render_all(w: &mut CodeWriter, g: &Gen<'_>) { let mut names = vec![g.root_error.clone(), g.trap_error.clone()]; + names.extend(RUNTIME_CLASSES.iter().map(|s| s.to_string())); for m in &g.model.modules { - if let Some(e) = &m.errors { - names.push(e.type_name.clone()); - names.extend(e.codes.iter().map(|c| py_code_class_name(&c.name))); + for t in g.errors_of(m) { + names.push(t.type_name.clone()); + names.extend( + t.codes + .iter() + .map(|c| g.code_class(&c.code.name).to_string()), + ); } for e in &m.enums { names.push(e.name.clone()); @@ -246,16 +463,27 @@ fn render_all(w: &mut CodeWriter, g: &Gen<'_>) { w.line("]"); } -impl LanguageBackend for PythonGenerator { - type Config = PythonConfig; - +impl Target for PythonGenerator { fn name(&self) -> &'static str { "python" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { + /// The import package, where the loader looks for a bundled library + /// first. + fn dev_bundle_dir(&self, model: &Model) -> Option { + Some(Utf8PathBuf::from( + Names::new(&model.identity, &self.config).import, + )) + } + + fn fixed_files(&self) -> &'static [&'static str] { + &["README.md", "__init__.py", "py.typed", "pyproject.toml"] + } + + fn render(&self, model: &Model) -> Vec { + let config = &self.config; let names = Names::new(&model.identity, config); - let dir = out_dir.join("python"); + let dir = Utf8PathBuf::new(); let pkg_dir = dir.join(&names.import); let mut files: Vec = self .package_sources(model, &names) @@ -281,12 +509,8 @@ impl LanguageBackend for PythonGenerator { /// One `py3-none-{platform}` wheel per desktop platform, with the /// library inside the import package where the loader looks first. /// Platforms without a wheel tag (Android, iOS, `wasm32`) are skipped. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let id = &model.identity; let names = Names::new(id, config); let sources = self.package_sources(model, &names); @@ -294,9 +518,7 @@ impl LanguageBackend for PythonGenerator { for nb in &ctx.binaries.binaries { let platform = nb.platform; let glibc = if platform.os() == Os::Linux { - std::fs::read(nb.library.as_std_path()) - .map(|bytes| glibc_requirement(&bytes)) - .unwrap_or((2, 17)) + glibc_requirement(&nb.library)? } else { (2, 17) }; @@ -321,6 +543,6 @@ impl LanguageBackend for PythonGenerator { files, }); } - Some(artifacts) + Ok(artifacts) } } diff --git a/crates/weaveffi-cli/src/targets/python/package.rs b/crates/weaveffi-cli/src/targets/python/package.rs index e68246f4..e6e7fb58 100644 --- a/crates/weaveffi-cli/src/targets/python/package.rs +++ b/crates/weaveffi-cli/src/targets/python/package.rs @@ -91,7 +91,7 @@ Python bindings for the `{library}` native library, built on `ctypes`. ## Requirements -- Python 3.9 or later +- Python 3.10 or later - The native library (`{linux}`, `{darwin}`, or `{windows}`) ## Install @@ -102,9 +102,11 @@ pip install . ## Loading the native library -The bindings load the library from `{env}` when it is set (a full path), +The bindings load the library from `{env}` when it's set (a full path), then from inside the installed package, then from the system loader's search -path. +path. Importing the package checks that the library is the one the bindings +were generated for; when it can't be loaded or doesn't match, the import +raises `LibraryLoadError`, an `ImportError`. ## Usage diff --git a/crates/weaveffi-cli/src/targets/python/runtime.rs b/crates/weaveffi-cli/src/targets/python/runtime.rs index 6dab8e1c..cad3a9c8 100644 --- a/crates/weaveffi-cli/src/targets/python/runtime.rs +++ b/crates/weaveffi-cli/src/targets/python/runtime.rs @@ -10,9 +10,10 @@ //! uses the feature. The contract table's entries are written into the //! writer where `core.py` says `{{CONTRACTS}}`. +use crate::codegen::contract; use crate::codegen::CodeWriter; -use weaveffi_model::contract; -use weaveffi_model::model::{contract_symbol, ABI_VERSION}; +use heck::ToSnakeCase; +use weaveffi_model::model::ABI_VERSION; use crate::targets::python::types::py_str_literal; use crate::targets::python::Gen; @@ -23,19 +24,20 @@ const CANCEL: &str = include_str!("runtime/cancel.py"); const CALLBACKS: &str = include_str!("runtime/callbacks.py"); /// Write the body of the `_CONTRACTS` table: per top-level module, its -/// contract symbol and the `(id, hash, path)` entries these bindings rely -/// on, sorted by id. +/// contract symbol and the `(id, hash, path)` rows these bindings rely on, +/// sorted by id, each with its canonical signature as a comment. fn render_contracts(w: &mut CodeWriter, g: &Gen<'_>) { w.scope(|w| { - for root in g.model.roots() { - w.line(format!("\"{}\": [", contract_symbol(g.prefix, &root.name))); + for table in contract::tables(g.model) { + w.line(format!("\"{}\": [", table.symbol)); w.scope(|w| { - for e in contract::entries(g.model, root) { + for row in &table.rows { w.line(format!( - "({:#018x}, {:#018x}, \"{}\"),", - e.id, - e.hash, - py_str_literal(&e.path) + "({}, {}, \"{}\"), # {}", + contract::hex(row.id), + contract::hex(row.hash), + py_str_literal(&row.path), + row.signature )); } }); @@ -52,6 +54,10 @@ fn fill(template: &str, g: &Gen<'_>) -> String { .replace("{{NAME}}", &id.name) .replace("{{ERROR}}", &g.root_error) .replace("{{TRAP}}", &g.trap_error) + .replace( + "{{ERROR_FROM}}", + &format!("_{}_from", g.root_error.to_snake_case()), + ) .replace("{{PREFIX}}", g.prefix) .replace("{{LIBRARY_ENV}}", &id.library_env_var()) .replace("{{LIB_DARWIN}}", &darwin) @@ -64,7 +70,7 @@ fn fill(template: &str, g: &Gen<'_>) -> String { pub(crate) fn render_runtime(w: &mut CodeWriter, g: &Gen<'_>) { let callbacks = g.model.has_callback_interfaces(); let is_async = g.model.has_async(); - let cancellable = g.model.callables().any(|(_, f)| f.cancellable); + let cancellable = g.model.callables().any(|(_, f)| f.cancellable()); let mut imports = String::new(); if callbacks { imports.push_str("import abc\n"); diff --git a/crates/weaveffi-cli/src/targets/python/runtime/aio.py b/crates/weaveffi-cli/src/targets/python/runtime/aio.py index f83cf533..c6ee279e 100644 --- a/crates/weaveffi-cli/src/targets/python/runtime/aio.py +++ b/crates/weaveffi-cli/src/targets/python/runtime/aio.py @@ -4,14 +4,14 @@ # The trampolines live at module scope, so nothing a pending call needs can # be garbage collected before the producer fires the completion, even when # the awaiting coroutine is gone. -_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, asyncio.Future[Any]]] = {} +_pending: dict[int, tuple[asyncio.AbstractEventLoop, asyncio.Future[Any]]] = {} _pending_lock = threading.Lock() _pending_next = 0 _error_free = _bind("{{PREFIX}}_error_free", None, ctypes.POINTER(_ErrorStruct)) -def _async_begin() -> Tuple[int, asyncio.Future[Any]]: +def _async_begin() -> tuple[int, asyncio.Future[Any]]: """Register a new pending call on the running event loop.""" global _pending_next loop = asyncio.get_running_loop() @@ -34,15 +34,15 @@ def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) exception it reports. The cancelled code maps to CancelledError.""" e = err.contents code = e.code - message = e.message.decode("utf-8", "replace") if e.message else "" - payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" + message = _peek_bytes(e.message_ptr, e.message_len).decode("utf-8", "replace") + payload = _peek_bytes(e.payload_ptr, e.payload_len) _error_free(err) if code == {{ERROR}}.CANCELLED_ERROR_CODE: return asyncio.CancelledError(message) return factory(code, message, payload) -def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: +def _async_settle(key: int | None, exc: BaseException | None, value: Any) -> None: """Hand a completion back to the event loop that awaits it.""" with _pending_lock: entry = _pending.pop(key or 0, None) @@ -56,7 +56,7 @@ def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) pass -def _async_resolve(future: asyncio.Future[Any], exc: Optional[BaseException], value: Any) -> None: +def _async_resolve(future: asyncio.Future[Any], exc: BaseException | None, value: Any) -> None: if future.done(): return if isinstance(exc, asyncio.CancelledError): diff --git a/crates/weaveffi-cli/src/targets/python/runtime/callbacks.py b/crates/weaveffi-cli/src/targets/python/runtime/callbacks.py index 0e59088e..08452572 100644 --- a/crates/weaveffi-cli/src/targets/python/runtime/callbacks.py +++ b/crates/weaveffi-cli/src/targets/python/runtime/callbacks.py @@ -5,13 +5,18 @@ # producer never holds a raw reference to a Python object. ctypes acquires # the GIL on entry to a trampoline, so the producer may call (and free) from # any thread. -_callbacks: Dict[int, Any] = {} +_callbacks: dict[int, Any] = {} _callbacks_lock = threading.Lock() _callbacks_next = 0 _alloc = _bind("{{PREFIX}}_alloc", ctypes.c_void_p, ctypes.c_size_t) _error_set = _bind( - "{{PREFIX}}_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p + "{{PREFIX}}_error_set", + None, + ctypes.POINTER(_ErrorStruct), + ctypes.c_int32, + ctypes.c_char_p, + ctypes.c_size_t, ) _error_set_payload = _bind( "{{PREFIX}}_error_set_payload", @@ -34,16 +39,16 @@ def _callback_register(impl: Any, cls: type) -> int: return ctx -def _callback_register_opt(impl: Any, cls: type) -> Optional[int]: +def _callback_register_opt(impl: Any, cls: type) -> int | None: """`_callback_register` for an optional callback: None passes none.""" return None if impl is None else _callback_register(impl, cls) -def _callback_get(ctx: Optional[int]) -> Any: +def _callback_get(ctx: int | None) -> Any: return _callbacks[ctx or 0] -def _callback_free(ctx: Optional[int]) -> None: +def _callback_free(ctx: int | None) -> None: # The producer's last reference is gone; it never passes `ctx` again. with _callbacks_lock: _callbacks.pop(ctx or 0, None) @@ -67,7 +72,24 @@ def _callback_return_bytes(out_ptr: Any, out_len: Any, data: bytes) -> None: out_len[0] = n -def _callback_return_object(obj: Any, cls: Type[_Object]) -> int: +def _callback_return_array( + out_ptr: Any, out_len: Any, values: Iterable[Any], kind: str, what: str +) -> None: + """Hand a typed-array return to the producer: a range-checked run + allocated with {{PREFIX}}_alloc, which the producer adopts and frees. + `out_len` is the element count.""" + data = _array(values, kind, what) + size = len(data) * data.itemsize + ptr = _alloc(size) if size else None + if size: + if not ptr: + raise MemoryError(f"{{PREFIX}}_alloc({size}) failed") + ctypes.memmove(ptr, data.buffer_info()[0], size) + out_ptr[0] = ptr + out_len[0] = len(data) + + +def _callback_return_object(obj: Any, cls: type[_Object]) -> int: """Hand an object return to the producer: one fresh strong reference it adopts.""" if not isinstance(obj, cls): @@ -75,25 +97,37 @@ def _callback_return_object(obj: Any, cls: Type[_Object]) -> int: return obj._clone_ref() -def _callback_return_object_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: +def _callback_return_object_opt(obj: Any, cls: type[_Object]) -> int | None: """`_callback_return_object` for an optional object: None returns null.""" return None if obj is None else _callback_return_object(obj, cls) -def _callback_fail(out_err: Any, exc: BaseException, domain: Optional[Type[{{ERROR}}]] = None) -> None: +# The codes a failed callback method reports when it raised anything but a +# code of its domain: -1 for a method that throws (a domain or `any`), -4 +# for one that declares no errors. +_GENERIC = {{ERROR}}.GENERIC_ERROR_CODE +_FOREIGN = {{ERROR}}.FOREIGN_ERROR_CODE + + +def _callback_fail( + out_err: Any, exc: BaseException, code: int, domain: type[{{ERROR}}] | None = None +) -> None: """Report an implementation's exception to the producer. A method that - declares errors passes its module's error domain, and an exception of - that domain carrying a declared code travels as that code with its - fields; every other exception reaches the producer as the foreign error - code (-4). Nothing unwinds through C.""" + throws an error domain passes it, and an exception of that domain + carrying a positive code travels as that code with its fields as the + payload; every other exception travels as `code` with its message. + Nothing unwinds through C.""" if domain is not None and isinstance(exc, domain) and exc.code > 0: - _error_set(out_err, exc.code, exc.message.encode("utf-8", "replace")) + code = exc.code payload = exc._payload() - if payload: - _error_set_payload(out_err, payload, len(payload)) - return - if isinstance(exc, ({{ERROR}}, {{TRAP}})): message = exc.message else: - message = str(exc) or type(exc).__name__ - _error_set(out_err, {{ERROR}}.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) + payload = b"" + if isinstance(exc, ({{ERROR}}, {{TRAP}})): + message = exc.message + else: + message = str(exc) or type(exc).__name__ + data = message.encode("utf-8", "replace") + _error_set(out_err, code, data, len(data)) + if payload: + _error_set_payload(out_err, payload, len(payload)) diff --git a/crates/weaveffi-cli/src/targets/python/runtime/core.py b/crates/weaveffi-cli/src/targets/python/runtime/core.py index 5216033d..c151bc31 100644 --- a/crates/weaveffi-cli/src/targets/python/runtime/core.py +++ b/crates/weaveffi-cli/src/targets/python/runtime/core.py @@ -1,28 +1,32 @@ """ctypes bindings for the {{NAME}} native library.""" from __future__ import annotations -{{IMPORTS}}import builtins +{{IMPORTS}}import array +import builtins import ctypes +import operator import os import platform import struct import threading import warnings +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass from enum import IntEnum -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar +from typing import Any, ClassVar, Generic, TypeVar class {{ERROR}}(Exception): """An error a fallible call reports. `code` is positive for one of the API's declared error codes, raised as - the declaring module's typed subclasses, and negative for a runtime - failure of a call that declares errors: a generic failure (-1), a - producer panic (-2), a marshalling failure (-3), or an exception raised - by a callback-interface implementation (-4). A cancelled async call (-5) - raises `asyncio.CancelledError` instead. A call that declares no errors - never raises this; its failures are bugs and raise `{{TRAP}}`. + the domain's typed subclasses, and negative for a runtime failure of a + call that throws: a generic failure (-1, also every failure of a call + declared `throws: any`), a producer panic (-2), a marshalling failure + (-3), or a failed callback-interface implementation (-4). A cancelled + async call (-5) raises `asyncio.CancelledError` instead. A call that + declares no errors never raises this; its failures are bugs and raise + `{{TRAP}}`. """ GENERIC_ERROR_CODE = -1 @@ -55,23 +59,41 @@ def __init__(self, code: int, message: str) -> None: self.message = message +class LibraryLoadError(ImportError): + """The native library couldn't be loaded, or it isn't the library these + bindings were generated for (another C ABI revision, or a declaration + that's missing or changed). Raised while the package is imported, so + catch it as `ImportError` around the import; `path` names the library. + """ + + class _ErrorStruct(ctypes.Structure): """The C `{{PREFIX}}_error` out-parameter every call reports through.""" _fields_ = [ ("code", ctypes.c_int32), - ("message", ctypes.c_char_p), + ("message_ptr", ctypes.c_void_p), + ("message_len", ctypes.c_size_t), ("payload_ptr", ctypes.c_void_p), ("payload_len", ctypes.c_size_t), ] +def _open(path: str, what: str) -> ctypes.CDLL: + try: + return ctypes.CDLL(path) + except OSError as exc: + raise LibraryLoadError( + f"cannot load {what}: {exc}", name=__name__, path=path + ) from exc + + def _load_library() -> ctypes.CDLL: # An explicit path wins; then a library bundled inside this package (a # platform wheel); then the system loader's search path. override = os.environ.get("{{LIBRARY_ENV}}") if override: - return ctypes.CDLL(override) + return _open(override, f"the {{NAME}} native library from {{LIBRARY_ENV}}={override!r}") system = platform.system() if system == "Darwin": name = "{{LIB_DARWIN}}" @@ -81,14 +103,12 @@ def _load_library() -> ctypes.CDLL: name = "{{LIB_LINUX}}" bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) if os.path.exists(bundled): - return ctypes.CDLL(bundled) - try: - return ctypes.CDLL(name) - except OSError as exc: - raise ImportError( - f"cannot load the native library {name!r} ({exc}); " - "set {{LIBRARY_ENV}} to its path" - ) from exc + return _open(bundled, f"the bundled {{NAME}} native library {bundled!r}") + return _open( + name, + f"the {{NAME}} native library {name!r} (set {{LIBRARY_ENV}} to its path, " + "or install a wheel that bundles it)", + ) _lib = _load_library() @@ -111,13 +131,17 @@ class _ContractEntry(ctypes.Structure): # The C ABI revision these bindings were generated against and, per # top-level module's contract symbol, the declarations they rely on: -# (id, hash, dotted path). +# (id, hash, dotted path), with each declaration's canonical signature. _ABI_VERSION = {{ABI_VERSION}} -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { +_CONTRACTS: dict[str, list[tuple[int, int, str]]] = { {{CONTRACTS}} } +def _refuse(message: str) -> LibraryLoadError: + return LibraryLoadError(f"{message} ({_lib._name})", name=__name__, path=_lib._name) + + def _check_contract() -> None: """Refuse to load a library built for another ABI revision, or one that lacks or changed a declaration these bindings use, before any other @@ -125,15 +149,16 @@ def _check_contract() -> None: try: abi_version = _bind("{{PREFIX}}_abi_version", ctypes.c_uint32) except AttributeError: - raise ImportError( - f"{_lib._name} does not export {{PREFIX}}_abi_version; " - "it is not the library these bindings were generated for" + raise _refuse( + "the library doesn't export {{PREFIX}}_abi_version, so it isn't the " + "{{NAME}} library these bindings were generated for" ) from None found = abi_version() if found != _ABI_VERSION: - raise ImportError( - f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " - f"but {_lib._name} reports revision {found}" + raise _refuse( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, but the " + f"library reports revision {found}; regenerate the bindings or rebuild " + "the library" ) for symbol, expected in _CONTRACTS.items(): try: @@ -141,18 +166,16 @@ def _check_contract() -> None: symbol, ctypes.POINTER(_ContractEntry), ctypes.POINTER(ctypes.c_size_t) ) except AttributeError: - raise ImportError(f"{_lib._name} does not export {symbol}") from None + raise _refuse(f"the library doesn't export {symbol}") from None length = ctypes.c_size_t() table = contract(ctypes.byref(length)) hashes = {table[i].id: table[i].hash for i in range(length.value)} if table else {} for entry_id, entry_hash, path in expected: actual = hashes.get(entry_id) if actual is None: - raise ImportError(f"{path} is missing from the library ({_lib._name})") + raise _refuse(f"{path} is missing from the library") if actual != entry_hash: - raise ImportError( - f"{path} changed since these bindings were generated ({_lib._name})" - ) + raise _refuse(f"{path} changed since these bindings were generated") _check_contract() @@ -171,12 +194,18 @@ def _debug_live(kind: int) -> int: return live -def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: +def _peek_bytes(ptr: int | None, length: int) -> bytes: + """Copy a byte run the producer only lends (a zero-length run's pointer + is never read).""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +def _read_error(err: _ErrorStruct) -> tuple[int, str, bytes]: """Copy a filled out-err slot's code, message, and payload, then release them.""" code = err.code - message = err.message.decode("utf-8", "replace") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + message = _peek_bytes(err.message_ptr, err.message_len).decode("utf-8", "replace") + payload = _peek_bytes(err.payload_ptr, err.payload_len) _error_clear(ctypes.byref(err)) return code, message, payload @@ -186,7 +215,19 @@ def _trap_from(code: int, message: str, payload: bytes = b"") -> {{TRAP}}: return {{TRAP}}(code, message) -def _take_bytes(ptr: Optional[int], length: int) -> bytes: +def {{ERROR_FROM}}(code: int, message: str, payload: bytes = b"") -> {{ERROR}}: + """The exception for a failed call declared `throws: any`.""" + return {{ERROR}}(code, message) + + +def _unknown_code(domain: type[{{ERROR}}], code: int, message: str) -> {{ERROR}}: + """The exception for a code a domain's factory doesn't know: a positive + code (from a library newer than these bindings) raises the domain's base + class with the code and message; a runtime code raises the root error.""" + return domain(code, message) if code > 0 else {{ERROR}}(code, message) + + +def _take_bytes(ptr: int | None, length: int) -> bytes: """Copy a producer-owned byte run (a string, bytes, or value buffer) and release it with {{PREFIX}}_free_bytes.""" if not ptr: @@ -197,17 +238,100 @@ def _take_bytes(ptr: Optional[int], length: int) -> bytes: _free_bytes(ptr, length) -def _take_str(ptr: Optional[int], length: int) -> str: +def _take_str(ptr: int | None, length: int) -> str: """Copy and release a producer-owned UTF-8 string.""" return _take_bytes(ptr, length).decode("utf-8") -def _peek_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a byte run the producer only lends for the current call.""" - return ctypes.string_at(ptr, length) if ptr and length else b"" +# Range checks. ctypes truncates an out-of-range integer argument to the C +# width silently, so every integer crossing directly is checked first. + + +def _int_check(kind: str, lo: int, hi: int) -> Callable[[int, str], int]: + def check(value: int, what: str) -> int: + try: + v = operator.index(value) + except TypeError: + raise TypeError(f"{what}: expected an integer, got {type(value).__name__}") from None + if lo <= v <= hi: + return v + raise OverflowError(f"{what}: {v} is out of range for {kind}") + + return check + + +_i8 = _int_check("i8", -(1 << 7), (1 << 7) - 1) +_i16 = _int_check("i16", -(1 << 15), (1 << 15) - 1) +_i32 = _int_check("i32", -(1 << 31), (1 << 31) - 1) +_i64 = _int_check("i64", -(1 << 63), (1 << 63) - 1) +_u8 = _int_check("u8", 0, (1 << 8) - 1) +_u16 = _int_check("u16", 0, (1 << 16) - 1) +_u32 = _int_check("u32", 0, (1 << 32) - 1) +_u64 = _int_check("u64", 0, (1 << 64) - 1) + + +def _float(value: float, what: str) -> float: + """A number as a float, rejecting anything else.""" + if isinstance(value, (int, float)): + return float(value) + raise TypeError(f"{what}: expected a number, got {type(value).__name__}") + + +# Typed arrays: numeric lists crossing directly. Each element kind's +# `array` typecode and ctypes element type. +_ARRAY_TYPES: dict[str, tuple[str, Any]] = { + "i8": ("b", ctypes.c_int8), + "i16": ("h", ctypes.c_int16), + "i32": ("i", ctypes.c_int32), + "i64": ("q", ctypes.c_int64), + "u16": ("H", ctypes.c_uint16), + "u32": ("I", ctypes.c_uint32), + "u64": ("Q", ctypes.c_uint64), + "f32": ("f", ctypes.c_float), + "f64": ("d", ctypes.c_double), +} + + +def _array(values: Iterable[Any], kind: str, what: str) -> array.array[Any]: + """Pack a sequence of numbers into a range-checked typed array whose + buffer is lent to one call. An `array.array` of the matching typecode + is lent as is, without a copy.""" + code = _ARRAY_TYPES[kind][0] + if isinstance(values, array.array) and values.typecode == code: + return values + if isinstance(values, (str, bytes, bytearray, memoryview)): + raise TypeError(f"{what}: expected a sequence of numbers, got {type(values).__name__}") + try: + return array.array(code, values) + except OverflowError: + raise OverflowError(f"{what}: an element is out of range for {kind}") from None + except TypeError as exc: + raise TypeError(f"{what}: {exc}") from None + + +def _peek_array(ptr: int | None, count: int, kind: str) -> list[Any]: + """Copy a typed array the producer only lends.""" + if not ptr or not count: + return [] + values: list[Any] = (_ARRAY_TYPES[kind][1] * count).from_address(ptr)[:] + return values + + +def _take_array(ptr: int | None, count: int, kind: str) -> list[Any]: + """Copy a producer-owned typed array and release it with + {{PREFIX}}_free_bytes.""" + if not ptr: + return [] + ctype = _ARRAY_TYPES[kind][1] + try: + values: list[Any] = (ctype * count).from_address(ptr)[:] + return values + finally: + _free_bytes(ptr, count * ctypes.sizeof(ctype)) _H = TypeVar("_H", bound="_Handle") +_T = TypeVar("_T") class _Handle: @@ -218,22 +342,15 @@ class _Handle: last such call returns instead of freeing the object mid-call. """ - # The native destroy (or iterator release) function, set per subclass. - _destroy: Callable[[int], None] - def _init_handle(self, ptr: int) -> None: - self._ptr: Optional[int] = ptr + self._ptr: int | None = ptr self._key = ptr self._calls = 0 self._closing = False self._lock = threading.Lock() - @classmethod - def _adopt(cls: Type[_H], ptr: int) -> _H: - """Wrap one strong reference the producer handed over.""" - obj = cls.__new__(cls) - obj._init_handle(ptr) - return obj + def _free(self, ptr: int) -> None: + """Release the native reference (each subclass releases its kind).""" def _acquire(self) -> int: with self._lock: @@ -250,10 +367,10 @@ def _release(self) -> None: return ptr, self._ptr = self._ptr, None if ptr is not None: - type(self)._destroy(ptr) + self._free(ptr) def close(self) -> None: - """Release the native reference. Idempotent; the wrapper is unusable + """Release the native reference. Idempotent; the object is unusable afterwards.""" lock = getattr(self, "_lock", None) if lock is None: @@ -266,7 +383,7 @@ def close(self) -> None: return ptr, self._ptr = self._ptr, None if ptr is not None: - type(self)._destroy(ptr) + self._free(ptr) def __enter__(self: _H) -> _H: return self @@ -275,19 +392,30 @@ def __exit__(self, *exc: object) -> None: self.close() def __del__(self) -> None: - # An unreachable wrapper has no call in flight: release directly. + # An unreachable handle has no call in flight: release directly. ptr = getattr(self, "_ptr", None) if ptr is not None: self._ptr = None - type(self)._destroy(ptr) + self._free(ptr) class _Object(_Handle): """Base of every interface wrapper: a reference-counted native object. Two wrappers are equal when they hold the same native object.""" - # The native clone function, set per subclass. - _clone: Callable[[int], int] + # The native destroy and clone functions, set per subclass. + _destroy: ClassVar[Callable[[int], None]] + _clone: ClassVar[Callable[[int], int]] + + @classmethod + def _adopt(cls: type[_H], ptr: int) -> _H: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _free(self, ptr: int) -> None: + type(self)._destroy(ptr) def _clone_ref(self) -> int: """A new strong reference to the same object, for a receiver that @@ -307,59 +435,93 @@ def __hash__(self) -> int: return hash(self._key) -class _Iterator(_Handle): - """Base of every lazy iterator over a producer stream: one native `next` - call per step; the handle is released on exhaustion, `close()`, or - garbage collection.""" +class NativeIterator(Iterator[_T], _Handle, Generic[_T]): + """A lazy iterator over a sequence the native library produces. - def __iter__(self: _H) -> _H: - return self + Each step pulls one element from the producer. The native handle is + released when the iterator is exhausted, on `close()`, at the end of a + `with` block, or by garbage collection; a closed iterator is exhausted. + Iterator-returning calls create these; there's no public constructor. + """ + + _pull: Callable[[int], _T] + _destroy_fn: Callable[[int], None] + + def __init__(self) -> None: + raise TypeError("NativeIterator objects come from the API's iterator-returning calls") + + def _free(self, ptr: int) -> None: + self._destroy_fn(ptr) - def _step(self) -> int: + def __next__(self) -> _T: with self._lock: ptr = self._ptr if ptr is None or self._closing: raise StopIteration self._calls += 1 - return ptr + try: + item = self._pull(ptr) + except StopIteration: + self._release() + self.close() + raise + except BaseException: + self._release() + raise + self._release() + return item + + def __repr__(self) -> str: + state = "closed" if self._ptr is None or self._closing else "open" + return f"" + +def _iterate( + ptr: int, pull: Callable[[int], _T], destroy: Callable[[int], None] +) -> NativeIterator[_T]: + """Wrap an iterator handle the producer returned.""" + it: NativeIterator[_T] = NativeIterator.__new__(NativeIterator) + it._init_handle(ptr) + it._pull = pull + it._destroy_fn = destroy + return it -def _required(ptr: Optional[int]) -> int: + +def _required(ptr: int | None) -> int: """A non-null object pointer the producer returned.""" if not ptr: raise {{TRAP}}(-3, "the producer returned a null object") return ptr -def _lend(obj: Any, cls: Type[_Object]) -> int: +def _lend(obj: Any, cls: type[_Object]) -> int: """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" if not isinstance(obj, cls): raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") return obj._acquire() -def _lend_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: +def _lend_opt(obj: Any, cls: type[_Object]) -> int | None: """`_lend` for an optional object: None lends a null pointer.""" return None if obj is None else _lend(obj, cls) -def _release_opt(obj: Optional[_Object]) -> None: +def _release_opt(obj: _Object | None) -> None: if obj is not None: obj._release() -_I8 = struct.Struct(" {{TRAP}}: return {{TRAP}}(-3, f"malformed value buffer: {what}") +def _bad_value(kind: str, value: object) -> Exception: + """The exception for a value `struct` can't pack as `kind`: out of range + for a number, else the wrong type.""" + if isinstance(value, (int, float)): + return OverflowError(f"{value} is out of range for {kind}") + return TypeError(f"expected a number for {kind}, got {type(value).__name__}") + + class _Writer: """Encodes values in the value-buffer wire format: little-endian, packed, no alignment.""" @@ -375,17 +545,17 @@ class _Writer: def __init__(self) -> None: self._buf = bytearray() - self._objects: List[Tuple[int, _Object]] = [] + self._objects: list[tuple[int, _Object]] = [] def finish(self) -> bytes: # Object tokens are minted last, so an encoding that fails part-way # never leaks a strong reference. - minted: List[Tuple[_Object, int]] = [] + minted: list[tuple[_Object, int]] = [] try: for pos, obj in self._objects: ptr = obj._clone_ref() minted.append((obj, ptr)) - _U64.pack_into(self._buf, pos, ptr) + _S_U64.pack_into(self._buf, pos, ptr) except BaseException: for obj, ptr in minted: type(obj)._destroy(ptr) @@ -393,61 +563,77 @@ def finish(self) -> bytes: self._objects = [] return bytes(self._buf) + def _put(self, fmt: struct.Struct, kind: str, v: Any) -> None: + try: + self._buf += fmt.pack(v) + except struct.error: + raise _bad_value(kind, v) from None + def write_bool(self, v: bool) -> None: self._buf.append(1 if v else 0) def write_i8(self, v: int) -> None: - self._buf += _I8.pack(v) + self._put(_S_I8, "i8", v) def write_u8(self, v: int) -> None: - self._buf += _U8.pack(v) + self._put(_S_U8, "u8", v) def write_i16(self, v: int) -> None: - self._buf += _I16.pack(v) + self._put(_S_I16, "i16", v) def write_u16(self, v: int) -> None: - self._buf += _U16.pack(v) + self._put(_S_U16, "u16", v) def write_i32(self, v: int) -> None: - self._buf += _I32.pack(v) + self._put(_S_I32, "i32", v) def write_u32(self, v: int) -> None: - self._buf += _U32.pack(v) + self._put(_S_U32, "u32", v) def write_i64(self, v: int) -> None: - self._buf += _I64.pack(v) + self._put(_S_I64, "i64", v) def write_u64(self, v: int) -> None: - self._buf += _U64.pack(v) + self._put(_S_U64, "u64", v) def write_f32(self, v: float) -> None: - self._buf += _F32.pack(v) + self._put(_S_F32, "f32", v) def write_f64(self, v: float) -> None: - self._buf += _F64.pack(v) + self._put(_S_F64, "f64", v) def write_count(self, n: int) -> None: - self._buf += _U32.pack(n) + self._buf += _S_U32.pack(n) def write_flag(self, present: bool) -> None: self._buf.append(1 if present else 0) def write_string(self, v: str) -> None: data = v.encode("utf-8") - self._buf += _U32.pack(len(data)) + self._buf += _S_U32.pack(len(data)) self._buf += data def write_bytes(self, v: bytes) -> None: - self._buf += _U32.pack(len(v)) + self._buf += _S_U32.pack(len(v)) self._buf += v - def write_numbers(self, code: str, values: List[Any]) -> None: + def write_numbers(self, code: str, kind: str, values: Sequence[Any]) -> None: """A list of one fixed-width numeric primitive (`code` is its struct format character), packed in one call.""" - self._buf += _U32.pack(len(values)) - self._buf += struct.pack(f"<{len(values)}{code}", *values) + try: + data = struct.pack(f"<{len(values)}{code}", *values) + except struct.error: + one = struct.Struct(f"<{code}") + for v in values: + try: + one.pack(v) + except struct.error: + raise _bad_value(kind, v) from None + raise + self._buf += _S_U32.pack(len(values)) + self._buf += data - def write_object(self, obj: Any, cls: Type[_Object]) -> None: + def write_object(self, obj: Any, cls: type[_Object]) -> None: """Reserve an object token: one strong reference to `obj` the reader adopts, cloned in `finish()`. A closed wrapper is rejected now.""" if not isinstance(obj, cls): @@ -483,39 +669,39 @@ def read_bool(self) -> bool: return b == 1 def read_i8(self) -> int: - return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + return int(_S_I8.unpack_from(self._data, self._take(1, "i8"))[0]) def read_u8(self) -> int: return self._data[self._take(1, "u8")] def read_i16(self) -> int: - return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + return int(_S_I16.unpack_from(self._data, self._take(2, "i16"))[0]) def read_u16(self) -> int: - return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + return int(_S_U16.unpack_from(self._data, self._take(2, "u16"))[0]) def read_i32(self) -> int: - return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + return int(_S_I32.unpack_from(self._data, self._take(4, "i32"))[0]) def read_u32(self) -> int: - return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + return int(_S_U32.unpack_from(self._data, self._take(4, "u32"))[0]) def read_i64(self) -> int: - return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + return int(_S_I64.unpack_from(self._data, self._take(8, "i64"))[0]) def read_u64(self) -> int: - return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + return int(_S_U64.unpack_from(self._data, self._take(8, "u64"))[0]) def read_f32(self) -> float: - return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + return float(_S_F32.unpack_from(self._data, self._take(4, "f32"))[0]) def read_f64(self) -> float: - return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + return float(_S_F64.unpack_from(self._data, self._take(8, "f64"))[0]) def read_count(self) -> int: # An element count, not a byte length: elements can encode to zero - # bytes, so it is not bounded by the remaining data. - return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + # bytes, so it isn't bounded by the remaining data. + return int(_S_U32.unpack_from(self._data, self._take(4, "count"))[0]) def read_flag(self) -> bool: b = self._data[self._take(1, "option flag")] @@ -524,7 +710,7 @@ def read_flag(self) -> bool: return b == 1 def read_bytes(self) -> bytes: - n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + n = int(_S_U32.unpack_from(self._data, self._take(4, "length"))[0]) pos = self._take(n, "bytes") return self._data[pos:pos + n] @@ -534,14 +720,14 @@ def read_string(self) -> str: except UnicodeDecodeError: raise _malformed("string is not valid UTF-8") from None - def read_enum(self, cls: Type[_E]) -> _E: + def read_enum(self, cls: type[_E]) -> _E: value = self.read_i32() try: return cls(value) except ValueError: raise _malformed(f"undeclared {cls.__name__} value {value}") from None - def read_numbers(self, code: str, size: int) -> List[Any]: + def read_numbers(self, code: str, size: int) -> list[Any]: """A list of one fixed-width numeric primitive, unpacked in one call.""" n = self.read_count() pos = self._take(n * size, "list") @@ -549,7 +735,7 @@ def read_numbers(self, code: str, size: int) -> List[Any]: def read_object(self) -> int: """An object token: one strong reference the caller adopts.""" - ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + ptr = int(_S_U64.unpack_from(self._data, self._take(8, "object token"))[0]) if ptr == 0: raise _malformed("null object token") return ptr diff --git a/crates/weaveffi-cli/src/targets/python/tests.rs b/crates/weaveffi-cli/src/targets/python/tests.rs index 50e25068..2c2b8fd4 100644 --- a/crates/weaveffi-cli/src/targets/python/tests.rs +++ b/crates/weaveffi-cli/src/targets/python/tests.rs @@ -1,16 +1,13 @@ -//! Unit tests: render a small API that exercises every ABI revision 4 shape -//! (prefixed runtime, contract table, pointer + length strings, -//! reference-counted objects, nullable objects, objects inside buffers, -//! composite codecs, typed errors with payloads, iterators, async and -//! cancellable functions, and callback interfaces with every return family, -//! a throwing method, and an optional parameter) and assert the generated -//! Python carries the pieces each contract clause requires. +//! Unit tests of what's specific to the Python target: the package layout +//! and configuration, the identifier policy (keyword and builtin escaping, +//! exception and code class names), feature-gated runtime parts, range +//! checks placed so a failure strands nothing, and wheel packaging. The +//! `kitchen_sink` snapshot pins the rest of the generated module. -use crate::backend::{LanguageBackend, OutputFile}; +use crate::codegen::OutputFile; use crate::package::{ArtifactKind, PackageContext}; use crate::platform::{BinarySet, NativeBinary, Platform}; -use camino::Utf8Path; -use weaveffi_model::contract; +use crate::targets::Target; use weaveffi_model::model::Model; use weaveffi_model::parse::parse_api_str; use weaveffi_model::pkg::Identity; @@ -19,59 +16,33 @@ use weaveffi_model::validate::validate; use crate::targets::python::{PythonConfig, PythonGenerator}; const KV: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: kv errors: - name: KvError - codes: - - { name: NotFound, code: 1, message: "not found", fields: [{ name: key, type: string }] } - - { name: Busy, code: 2, message: "busy" } + - name: KvError + codes: + - { name: NotFound, code: 1, message: "not found", fields: [{ name: key, type: string }] } + - { name: Timeout, code: 2, message: "timed out" } structs: - name: Entry fields: - { name: key, type: string } - - { name: store, type: Store } - - { name: mirrors, type: "[Store]" } - - { name: weights, type: "[i32]" } - - { name: labels, type: "{string:i64?}" } + - { name: int, type: "[i32]" } interfaces: - name: Store - doc: A key-value store. constructors: - { name: new, params: [{ name: path, type: string }] } methods: - - { name: get, params: [{ name: key, type: string }], return: string, throws: true } - - { name: has, params: [{ name: key, type: string }], return: bool } - - { name: size, params: [], return: u32, deprecated: "use count" } + - { name: get, params: [{ name: key, type: string }], return: string, throws: KvError } callback_interfaces: - name: Watcher - doc: Observes store changes. methods: - - name: on_change - params: - - { name: text, type: string } - - { name: entry, type: Entry } - - { name: store, type: Store } - - { name: should_stop, params: [{ name: count, type: i32 }], return: bool } - - { name: label, params: [], return: string, throws: true } - - { name: snapshot, params: [], return: Entry } - - { name: pick, params: [{ name: store, type: "Store?" }], return: "Store?" } - - { name: home, params: [], return: Store } + - { name: on_change, params: [{ name: count, type: i32 }] } functions: - - name: pick - params: [{ name: store, type: "Store?" }] - return: "Store?" - - { name: watch, params: [{ name: watcher, type: Watcher }] } - - { name: maybe_watch, params: [{ name: watcher, type: "Watcher?" }] } + - { name: watch, params: [{ name: watcher, type: Watcher }, { name: limit, type: u8 }] } + - { name: scale, params: [{ name: len, type: i16 }, { name: class, type: "u32?" }], return: i16 } - { name: all_stores, params: [], return: "iter" } - - { name: fetch, params: [{ name: id, type: i64 }], return: Store, async: true } - - name: sleep - params: [{ name: ms, type: i64 }] - return: string - async: true - cancellable: true - throws: true "#; fn model(yaml: &str) -> Model { @@ -81,7 +52,7 @@ fn model(yaml: &str) -> Model { } fn render_files(model: &Model, config: &PythonConfig) -> Vec { - PythonGenerator.files(model, Utf8Path::new("out"), config) + PythonGenerator::from(config.clone()).render(model) } /// Render `model` and return the implementation module. @@ -104,329 +75,33 @@ fn layout_follows_the_identity() { assert_eq!( paths, [ - "out/python/kv_lib/__init__.py", - "out/python/kv_lib/kv_lib.py", - "out/python/kv_lib/py.typed", - "out/python/pyproject.toml", - "out/python/README.md", + "kv_lib/__init__.py", + "kv_lib/kv_lib.py", + "kv_lib/py.typed", + "pyproject.toml", + "README.md", ] ); assert_has(&files[0].contents, "from .kv_lib import *"); let pyproject = &files[3].contents; assert_has(pyproject, "name = \"kv-lib\""); - assert_has(pyproject, "requires-python = \">=3.9\""); + assert_has(pyproject, "requires-python = \">=3.10\""); assert_has(pyproject, "packages = [\"kv_lib\"]"); assert_has(pyproject, "\"kv_lib\" = [\"py.typed\", \"*.so\""); + assert_has(&files[4].contents, "- Python 3.10 or later"); let config = PythonConfig { name: Some("kv-python".into()), import_name: Some("kvpy".into()), - ..PythonConfig::default() + requires_python: ">=3.12".into(), }; let files = render_files(&model(KV), &config); - assert!(files.iter().any(|f| f.path == "out/python/kvpy/kvpy.py")); + assert!(files.iter().any(|f| f.path == "kvpy/kvpy.py")); let pyproject = files.iter().find(|f| f.path.ends_with("pyproject.toml")); assert_has(&pyproject.unwrap().contents, "name = \"kv-python\""); -} - -#[test] -fn runtime_checks_the_revision_and_every_contract_entry() { - let m = model(KV); - let py = render(&m); - assert_has(&py, "from __future__ import annotations\n"); - assert_has(&py, "_ABI_VERSION = 4\n"); - assert_has(&py, " \"kv_lib_kv_contract\": [\n"); - let root = m.roots().next().unwrap(); - for e in contract::entries(&m, root) { - assert_has( - &py, - &format!("({:#018x}, {:#018x}, \"{}\"),", e.id, e.hash, e.path), - ); - } - assert_has( - &py, - "raise ImportError(f\"{path} is missing from the library", - ); - assert_has(&py, "f\"{path} changed since these bindings were generated"); - assert!(!py.contains("checksum"), "no trace of the checksum check"); - assert_has(&py, "_bind(\"kv_lib_abi_version\", ctypes.c_uint32)"); - assert_has(&py, "os.environ.get(\"KV_LIB_LIBRARY\")"); - assert_has(&py, "name = \"libkv_lib.dylib\""); - assert_has( - &py, - "_bind(\"kv_lib_free_bytes\", None, ctypes.c_void_p, ctypes.c_size_t)", - ); - assert_has( - &py, - "_bind(\"kv_lib_alloc\", ctypes.c_void_p, ctypes.c_size_t)", - ); - assert_has(&py, "\"kv_lib_error_set_payload\","); - assert!(!py.contains("dealloc")); - assert_has(&py, "class Error(Exception):"); - assert_has(&py, "class InternalError(RuntimeError):"); - // Nothing but the generated-file header names WeaveFFI. - for line in py.lines().filter(|l| !l.starts_with('#')) { - assert!( - !line.to_ascii_lowercase().contains("weaveffi"), - "branded line: {line}" - ); - } -} - -#[test] -fn failures_raise_the_domain_or_the_unchecked_trap() { - let py = render(&model(KV)); - // A throwing call raises its domain; any other failure traps. - assert_has( - &py, - " if _err.code:\n raise _kv_error_from(*_read_error(_err))\n \ - return _take_str(_ret, _out_len.value)", - ); - assert_has( - &py, - " if _err.code:\n raise _trap_from(*_read_error(_err))\n \ - return _ret", - ); - // Codes carry their payload fields as constructor arguments. - assert_has(&py, "class NotFound(KvError):"); - assert_has( - &py, - " def __init__(self, key: str, message: str = \"not found\") -> None:", - ); - assert_has( - &py, - " return _decode(payload, lambda _r: NotFound(\n \ - key=_r.read_string(),\n message=message,\n ))", - ); - assert_has(&py, " def _payload(self) -> bytes:"); - assert_has(&py, "KvError.NotFound = NotFound"); -} - -#[test] -fn prototypes_are_bound_once_with_c_bool_and_ptr_len_strings() { - let py = render(&model(KV)); - assert_has( - &py, - "_c_kv_Store_has = _bind(\"kv_lib_kv_Store_has\", ctypes.c_bool, ctypes.c_void_p, \ - ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct))", - ); - assert_eq!(py.matches("_c_kv_Store_get = _bind(").count(), 1); - assert!(!py.contains(".argtypes = ["), "no per-call binding"); - // A string crosses as UTF-8 bytes plus a length and comes back owned. - assert_has(&py, "_key_b = key.encode(\"utf-8\")"); - assert_has(&py, "_key_b, len(_key_b)"); - // A direct result is annotated for checkers. - assert_has(&py, "_ret: bool = _c_kv_Store_has("); - // Deprecation warns and is documented. - assert_has( - &py, - "warnings.warn(\"use count\", DeprecationWarning, stacklevel=2)", - ); - assert_has(&py, "Deprecated: use count"); -} - -#[test] -fn objects_are_lent_for_calls_and_adopted_from_returns() { - let py = render(&model(KV)); - assert_has(&py, "class Store(_Object):"); - assert_has(&py, " _destroy = staticmethod(_c_kv_Store_destroy)\n"); - assert_has(&py, " _clone = staticmethod(_c_kv_Store_clone)\n"); - assert_has(&py, "def __init__(self, path: str) -> None:"); - assert_has(&py, "self._init_handle(_required(_ret))"); - // `self` is lent for the call and released after it, even on error. - assert_has( - &py, - "_self_p = self._acquire()\n try:\n _ret = _c_kv_Store_get(_self_p,", - ); - assert_has(&py, " finally:\n self._release()\n"); - // Nullable objects map None to NULL both ways. - assert_has(&py, "_store_p = _lend_opt(store, Store)"); - assert_has(&py, "_release_opt(store)"); - assert_has(&py, "return Store._adopt(_ret) if _ret else None"); -} - -#[test] -fn composites_get_one_codec_function_each() { - let py = render(&model(KV)); - // Objects inside buffers are cloned when the buffer is finished and - // adopted on read. - assert_has(&py, "_w.write_object(value.store, Store)"); - assert_has(&py, "store=Store._adopt(_r.read_object()),"); - // Record fields delegate to one function per composite type. - assert_has(&py, " _write_list_Store(_w, value.mirrors)\n"); - assert_has(&py, " mirrors=_read_list_Store(_r),\n"); - assert_eq!(py.matches("def _write_list_Store(").count(), 1); - assert_eq!(py.matches("def _read_list_Store(").count(), 1); - assert_has( - &py, - "def _read_list_Store(_r: _Reader) -> List[Store]:\n \ - return [Store._adopt(_r.read_object()) for _ in range(_r.read_count())]", - ); - // Numeric lists take the packed fast path. - assert_has(&py, " _w.write_numbers(\"i\", value)\n"); - assert_has(&py, " return _r.read_numbers(\"i\", 4)\n"); - // Maps reject a repeated key; nested composites call each other. - assert_has( - &py, - "def _read_map_string_opt_i64(_r: _Reader) -> Dict[str, Optional[int]]:\n \ - n = _r.read_count()\n \ - value = {_r.read_string(): _read_opt_i64(_r) for _ in range(n)}", - ); - assert_has(&py, "raise _malformed(\"repeated map key\")"); - assert!(!py.contains("for _e0 in"), "no loop inlined at a call site"); -} - -#[test] -fn iterators_pull_lazily_through_prebound_functions() { - let py = render(&model(KV)); - assert_has(&py, "class _kv_AllStoresIterator(_Iterator):"); - assert_has( - &py, - " _destroy = staticmethod(_c_kv_AllStoresIterator_destroy)", - ); - assert_has( - &py, - "_more = _c_kv_AllStoresIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err))", - ); - assert_has(&py, "return Store._adopt(_required(_item.value))"); - assert_has(&py, "return _kv_AllStoresIterator._adopt(_ret)"); - assert_has(&py, "def all_stores() -> Iterator[Store]:"); -} - -#[test] -fn async_calls_complete_through_static_trampolines() { - let py = render(&model(KV)); - assert_has(&py, "import asyncio\n"); - assert_has( - &py, - "_c_kv_fetch_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, \ - ctypes.POINTER(_ErrorStruct), ctypes.c_void_p)", - ); - assert_has( - &py, - "def _kv_fetch_complete(context: Optional[int], err: Any, result: Optional[int]) -> None:", - ); - assert_has( - &py, - "_async_settle(context, _async_error(err, _trap_from), None)", - ); - assert_has( - &py, - "_async_settle(context, None, Store._adopt(_required(result)))", - ); - assert_has( - &py, - "_kv_fetch_completion = _c_kv_fetch_callback(_kv_fetch_complete)", - ); - assert_has(&py, "_c_kv_fetch(id, _kv_fetch_completion, _call)"); - assert_has( - &py, - " _result: Store = await _future\n return _result\n", - ); - // A cancellable call owns a token for its duration. - assert_has(&py, "_token = _cancel_token_create()"); - assert_has(&py, "_c_kv_sleep(ms, _token, _kv_sleep_completion, _call)"); - assert_has( - &py, - "_result: str = await _async_wait_cancellable(_future, _token)", - ); - assert_has( - &py, - "_async_settle(context, _async_error(err, _kv_error_from), None)", - ); - assert_has( - &py, - "_async_settle(context, None, _take_str(result_ptr, result_len))", - ); - assert_has(&py, "async def sleep(ms: int) -> str:"); -} - -#[test] -fn callback_vtables_lead_with_the_header() { - let py = render(&model(KV)); - assert_has(&py, "import abc\n"); - assert_has(&py, "class Watcher(abc.ABC):"); - assert_has(&py, "class _WatcherVtable(ctypes.Structure):"); - assert_has(&py, "The C vtable `kv_lib_kv_Watcher_vtable`."); - assert_has( - &py, - " (\"size\", ctypes.c_uint32),\n (\"flags\", ctypes.c_uint32),\n \ - (\"free\", _CallbackFree),\n (\"on_change\", _Watcher_on_change_t),", - ); - assert_has( - &py, - "_Watcher_vtable = _WatcherVtable(\n ctypes.sizeof(_WatcherVtable),\n 0,\n \ - _callback_free_fn,\n _Watcher_on_change_t(_Watcher_on_change),", - ); - assert_eq!(py.matches("_Watcher_vtable = _WatcherVtable(").count(), 1); - assert_has( - &py, - "_Watcher_vtable_ptr = ctypes.addressof(_Watcher_vtable)", - ); - assert_has( - &py, - "_Watcher_on_change_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, \ - ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, \ - ctypes.POINTER(_ErrorStruct))", - ); -} - -#[test] -fn callback_trampolines_adopt_objects_and_return_every_family() { - let py = render(&model(KV)); - // Object parameters are adopted before anything else can fail. - assert_has( - &py, - " _store = Store._adopt(_required(store))\n \ - _callback_get(ctx).on_change(_peek_bytes(text_ptr, text_len).decode(\"utf-8\"), \ - _decode(_peek_bytes(entry_ptr, entry_len), _read_Entry), _store)", - ); - // Direct returns by value, with a default after a failure. - assert_has(&py, "return bool(_callback_get(ctx).should_stop(count))"); - assert_has(&py, "_callback_fail(out_err, exc)\n return False"); - // Strings and buffers through the out slots, allocated for the producer. - assert_has( - &py, - "def _Watcher_label(ctx: Optional[int], out_ptr: Any, out_len: Any, out_err: Any) -> None:", - ); - assert_has( - &py, - "_callback_return_bytes(out_ptr, out_len, _ret.encode(\"utf-8\"))", - ); - assert_has( - &py, - " _w = _Writer()\n _write_Entry(_w, _ret)\n \ - _callback_return_bytes(out_ptr, out_len, _w.finish())", - ); - // Objects as a fresh reference; `I?` may be None. - assert_has(&py, "return _callback_return_object(_ret, Store)"); - assert_has(&py, "return _callback_return_object_opt(_ret, Store)"); - assert_has(&py, "_store = Store._adopt(store) if store else None"); - // A throwing method reports its domain's codes; others report -4. - assert_has(&py, "_callback_fail(out_err, exc, KvError)"); - assert_eq!( - py.matches("_callback_fail(out_err, exc, KvError)").count(), - 1 - ); -} - -#[test] -fn callback_parameters_register_and_none_passes_a_null_vtable() { - let py = render(&model(KV)); - assert_has(&py, "_watcher_ctx = _callback_register(watcher, Watcher)"); - assert_has( - &py, - "_c_kv_watch(_watcher_ctx, _Watcher_vtable_ptr, ctypes.byref(_err))", - ); - assert_has(&py, "def maybe_watch(watcher: Optional[Watcher]) -> None:"); - assert_has( - &py, - "_watcher_ctx = _callback_register_opt(watcher, Watcher)", - ); - assert_has( - &py, - "_watcher_ctx, _Watcher_vtable_ptr if watcher is not None else None,", - ); + assert_has(&pyproject.unwrap().contents, "requires-python = \">=3.12\""); + let dir = PythonGenerator::from(config).dev_bundle_dir(&model(KV)); + assert_eq!(dir.as_deref().map(|d| d.as_str()), Some("kvpy")); } #[test] @@ -446,48 +121,53 @@ fn public_names_are_exported_and_nothing_else() { [ "Error", "InternalError", + "LibraryLoadError", + "NativeIterator", "KvError", - "NotFound", - "Busy", + "NotFoundError", + "KvTimeoutError", "Entry", "Watcher", "Store", - "pick", "watch", - "maybe_watch", + "scale", "all_stores", - "fetch", - "sleep", ] ); } #[test] -fn feature_runtime_is_emitted_only_when_used() { +fn code_classes_never_shadow_builtins_or_declarations() { + let py = render(&model(KV)); + // `Timeout` would be `TimeoutError`, Python's builtin: the domain's stem + // qualifies it. The scoped alias keeps the bare code name. + assert_has(&py, "class NotFoundError(KvError):"); + assert_has(&py, "class KvTimeoutError(KvError):"); + assert_has(&py, "KvError.Timeout = KvTimeoutError"); + assert!(!py.contains("class TimeoutError")); + let py = render(&model( r#" -version: "0.11.0" +version: "0.12.0" modules: - - name: math - functions: - - name: add - params: [{ name: a, type: i32 }, { name: b, type: i32 }] - return: i32 + - name: kv + errors: + - name: Failure + codes: [{ name: Broken, code: 1, message: broken }] + structs: + - name: BrokenError + fields: [{ name: code, type: i32 }] "#, )); - assert!(!py.contains("import abc")); - assert!(!py.contains("import asyncio")); - assert!(!py.contains("_callbacks")); - assert!(!py.contains("_cancel_token")); - assert!(!py.contains("Value-buffer codecs of composite types")); - assert_has(&py, "def add(a: int, b: int) -> int:"); + assert_has(&py, "class FailureError(Error):"); + assert_has(&py, "class FailureBrokenError(FailureError):"); } #[test] fn exception_names_avoid_user_type_names() { let py = render(&model( r#" -version: "0.11.0" +version: "0.12.0" modules: - name: kv structs: @@ -499,25 +179,101 @@ modules: )); assert_has(&py, "class KvLibError(Exception):"); assert_has(&py, "class KvLibInternalError(RuntimeError):"); + assert_has(&py, "def _kv_lib_error_from("); } #[test] -fn generated_source_is_deterministic() { - let model = model(KV); - assert_eq!(render(&model), render(&model)); +fn names_escape_keywords_and_builtins_the_body_uses() { + let py = render(&model(KV)); + // A parameter named like a keyword or a builtin the body calls gets `_`. + assert_has(&py, "def scale(len_: int, class_: int | None) -> int:"); + assert_has(&py, "_i16(len_, \"len_\")"); + // A field named like a builtin type spells the builtins in its class. + assert_has(&py, " int: builtins.list[builtins.int]\n"); +} + +#[test] +fn range_checks_never_strand_a_lent_object_or_registration() { + let py = render(&model(KV)); + // Nothing lent: the checks run in the argument list. + assert_has( + &py, + "_ret: int = _c_kv_scale(_i16(len_, \"len_\"), class_ is not None, \ + _u32(class_, \"class_\") if class_ is not None else 0, ctypes.byref(_err))", + ); + // A callback is registered just before the call, so the check runs + // first. + assert_has( + &py, + " _limit_v = _u8(limit, \"limit\")\n _err = _ErrorStruct()\n \ + _watcher_ctx = _callback_register(watcher, Watcher)\n", + ); +} + +#[test] +fn feature_runtime_is_emitted_only_when_used() { + let py = render(&model( + r#" +version: "0.12.0" +modules: + - name: math + functions: + - name: add + params: [{ name: a, type: i32 }, { name: b, type: i32 }] + return: i32 +"#, + )); + assert!(!py.contains("import abc")); + assert!(!py.contains("import asyncio")); + assert!(!py.contains("_callbacks")); + assert!(!py.contains("_cancel_token")); + assert!(!py.contains("Value-buffer codecs of composite types")); + assert_has(&py, "def add(a: int, b: int) -> int:"); + // Nothing but the generated-file header names WeaveFFI. + for line in py.lines().filter(|l| !l.starts_with('#')) { + assert!( + !line.to_ascii_lowercase().contains("weaveffi"), + "branded line: {line}" + ); + } +} + +/// A 64-bit little-endian ELF shared-object header with no sections. +fn minimal_elf() -> Vec { + let mut elf = vec![0u8; 64]; + elf[..4].copy_from_slice(b"\x7fELF"); + elf[4] = 2; // ELFCLASS64 + elf[5] = 1; // ELFDATA2LSB + elf[6] = 1; // EV_CURRENT + elf[16..18].copy_from_slice(&3u16.to_le_bytes()); // ET_DYN + elf[18..20].copy_from_slice(&62u16.to_le_bytes()); // EM_X86_64 + elf[20..24].copy_from_slice(&1u32.to_le_bytes()); // e_version + elf[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize + elf } #[test] fn package_bundles_the_identity_library_per_wheel_platform() { let model = model(KV); + // The manylinux tag is read from a Linux library's ELF version needs, so + // those must be real ELF files (a bare header needs nothing, giving the + // 2.17 floor); the others are never read. + let dir = tempfile::tempdir().unwrap(); + let elf = dir.path().join("libkv_lib.so"); + std::fs::write(&elf, minimal_elf()).unwrap(); + let elf = camino::Utf8PathBuf::from_path_buf(elf).unwrap(); let mut binaries = BinarySet::new("kv_lib"); for p in Platform::ALL { - binaries.insert(NativeBinary::new(p, format!("/tmp/{}/lib", p.id()))); + let path = match p.os() { + crate::platform::Os::Linux => elf.clone(), + _ => format!("/tmp/{}/lib", p.id()).into(), + }; + binaries.insert(NativeBinary::new(p, path)); } let mut ctx = PackageContext::new(&binaries); ctx.macos_deployment_target = "12.0"; - let artifacts = PythonGenerator - .package(&model, &ctx, &PythonConfig::default()) + let artifacts = PythonGenerator::from(PythonConfig::default()) + .package(&model, &ctx) .expect("python packages"); let wheels: Vec<&str> = artifacts.iter().map(|a| a.path.as_str()).collect(); assert_eq!( @@ -535,7 +291,7 @@ fn package_bundles_the_identity_library_per_wheel_platform() { panic!("a wheel"); }; assert_eq!(meta.name, "kv-lib"); - assert_eq!(meta.requires_python.as_deref(), Some(">=3.9")); + assert_eq!(meta.requires_python.as_deref(), Some(">=3.10")); let names: Vec<&str> = mac.files.iter().map(|f| f.path.as_str()).collect(); assert_eq!( names, @@ -553,7 +309,7 @@ fn package_bundles_the_identity_library_per_wheel_platform() { fn members_never_replace_the_wrapper_close() { let out = render(&model( r#" -version: "0.11.0" +version: "0.12.0" modules: - name: kv interfaces: diff --git a/crates/weaveffi-cli/src/targets/python/types.rs b/crates/weaveffi-cli/src/targets/python/types.rs index 9c5fa821..e019e99e 100644 --- a/crates/weaveffi-cli/src/targets/python/types.rs +++ b/crates/weaveffi-cli/src/targets/python/types.rs @@ -4,55 +4,88 @@ use crate::lang; use heck::ToSnakeCase; -use weaveffi_model::abi::CType; +use weaveffi_model::abi::{CType, ConstPos}; use weaveffi_model::model::FieldBinding; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty}; -/// The Python typing hint for `ty` as it appears in annotations. The module +/// Which way a value travels, which decides how its containers are +/// annotated: what the bindings accept is abstract (any `Sequence` or +/// `Mapping`), and what they hand out is concrete (`list`, `dict`). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Dir { + /// Into the native library: function parameters, callback returns, and + /// value-buffer writers. + In, + /// Out of the native library: function returns, callback parameters, + /// record and error fields, and value-buffer readers. + Out, +} + +/// The Python typing hint for `ty` travelling in direction `dir`. The module /// starts with `from __future__ import annotations`, so user types are /// written bare even when declared later in the file; type names are /// global, and every module's declarations share one Python namespace. -pub(crate) fn py_type_hint(ty: &Ty) -> String { +pub(crate) fn py_type_hint(ty: &Ty, dir: Dir) -> String { match ty { - Ty::Prim( - Prim::I8 - | Prim::I16 - | Prim::I32 - | Prim::I64 - | Prim::U8 - | Prim::U16 - | Prim::U32 - | Prim::U64, - ) => "int".into(), Ty::Prim(Prim::F32 | Prim::F64) => "float".into(), Ty::Prim(Prim::Bool) => "bool".into(), Ty::Prim(Prim::String) => "str".into(), Ty::Prim(Prim::Bytes) => "bytes".into(), - Ty::Enum(name) - | Ty::Record(name) - | Ty::RichEnum(name) - | Ty::Interface(name) - | Ty::CallbackInterface(name) => name.clone(), - Ty::Optional(inner) => format!("Optional[{}]", py_type_hint(inner)), - Ty::List(inner) => format!("List[{}]", py_type_hint(inner)), - Ty::Map(k, v) => format!("Dict[{}, {}]", py_type_hint(k), py_type_hint(v)), - Ty::Iterator(inner) => format!("Iterator[{}]", py_type_hint(inner)), + Ty::Prim(_) => "int".into(), + Ty::Enum(name) | Ty::Record(name) | Ty::RichEnum(name) | Ty::Interface(name) => { + name.clone() + } + Ty::Optional(inner) => format!("{} | None", py_type_hint(inner, dir)), + Ty::List(inner) => match dir { + Dir::In => format!("Sequence[{}]", py_type_hint(inner, dir)), + Dir::Out => format!("list[{}]", py_type_hint(inner, dir)), + }, + Ty::Map(k, v) => match dir { + Dir::In => format!( + "Mapping[{}, {}]", + py_type_hint(k, dir), + py_type_hint(v, dir) + ), + Dir::Out => format!("dict[{}, {}]", py_type_hint(k, dir), py_type_hint(v, dir)), + }, } } -/// The hint for an optional return or `None`: `None` for a void callable. -pub(crate) fn py_return_hint(ty: Option<&Ty>) -> String { - ty.map_or_else(|| "None".to_string(), py_type_hint) +/// The hint for a parameter: its value type (accepted abstractly), or a +/// callback interface's class (`| None` when nullable). +pub(crate) fn py_param_hint(ty: &ParamTy) -> String { + match ty { + ParamTy::Value(ty) => py_type_hint(ty, Dir::In), + ParamTy::Callback { name, nullable } => { + if *nullable { + format!("{name} | None") + } else { + name.clone() + } + } + } +} + +/// The hint for a callable's return: `None` for a void callable, the +/// iterator's `NativeIterator[T]`, or the value type. +pub(crate) fn py_return_hint(ret: Option<&RetTy>) -> String { + match ret { + None => "None".into(), + Some(RetTy::Value(ty)) => py_type_hint(ty, Dir::Out), + Some(RetTy::Iterator(elem)) => format!("NativeIterator[{}]", py_type_hint(elem, Dir::Out)), + } } /// The builtin types an annotation names. -const BUILTIN_HINTS: &[&str] = &["bool", "bytes", "float", "int", "str"]; +const BUILTIN_HINTS: &[&str] = &[ + "bool", "bytes", "dict", "float", "int", "list", "str", "type", +]; /// The annotation for `ty` inside a class declaring `fields`. A field named /// like a builtin type (`int: int`) shadows it for every later annotation in /// the class body, so such classes spell the builtins `builtins.int`. pub(crate) fn py_field_hint(fields: &[FieldBinding], ty: &Ty) -> String { - let hint = py_type_hint(ty); + let hint = py_type_hint(ty, Dir::Out); if !fields .iter() .any(|f| BUILTIN_HINTS.contains(&f.name.as_str())) @@ -80,25 +113,58 @@ pub(crate) fn py_field_hint(fields: &[FieldBinding], ty: &Ty) -> String { out } -/// Which side of the boundary a `ctypes` slot type describes. -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum Slot { - /// An argument the wrapper passes in (`argtypes`). A `const uint8_t*` - /// is `c_char_p`, which accepts a Python `bytes` object directly - /// (interior NUL bytes included, since the length travels separately). - Arg, - /// A value the wrapper receives (`restype`, or a parameter of a - /// `CFUNCTYPE` the producer calls). A `uint8_t*` stays a raw `c_void_p` - /// address so the wrapper can copy exactly `len` bytes and free them. - Recv, +/// The IDL spelling of a slice element or range-checked integer kind, which +/// names the runtime's per-kind helpers (`_i32`, `_take_array(.., "f64")`). +pub(crate) fn prim_kind(p: Prim) -> &'static str { + p.snake() +} + +/// The runtime checker a direct integer slot of C type `ty` goes through +/// before `ctypes` sees it (`ctypes` truncates an out-of-range integer +/// silently): `_i32` for `int32_t` and every C-style enum, `_u64` for +/// `uint64_t`, and so on. `None` for floats and `bool`, which need no check. +pub(crate) fn int_checker(ty: &CType) -> Option<&'static str> { + Some(match ty { + CType::Int8 => "_i8", + CType::Int16 => "_i16", + CType::Int32 | CType::Enum { .. } => "_i32", + CType::Int64 => "_i64", + CType::Uint8 => "_u8", + CType::Uint16 => "_u16", + CType::Uint32 => "_u32", + CType::Uint64 | CType::Size => "_u64", + _ => return None, + }) } -/// Maps a shared ABI [`CType`] onto its `ctypes` spelling. The structural -/// lowering (which slots exist, in what order) comes from the model; this is -/// only the Python vocabulary applied to each slot. Opaque object, iterator, -/// vtable, and cancel-token pointers are `c_void_p`; `bool` is `c_bool`; -/// enums are `c_int32`; the error struct is the runtime's `_ErrorStruct`. -pub(crate) fn py_ctype(ty: &CType, slot: Slot) -> String { +/// The C-style enum a direct slot of C type `ty` carries, whose class +/// re-wraps a received integer (`Priority(x)`). +pub(crate) fn enum_class(ty: &CType) -> Option<&str> { + match ty { + CType::Enum { name, .. } => Some(name), + _ => None, + } +} + +/// Maps a shared ABI [`CType`] onto the `ctypes` spelling of a parameter +/// slot (an `argtypes` entry, or a parameter of a `CFUNCTYPE` the producer +/// calls). The structural lowering (which slots exist, in what order) comes +/// from the model; this is only the Python vocabulary applied to each slot. +/// +/// - A borrowed `const uint8_t*` run the wrapper passes is `c_char_p`, +/// which accepts a Python `bytes` object directly (interior NUL bytes +/// included, since the length travels separately); one the producer +/// passes to a trampoline is a raw `c_void_p` address, so exactly `len` +/// bytes are copied. +/// - A borrowed typed array (`const T*`) is a `c_void_p` address: the +/// wrapper passes an `array.array`'s buffer, and a trampoline reads `len` +/// elements from it. +/// - An out slot (`T*`) is `POINTER(T)`, and an out slot for a run or an +/// object (`T**`) is `POINTER(c_void_p)`. +/// - Opaque object, iterator, vtable, and cancel-token pointers are +/// `c_void_p`; `bool` is `c_bool`; enums are `c_int32`; the error struct +/// is the runtime's `_ErrorStruct`. +pub(crate) fn py_ctype(ty: &CType, outbound: bool) -> String { match ty { CType::Int8 => "ctypes.c_int8".into(), CType::Int16 => "ctypes.c_int16".into(), @@ -119,25 +185,66 @@ pub(crate) fn py_ctype(ty: &CType, slot: Slot) -> String { | CType::StructTag { .. } | CType::VtableTag { .. } | CType::Named(_) => "ctypes.c_void_p".into(), + CType::Ptr { konst, pointee } => match (konst, pointee.as_ref()) { + (ConstPos::West, CType::Uint8) if outbound => "ctypes.c_char_p".into(), + (_, CType::Error) => "ctypes.POINTER(_ErrorStruct)".into(), + (_, CType::Ptr { .. }) => "ctypes.POINTER(ctypes.c_void_p)".into(), + (ConstPos::None, scalar) if is_scalar(scalar) => { + format!("ctypes.POINTER({})", py_ctype(scalar, outbound)) + } + _ => "ctypes.c_void_p".into(), + }, + } +} + +/// Whether `ty` is a by-value number, `bool`, `size_t`, or enum. +fn is_scalar(ty: &CType) -> bool { + matches!( + ty, + CType::Int8 + | CType::Int16 + | CType::Int32 + | CType::Int64 + | CType::Uint8 + | CType::Uint16 + | CType::Uint32 + | CType::Uint64 + | CType::Float + | CType::Double + | CType::Bool + | CType::Size + | CType::Enum { .. } + ) +} + +/// The `ctypes` spelling of a C return type (a `restype`). Every pointer +/// return (a string, bytes, buffer, or typed-array run, an object, or an +/// iterator handle) is a raw `c_void_p` address, so the wrapper can copy +/// exactly what it owes and release it. +pub(crate) fn py_restype(ty: &CType) -> String { + match ty { + CType::Ptr { .. } => "ctypes.c_void_p".into(), + other => py_ctype(other, false), + } +} + +/// The `ctypes` type of the local a wrapper allocates for the out slot +/// `slot` (a `T*` or `T**`) and passes by reference: the pointee, with a +/// pointer pointee received as a raw `c_void_p` address. +pub(crate) fn py_out_local(slot: &CType) -> String { + match slot { CType::Ptr { pointee, .. } => match pointee.as_ref() { - CType::Uint8 if slot == Slot::Arg => "ctypes.c_char_p".into(), - CType::Char => "ctypes.c_char_p".into(), - CType::Ptr { .. } => "ctypes.POINTER(ctypes.c_void_p)".into(), - CType::Uint8 - | CType::StructTag { .. } - | CType::VtableTag { .. } - | CType::CancelToken - | CType::Void - | CType::Named(_) => "ctypes.c_void_p".into(), - other => format!("ctypes.POINTER({})", py_ctype(other, slot)), + CType::Ptr { .. } => "ctypes.c_void_p".into(), + scalar => py_ctype(scalar, true), }, + other => unreachable!("an out slot is a pointer, not {other:?}"), } } /// The Python type a `ctypes` callback receives for one C slot (a /// trampoline or completion parameter), as an annotation: integers and -/// sizes are `int`, floats `float`, `bool` `bool`, an address -/// `Optional[int]` (`None` for null), and a typed pointer `Any`. +/// sizes are `int`, floats `float`, `bool` `bool`, an address `int | None` +/// (`None` for null), and a typed pointer `Any`. pub(crate) fn py_slot_hint(ty: &CType) -> &'static str { match ty { CType::Float | CType::Double => "float", @@ -146,18 +253,15 @@ pub(crate) fn py_slot_hint(ty: &CType) -> &'static str { CType::CancelToken | CType::StructTag { .. } | CType::VtableTag { .. } - | CType::Named(_) => "Optional[int]", + | CType::Named(_) => "int | None", CType::Error => "Any", - CType::Ptr { pointee, .. } => match pointee.as_ref() { - CType::Uint8 - | CType::Char - | CType::StructTag { .. } - | CType::VtableTag { .. } - | CType::CancelToken - | CType::Void - | CType::Named(_) => "Optional[int]", - _ => "Any", - }, + CType::Ptr { .. } => { + if py_ctype(ty, false) == "ctypes.c_void_p" { + "int | None" + } else { + "Any" + } + } _ => "int", } } @@ -175,13 +279,14 @@ const PY_BODY_NAMES: &[&str] = &[ "int", "isinstance", "len", + "list", "range", "self", "warnings", ]; /// The Python spelling of an IDL value identifier (parameter name): -/// snake_case via heck, then escaped with a trailing `_` when it is a +/// snake_case via heck, then escaped with a trailing `_` when it's a /// keyword (`class`) or a name the generated body relies on (`self`, /// `len`). IDL names are usually already snake, so the case conversion is a /// safety net for camelCase inputs. diff --git a/crates/weaveffi-cli/src/targets/ruby/callbacks.rs b/crates/weaveffi-cli/src/targets/ruby/callbacks.rs index d86a07e7..706e5fad 100644 --- a/crates/weaveffi-cli/src/targets/ruby/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/ruby/callbacks.rs @@ -1,372 +1,295 @@ -//! Callback-interface rendering: the consumer-facing Ruby module (duck-typed -//! method set with `NotImplementedError` defaults), the `FFI::Struct` vtable -//! layout with its `{size, flags, free}` header, one trampoline -//! `FFI::Function` per method plus `free`, and the single process-wide -//! vtable instance every registration passes to the producer. +//! Callback interfaces: the consumer-facing Ruby module (a duck-typed +//! method set with `NotImplementedError` defaults) and its vtable, one +//! trampoline `FFI::Function` per method, which the runtime lays out behind +//! the shared `{size, flags, free}` header. //! -//! The trampolines follow [`CallbackProtocol`](weaveffi_model::plan::CallbackProtocol): -//! `ctx` is resolved through the module's implementation registry, object -//! arguments are adopted into wrappers first, borrowed string, bytes, and -//! buffer arguments are copied or decoded before the implementation runs, -//! and the return crosses back per its family (a direct value, a fresh -//! object reference, or a `{prefix}_alloc` run in the out slots). A domain -//! error raised by a method declared `throws` is reported with its code and -//! payload; any other exception is reported as `-4`. Nothing unwinds +//! A trampoline resolves `ctx` to the registered implementation, receives +//! each argument per its [`ArgPass`] (objects first, so their references +//! are owned by a wrapper even when a later step raises; strings, bytes, +//! buffers, and typed arrays copied before the implementation runs), calls +//! the method, and hands the return back per its [`CallbackRetPass`]. An +//! exception is reported through `out_err` per the method's +//! [`ErrorStrategy`]; nothing unwinds //! through the C frame. -use crate::codegen::common::DocCommentStyle; -use crate::codegen::CodeWriter; -use heck::ToShoutySnakeCase; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; use weaveffi_model::ty::{Prim, Ty}; -use crate::targets::ruby::docs::{emit_param_docs, emit_return_doc}; -use crate::targets::ruby::entities::rb_error_payload_name; -use crate::targets::ruby::types::{rb_abi_types, rb_ffi_type, rb_param_name}; +use crate::codegen::docs::Doc; +use crate::codegen::CodeWriter; +use crate::targets::ruby::docs::{self, CallableDoc, ParamDoc}; +use crate::targets::ruby::types::{ + rb_callback_method_name, rb_const, rb_doc_type, rb_domain, rb_elem, rb_error_arg, rb_ffi_type, + rb_ffi_types, rb_param_name, rb_scalar, rb_slot, rb_wire, +}; use crate::targets::ruby::RbCtx; -/// The `FFI::Struct` subclass laying out one callback interface's vtable: -/// `Listener` becomes `WvListenerVtable`. -fn rb_vtable_class(name: &str) -> String { - format!("Wv{name}Vtable") -} - -/// The constant holding the one process-wide vtable instance for a callback -/// interface: `Listener` becomes `WV_LISTENER_VTABLE`. -pub(crate) fn rb_vtable_const(name: &str) -> String { - format!("WV_{}_VTABLE", name.to_shouty_snake_case()) -} - -/// The constant pinning one method's trampoline `FFI::Function`: -/// `Listener.on_change` becomes `WV_LISTENER_ON_CHANGE`. -fn rb_trampoline_const(cb: &str, method: &str) -> String { - format!( - "WV_{}_{}", - cb.to_shouty_snake_case(), - method.to_shouty_snake_case() - ) -} - -/// The constant pinning the `free` trampoline. -fn rb_free_const(cb: &str) -> String { - format!("WV_{}_FREE", cb.to_shouty_snake_case()) -} - -/// The trampoline's Ruby formal parameter names for one method, position -/// for position with [`CallbackMethodBinding::abi_params`]: `wv_ctx`, the -/// value of a direct parameter under its own name, `{name}_ptr` (and -/// `{name}_len`) for every other parameter, the `wv_out_ptr`/`wv_out_len` -/// slots of a string, bytes, or buffer return, then `wv_err`. -fn rb_tramp_formals(m: &CallbackMethodBinding) -> Vec { - let mut formals = vec!["wv_ctx".to_string()]; - for p in &m.params { - let n = rb_param_name(&p.name); - match p.arg_pass() { - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { - formals.push(format!("{n}_ptr")); - formals.push(format!("{n}_len")); - } - ArgPass::Object { .. } => formals.push(format!("{n}_ptr")), - _ => formals.push(n), - } - } - if matches!( - RetPass::of(m.ret.as_ref()), - RetPass::String | RetPass::Bytes | RetPass::Buffer - ) { - formals.push("wv_out_ptr".to_string()); - formals.push("wv_out_len".to_string()); - } - formals.push("wv_err".to_string()); - formals -} - -/// Render one callback interface: the consumer-facing module, the vtable -/// layout, the trampolines, and the static vtable instance. `error` is the -/// domain in scope for the interface's module, which its `throws` methods -/// may raise. -pub(crate) fn render_callback_interface( +/// Render the consumer-facing module of one callback interface. +pub(crate) fn render_callback_module( w: &mut CodeWriter, ctx: &RbCtx, - error: Option<&ErrorBinding>, cb: &CallbackInterfaceBinding, ) { - let local = &cb.name; - let vtable_class = rb_vtable_class(&cb.name); - let vtable_const = rb_vtable_const(&cb.name); - - // The consumer-facing module: documentation of the required methods, and - // NotImplementedError defaults for consumers who include it. + let name = rb_const(&cb.name); w.blank(); - w.doc(&cb.doc, DocCommentStyle::Hash); - if cb.doc.is_some() { - w.line("#"); - } - w.line("# Consumer-implemented callback interface. Any object responding to the"); - w.line(format!( - "# methods below is accepted wherever a {local} is expected; include" + let doc = Doc::new(&cb.doc, &cb.deprecated); + let mut extra = CallableDoc::default(); + extra.notes.push(format!( + "A callback interface: pass any object that responds to the methods \ + below wherever a {name} is expected. Including this module documents \ + the intent and supplies NotImplementedError defaults. The library may \ + call the methods from any thread until it releases the \ + implementation." )); - w.line("# this module to inherit NotImplementedError defaults. The library"); - w.line("# may call the methods from any thread until it releases the"); - w.line("# implementation."); - if let Some(msg) = &cb.deprecated { - w.line(format!("# @deprecated {msg}")); - } - w.block(format!("module {local}"), "end", |w| { + docs::emit(w, &ctx.names, &doc, &extra); + w.block(format!("module {name}"), "end", |w| { for (idx, m) in cb.methods.iter().enumerate() { if idx > 0 { w.blank(); } - w.doc(&m.doc, DocCommentStyle::Hash); - if let Some(msg) = &m.deprecated { - w.line(format!("# @deprecated {msg}")); + let mut extra = CallableDoc::default(); + for p in &m.params { + extra.params.push(ParamDoc { + name: rb_param_name(&p.name), + ty: rb_doc_type(&p.ty), + doc: p.doc.clone(), + }); } - emit_param_docs(w, &m.params); - emit_return_doc(w, m.ret.as_ref()); - if let (ErrorStrategy::Throws, Some(eb)) = (m.error_strategy(), error) { - w.line(format!( - "# @raise [{}] reported to the library with its code and fields", - eb.type_name - )); + extra.ret = Some(m.ret.as_ref().map_or("void".to_string(), rb_doc_type)); + match &m.error { + ErrorStrategy::Trap => {} + ErrorStrategy::Untyped => extra.raises.push(( + "StandardError".to_string(), + "reported to the library with its message".to_string(), + )), + ErrorStrategy::Domain(d) => extra.raises.push(( + rb_domain(d), + "reported to the library with its code and fields".to_string(), + )), } + docs::emit(w, &ctx.names, &Doc::new(&m.doc, &m.deprecated), &extra); let formals: Vec = m.params.iter().map(|p| rb_param_name(&p.name)).collect(); + let method = rb_callback_method_name(&m.name); let open = if formals.is_empty() { - format!("def {}", m.name) + format!("def {method}") } else { - format!("def {}({})", m.name, formals.join(", ")) + format!("def {method}({})", formals.join(", ")) }; w.block(open, "end", |w| { w.line(format!( - "raise NotImplementedError, \"#{{self.class}}#{} is not implemented\"", - m.name + "raise NotImplementedError, \"#{{self.class}}#{method} is not implemented\"" )); }); } }); +} - // The vtable layout: the fixed header, then one pointer per method in - // declaration order. +/// Render the vtable registration of one callback interface: +/// `Bridge.vtable(Iface, trampoline, ...)`, with the trampolines in +/// declaration order. +pub(crate) fn render_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding) { w.blank(); - w.line("# @api private"); - w.line(format!( - "# The C vtable layout the library calls a {local} through." - )); - w.block(format!("class {vtable_class} < FFI::Struct"), "end", |w| { - let mut fields = vec![ - ":wv_size, :uint32".to_string(), - ":wv_flags, :uint32".to_string(), - ":wv_free, :pointer".to_string(), - ]; - fields.extend(cb.methods.iter().map(|m| format!(":{}, :pointer", m.name))); - w.line(format!("layout {}", fields.join(",\n "))); + w.line(format!("# The {} vtable.", rb_const(&cb.name))); + w.line("Bridge.vtable("); + w.scope(|w| { + w.line(format!("{},", rb_const(&cb.name))); + for m in &cb.methods { + render_trampoline(w, cb, m); + } }); + w.line(")"); +} - // The free hook: drop the registry entry; the producer never touches - // ctx again after this fires. - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Releases a {local} implementation when the library drops its last" - )); - w.line("# reference. May run on any library thread."); +/// The value a trampoline returns when the method failed: the C zero of +/// its return. +fn failure_value(m: &CallbackMethodBinding) -> &'static str { + match (&m.ret_pass, m.ret.as_ref()) { + (CallbackRetPass::Direct, Some(Ty::Prim(Prim::Bool))) + | (CallbackRetPass::OptDirect { .. }, _) => "false", + (CallbackRetPass::Direct, Some(Ty::Prim(Prim::F32 | Prim::F64))) => "0.0", + (CallbackRetPass::Direct, _) => "0", + _ => "nil", + } +} + +/// Render one method's trampoline as an argument of `Bridge.vtable`. +fn render_trampoline(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding) { + let formals: Vec = m.abi.params.iter().map(rb_slot).collect(); + let ctx_slot = formals + .first() + .expect("a callback method takes ctx") + .clone(); + let err_slot = formals + .last() + .expect("a callback method takes out_err") + .clone(); + w.line(format!("# {}#{}", rb_const(&cb.name), m.name)); w.block( format!( - "{} = FFI::Function.new(:void, [:pointer]) do |ctx|", - rb_free_const(&cb.name) + "FFI::Function.new({}, {}) do |{}|", + rb_ffi_type(&m.abi.ret), + rb_ffi_types(&m.abi.params), + formals.join(", ") ), - "end", + "end,", |w| { - w.line("_wv_cb_free(ctx)"); + let default = failure_value(m); + let open = if default == "nil" { + format!("Bridge.callback({err_slot}, {}) do", rb_error_arg(&m.error)) + } else { + format!( + "Bridge.callback({err_slot}, {}, {default}) do", + rb_error_arg(&m.error) + ) + }; + w.block(open, "end", |w| { + let objects_first = m + .params + .iter() + .filter(|p| matches!(p.pass, ArgPass::Object { .. })) + .chain( + m.params + .iter() + .filter(|p| !matches!(p.pass, ArgPass::Object { .. })), + ); + let mut values = std::collections::HashMap::new(); + for p in objects_first { + let (line, value) = receive_arg(&p.name, &p.ty, &p.pass); + if let Some(line) = line { + w.line(line); + } + values.insert(p.name.clone(), value); + } + let args: Vec<&str> = m.params.iter().map(|p| values[&p.name].as_str()).collect(); + let method = rb_callback_method_name(&m.name); + let call = if args.is_empty() { + format!("Bridge.impl({ctx_slot}).{method}") + } else { + format!("Bridge.impl({ctx_slot}).{method}({})", args.join(", ")) + }; + return_value(w, m, &call); + }); }, ); - - let protocol = cb.protocol(); - for (m, args) in cb.methods.iter().zip(&protocol.method_args) { - render_trampoline(w, ctx, error, cb, m, args); - } - - // The single static vtable instance, filled with the pinned trampolines. - w.blank(); - w.line(format!( - "# The one process-wide {local} vtable every registration hands the" - )); - w.line("# library; its entries live for the process lifetime."); - w.line(format!("{vtable_const} = {vtable_class}.new")); - w.line(format!("{vtable_const}[:wv_size] = {vtable_class}.size")); - w.line(format!("{vtable_const}[:wv_flags] = 0")); - w.line(format!( - "{vtable_const}[:wv_free] = {}", - rb_free_const(&cb.name) - )); - for m in &cb.methods { - w.line(format!( - "{vtable_const}[:{}] = {}", - m.name, - rb_trampoline_const(&cb.name, &m.name) - )); - } -} - -/// The integer-range kind (`:i32`, ...) the runtime checks a direct -/// integer return against; C-style enums cross as `i32`. -fn int_kind(ty: &Ty) -> &'static str { - match ty { - Ty::Prim(p) if p.is_integer() => p.snake(), - Ty::Enum(_) => "i32", - other => unreachable!("{other} is not an integer type"), - } } -/// Render one method's trampoline: an `FFI::Function` matching the vtable -/// entry's C signature that receives each argument per its [`RetPass`] -/// plan, resolves `ctx`, invokes the implementation, and hands the return -/// back per its family. Object arguments are adopted first, so their -/// references are owned by a wrapper even when a later step raises. A -/// domain error from a `throws` method goes through `_wv_cb_throw`; any -/// other exception through `_wv_cb_fail` (code `-4`); either way the -/// method's zero value is returned. -fn render_trampoline( - w: &mut CodeWriter, - ctx: &RbCtx, - error: Option<&ErrorBinding>, - cb: &CallbackInterfaceBinding, - m: &CallbackMethodBinding, - args: &[RetPass], -) { - let ret_pass = RetPass::of(m.ret.as_ref()); - let default = match (&ret_pass, m.ret.as_ref()) { - (RetPass::Direct, Some(Ty::Prim(Prim::Bool))) => "false", - (RetPass::Direct, Some(Ty::Prim(Prim::F32 | Prim::F64))) => "0.0", - (RetPass::Direct, _) => "0", - (RetPass::Object { .. }, _) => "FFI::Pointer::NULL", - _ => "nil", +/// The statement receiving one trampoline argument (if it needs one) and +/// the expression the method is called with. +fn receive_arg(name: &str, ty: &Ty, pass: &ArgPass) -> (Option, String) { + let local = rb_param_name(name); + let line = match pass { + ArgPass::Direct { slot } => return (None, rb_slot(slot)), + ArgPass::OptDirect { has, value, .. } => { + format!("{local} = {} ? {} : nil", rb_slot(has), rb_slot(value)) + } + ArgPass::Slice { ptr, len, elem } => format!( + "{local} = Bridge.borrow_slice({}, {}, {})", + rb_slot(ptr), + rb_slot(len), + rb_elem(*elem) + ), + ArgPass::String { ptr, len } => format!( + "{local} = Bridge.borrow_string({}, {})", + rb_slot(ptr), + rb_slot(len) + ), + ArgPass::Bytes { ptr, len } => format!( + "{local} = Bridge.borrow_bytes({}, {})", + rb_slot(ptr), + rb_slot(len) + ), + ArgPass::Buffer { ptr, len } => format!( + "{local} = Bridge.decode_borrowed({}, {}, {})", + rb_slot(ptr), + rb_slot(len), + rb_wire(ty) + ), + ArgPass::Object { + slot, + nullable, + interface, + } => { + let slot = rb_slot(slot); + let class = rb_const(interface); + if *nullable { + format!("{local} = {slot}.null? ? nil : Bridge.adopt({class}, {slot})") + } else { + format!("{local} = Bridge.adopt({class}, {slot})") + } + } + ArgPass::Callback { .. } => unreachable!("a callback method takes no callback"), }; + (Some(line), local) +} - w.blank(); - w.line("# @api private"); - w.line(format!("# Trampoline for {}#{}.", cb.name, m.name)); - w.line(format!( - "{} = FFI::Function.new({}, [{}]) do |{}|", - rb_trampoline_const(&cb.name, &m.name), - rb_ffi_type(&m.abi_ret), - rb_abi_types(&m.abi_params).join(", "), - rb_tramp_formals(m).join(", ") - )); - w.scope(|w| { - let params: Vec<_> = m.params.iter().zip(args).collect(); - let is_object = |pass: &RetPass| matches!(pass, RetPass::Object { .. }); - for (p, pass) in params.iter().filter(|(_, pass)| is_object(pass)) { - render_tramp_arg(w, ctx, &p.name, &p.ty, pass); - } - for (p, pass) in params.iter().filter(|(_, pass)| !is_object(pass)) { - render_tramp_arg(w, ctx, &p.name, &p.ty, pass); +/// Emit the statement handing the method's return back per its +/// [`CallbackRetPass`] (the block's value is the C return). +fn return_value(w: &mut CodeWriter, m: &CallbackMethodBinding, call: &str) { + let ty = || m.ret.as_ref().expect("a value return has a type"); + match &m.ret_pass { + CallbackRetPass::Void => { + w.line(call); + w.line("nil"); } - let call_args: Vec = m.params.iter().map(|p| rb_param_name(&p.name)).collect(); - let call = if call_args.is_empty() { - format!("_wv_cb_lookup(wv_ctx).{}", m.name) - } else { - format!("_wv_cb_lookup(wv_ctx).{}({})", m.name, call_args.join(", ")) - }; - // Every conversion runs inside the rescue, so a wrong-typed return - // surfaces as a callback failure rather than escaping through ffi's - // own conversion. - match (&ret_pass, m.ret.as_ref()) { - (RetPass::Void, _) => { - w.line(call); - w.line("nil"); - } - (RetPass::Direct, Some(Ty::Prim(Prim::Bool))) => { + CallbackRetPass::Direct => match ty() { + Ty::Prim(Prim::Bool) => { w.line(format!("{call} ? true : false")); } - (RetPass::Direct, Some(Ty::Prim(Prim::F32 | Prim::F64))) => { - w.line(format!("_wv_float({call})")); - } - (RetPass::Direct, Some(ty)) => { - w.line(format!("_wv_int({call}, :{})", int_kind(ty))); - } - (RetPass::Object { .. }, Some(ty)) => { - let class = ty.interface_name().expect("object plan names an interface"); - w.line(format!("_wv_cb_return_object({call}, {class})")); - } - (RetPass::String, _) => { - w.line(format!( - "_wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_str({call}))" - )); - } - (RetPass::Bytes, _) => { - w.line(format!( - "_wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_bytes({call}))" - )); - } - (RetPass::Buffer, Some(ty)) if ctx.codecs.carries_objects(ty) => { - w.line(format!("result = {call}")); - w.line("writer = WvBufferWriter.new"); - w.line(ctx.codecs.write(ty, "writer", "result", "")); - w.line("_wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_seal(writer).first)"); + other => { + w.line(format!("Bridge.scalar({call}, {})", rb_scalar(other))); } - (RetPass::Buffer, Some(ty)) => { - w.line(format!("result = {call}")); - w.line(format!( - "_wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_encode {{ |w| {} }})", - ctx.codecs.write(ty, "w", "result", "") - )); - } - _ => unreachable!("a typed return has a type"), + }, + CallbackRetPass::OptDirect { out_value } => { + let inner = match ty() { + Ty::Optional(inner) => inner, + other => unreachable!("{other} isn't optional"), + }; + w.line(format!( + "Bridge.opt_return({call}, {}, {})", + rb_scalar(inner), + rb_slot(out_value) + )); } - }); - if let (ErrorStrategy::Throws, Some(eb)) = (m.error_strategy(), error) { - w.line(format!("rescue {} => e", eb.type_name)); - w.scope(|w| { + CallbackRetPass::Slice { + out_ptr, + out_len, + elem, + } => { w.line(format!( - "_wv_cb_throw(wv_err, e) {{ {}(e) }}", - rb_error_payload_name(eb) + "Bridge.slice_return({call}, {}, {}, {})", + rb_elem(*elem), + rb_slot(out_ptr), + rb_slot(out_len) )); - w.line(default); - }); - } - // Rescue Exception, not StandardError: NotImplementedError is a - // ScriptError, and nothing may unwind through the C frame. - w.line("rescue Exception => e # rubocop:disable Lint/RescueException"); - w.scope(|w| { - w.line("_wv_cb_fail(wv_err, e)"); - w.line(default); - }); - w.line("end"); -} - -/// Emit the statement receiving one trampoline argument into the local -/// named after the parameter, per its [`RetPass`] plan. Strings, bytes, and -/// buffers are borrowed `(ptr, len)` pairs copied or decoded before the -/// implementation runs; objects transfer one strong reference that is -/// adopted into a wrapper (`nil` for a null optional slot); direct values -/// arrive under the parameter's own name already. -fn render_tramp_arg(w: &mut CodeWriter, ctx: &RbCtx, name: &str, ty: &Ty, pass: &RetPass) { - let n = rb_param_name(name); - match pass { - RetPass::Void => unreachable!("callback parameters always have a type"), - RetPass::Direct => {} - RetPass::String => { - w.line(format!("{n} = _wv_borrow_string({n}_ptr, {n}_len)")); } - RetPass::Bytes => { - w.line(format!("{n} = _wv_borrow_bytes({n}_ptr, {n}_len)")); + CallbackRetPass::String { out_ptr, out_len } => { + w.line(format!( + "Bridge.string_return({call}, {}, {})", + rb_slot(out_ptr), + rb_slot(out_len) + )); } - RetPass::Buffer => { + CallbackRetPass::Bytes { out_ptr, out_len } => { w.line(format!( - "{n} = _wv_decode_borrowed({n}_ptr, {n}_len) {{ |r| {} }}", - ctx.codecs.read(ty, "r", "") + "Bridge.bytes_return({call}, {}, {})", + rb_slot(out_ptr), + rb_slot(out_len) )); } - RetPass::Object { nullable } => { - let class = ty.interface_name().expect("object plan names an interface"); - if *nullable { - w.line(format!( - "{n} = {n}_ptr.null? ? nil : {class}._from_ptr({n}_ptr)" - )); - } else { - w.line(format!("{n} = {class}._from_ptr({n}_ptr)")); - } + CallbackRetPass::Buffer { out_ptr, out_len } => { + w.line(format!( + "Bridge.buffer_return({}, {call}, {}, {})", + rb_wire(ty()), + rb_slot(out_ptr), + rb_slot(out_len) + )); + } + CallbackRetPass::Object { interface, .. } => { + w.line(format!( + "Bridge.object_return({call}, {})", + rb_const(interface) + )); } } } diff --git a/crates/weaveffi-cli/src/targets/ruby/calls.rs b/crates/weaveffi-cli/src/targets/ruby/calls.rs index c6b373ac..17cbe56d 100644 --- a/crates/weaveffi-cli/src/targets/ruby/calls.rs +++ b/crates/weaveffi-cli/src/targets/ruby/calls.rs @@ -1,125 +1,91 @@ -//! Callable rendering: FFI attachments, async completion trampolines, the -//! sync, async, and iterator wrapper bodies, and the parameter and return -//! marshalling they share. +//! Callable rendering: the `attach_function` lines of the private `Native` +//! module and the idiomatic wrappers (module functions, interface +//! constructors, methods, and statics) in their sync, async, and iterator +//! shapes. //! -//! Marshalling dispatch goes through the shared plan layer ([`ArgPass`], -//! [`RetPass`]), so this backend can't drift from the others on -//! call-boundary semantics: strings, bytes, and value buffers cross as -//! borrowed `(ptr, len)` pairs, objects are borrowed as parameters (pinned -//! for the call through `_wv_pin`) and adopted as returns, and a callback -//! interface crosses as a handle-table key plus the interface's static -//! vtable (or two NULLs for an absent optional one). +//! A wrapper checks and converts its arguments per their [`ArgPass`], then +//! hands the C call to one runtime helper (`Bridge.call`, `Bridge.await`, +//! `Bridge.iterate`) as a block. The helper allocates the error and out +//! slots, seals the value-buffer arguments, raises per the callable's +//! [`ErrorStrategy`], and receives the +//! result per its kind, so no wrapper repeats that plumbing. //! -//! Every call into the library except the trivial runtime helpers releases -//! the GVL (`blocking: true`): a call may run for a long time, and a -//! producer thread may need the GVL to run a callback implementation while -//! the call is in flight. +//! Every call into the library except the trivial helpers releases the GVL +//! (`blocking: true`): a call may run for a long time, and a producer thread +//! may need the GVL to run a callback implementation while the call is in +//! flight. -use crate::codegen::common::DocCommentStyle; -use crate::codegen::CodeWriter; -use heck::{ToShoutySnakeCase, ToSnakeCase}; -use weaveffi_model::model::ParamBinding; -use weaveffi_model::model::{AsyncBinding, CallShape, ErrorBinding, FnBinding, IteratorBinding}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; +use weaveffi_model::model::{CallShape, FnBinding}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy}; use weaveffi_model::ty::{Prim, Ty}; -use crate::targets::ruby::callbacks::rb_vtable_const; -use crate::targets::ruby::docs::{emit_param_docs, emit_return_doc}; -use crate::targets::ruby::entities::{rb_checker_name, rb_error_factory_name}; +use crate::codegen::docs::Doc; +use crate::codegen::CodeWriter; +use crate::targets::ruby::docs::{self, CallableDoc, ParamDoc}; use crate::targets::ruby::types::{ - rb_abi_types, rb_direct_type, rb_ffi_type, rb_member_name, rb_param_name, rb_str_literal, + rb_checked_int, rb_const, rb_elem, rb_error_arg, rb_ffi_type, rb_ffi_types, rb_function_name, + rb_item_kind, rb_member_name, rb_param_doc_type, rb_param_name, rb_result_kind, + rb_ret_doc_type, rb_return_kind, rb_scalar, rb_slot, rb_str_literal, rb_wire, }; use crate::targets::ruby::RbCtx; /// Where a rendered Ruby callable lives and how it's spelled. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ScopeKind { - /// A module-level free function (`def self.name` on the top-level module). + /// A module-level function (`def self.name` on the gem's module). Free, - /// An instance method on an interface class: `def name`, borrowing the - /// wrapper's own pointer as the leading C argument. + /// An instance method on an interface class (`def name`), pinning the + /// wrapper's own pointer as the receiver slot. Method, /// A static member of an interface class (`def self.name`). Static, - /// A non-`new` constructor: a class method adopting the returned - /// reference through `_from_ptr` (never re-running `initialize`). + /// A constructor other than `new`: a class method returning a wrapper + /// that adopted the returned reference (never re-running `initialize`). Factory, - /// The canonical `new` constructor, emitted as `initialize`. + /// The `new` constructor, emitted as `initialize`. Init, } -/// The scope of one rendered callable: its kind, the top-level Ruby module, -/// and (for an interface member) the class it belongs to. +/// The scope of one rendered callable: its kind and, for an interface +/// member, the class it belongs to. pub(crate) struct RbScope<'a> { /// Where the callable lives. pub(crate) kind: ScopeKind, - /// The top-level Ruby module name. - pub(crate) module: &'a str, /// The interface class of a member; `None` for a free function. pub(crate) class: Option<&'a str>, } -impl<'a> RbScope<'a> { - /// The scope of a free function. - pub(crate) fn free(module: &'a str) -> Self { - RbScope { - kind: ScopeKind::Free, - module, - class: None, - } - } - - /// The scope of an interface member of the given kind. - pub(crate) fn member(kind: ScopeKind, module: &'a str, class: &'a str) -> Self { - RbScope { - kind, - module, - class: Some(class), - } - } - - /// The receiver prefix for module singleton calls (attached C symbols, - /// error checkers, runtime helpers): `"{ModuleName}."` inside a class - /// body, empty at module scope, where `self` already is the module. - fn qualifier(&self) -> String { - match self.kind { - ScopeKind::Free => String::new(), - _ => format!("{}.", self.module), - } - } - - /// The Ruby name of `f` in this scope: snake_case, escaped away from - /// the wrapper's own methods for an interface member. +impl RbScope<'_> { + /// The Ruby name of `f` in this scope. fn name(&self, f: &FnBinding) -> String { match self.kind { ScopeKind::Method => rb_member_name(&f.name, true), ScopeKind::Static | ScopeKind::Factory => rb_member_name(&f.name, false), - ScopeKind::Free | ScopeKind::Init => f.name.to_snake_case(), + ScopeKind::Free => rb_function_name(&f.name), + ScopeKind::Init => "initialize".to_string(), } } /// The `def` opener for `f` with the given formal parameters. - fn def_open(&self, f: &FnBinding, params: &[String]) -> String { - let args = if params.is_empty() { + fn def_open(&self, f: &FnBinding, formals: &[String]) -> String { + let args = if formals.is_empty() { String::new() } else { - format!("({})", params.join(", ")) + format!("({})", formals.join(", ")) }; let name = self.name(f); match self.kind { - ScopeKind::Method => format!("def {name}{args}"), + ScopeKind::Method | ScopeKind::Init => format!("def {name}{args}"), ScopeKind::Free | ScopeKind::Static | ScopeKind::Factory => { format!("def self.{name}{args}") } - ScopeKind::Init => format!("def initialize{args}"), } } /// How a caller spells `f`: `Kvstore.open_store`, `Kvstore::Store#size`, /// `Kvstore::Store.open`, or `Kvstore::Store.new`. - fn display(&self, f: &FnBinding) -> String { + fn display(&self, module: &str, f: &FnBinding) -> String { let name = self.name(f); - let module = self.module; match (self.kind, self.class) { (ScopeKind::Method, Some(class)) => format!("{module}::{class}#{name}"), (ScopeKind::Init, Some(class)) => format!("{module}::{class}.new"), @@ -129,219 +95,203 @@ impl<'a> RbScope<'a> { } } -/// Render one callable: a free function or an interface member. `error` is -/// the domain in scope for throwing callables; `scope` picks the def -/// spelling, receiver, and result handling. -pub(crate) fn render_callable( - w: &mut CodeWriter, - ctx: &RbCtx, - error: Option<&ErrorBinding>, - f: &FnBinding, - scope: &RbScope, -) { - w.blank(); - w.doc(&f.doc, DocCommentStyle::Hash); - match &f.shape { - CallShape::Sync(_) => {} - CallShape::Async(_) => { - w.line("# Blocks until the call completes on a producer thread."); - } - CallShape::Iterator(_) => { - w.line("# Returns a lazy Enumerator that pulls one element per step. The"); - w.line("# producer iterator starts on the first pull and is released when"); - w.line("# iteration finishes or is abandoned early."); - } - } - emit_param_docs(w, &f.params); - if f.cancellable { - w.line("# @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled"); - } - if scope.kind != ScopeKind::Init { - emit_return_doc(w, f.ret.as_ref()); - } - if let Some(msg) = &f.deprecated { - w.line(format!("# @deprecated {msg}")); - } - match &f.shape { - CallShape::Sync(abi) => render_sync(w, ctx, error, f, &abi.symbol, scope), - CallShape::Async(a) => render_async(w, ctx, f, a, scope), - CallShape::Iterator(it) => render_iterator(w, ctx, error, f, it, scope), - } -} - -/// Attach the C symbols for one callable: the plain symbol for a sync shape, -/// the launcher for an async shape (its completion trampoline is rendered -/// by [`render_async_trampoline`]), and the launch/next/destroy triple for -/// an iterator. -/// -/// Calls release the GVL (`blocking: true`) so a long call doesn't stall -/// other Ruby threads and a producer thread can run a callback -/// implementation while the call is in flight; an iterator's `_destroy`, -/// like an object's `_clone` and `_destroy`, is a trivial helper that keeps -/// it (and may therefore run from a GC finalizer). -pub(crate) fn render_attach_function(w: &mut CodeWriter, f: &FnBinding) { - match &f.shape { - CallShape::Sync(abi) => { - w.line(format!( - "attach_function :{}, [{}], {}, blocking: true", - abi.symbol, - rb_abi_types(&abi.params).join(", "), - rb_ffi_type(&abi.ret), - )); - } - CallShape::Async(a) => { - w.line(format!( - "attach_function :{}, [{}], :void, blocking: true", - a.launch.symbol, - rb_abi_types(&a.launch.params).join(", ") - )); - } - CallShape::Iterator(it) => { - w.line(format!( - "attach_function :{}, [{}], :pointer, blocking: true", - it.launch.symbol, - rb_abi_types(&it.launch.params).join(", ") - )); - w.line(format!( - "attach_function :{}, [{}], :int32, blocking: true", - it.next.symbol, - rb_abi_types(&it.next.params).join(", ") - )); - w.line(format!( - "attach_function :{}, [:pointer], :void", - it.destroy_symbol, - )); - } +/// Emit the `attach_function` lines of one callable inside `Native`: the +/// symbol it calls (the sync entry, the async launcher, or the iterator +/// launcher) and, for an iterator, its `_next` and `_destroy`. An +/// iterator's `_destroy`, like an object's `_clone` and `_destroy`, is a +/// trivial helper that keeps the GVL (and may therefore run from a GC +/// finalizer). +pub(crate) fn render_attach(w: &mut CodeWriter, f: &FnBinding) { + w.line(format!( + "attach_function :{}, {}, {}, blocking: true", + f.abi.symbol, + rb_ffi_types(&f.abi.params), + rb_ffi_type(&f.abi.ret), + )); + if let Some(it) = f.iterator() { + w.line(format!( + "attach_function :{}, {}, {}, blocking: true", + it.next.symbol, + rb_ffi_types(&it.next.params), + rb_ffi_type(&it.next.ret), + )); + w.line(format!( + "attach_function :{}, [:pointer], :void", + it.destroy_symbol + )); } } -/// The constant pinning an async function's completion trampoline, named -/// after its C callback typedef: `kvstore_kv_Store_compact_callback` becomes -/// `KVSTORE_KV_STORE_COMPACT_CALLBACK`. -fn rb_async_const(a: &AsyncBinding) -> String { - a.callback_type.to_shouty_snake_case() +/// The Ruby formal parameter of each IDL parameter, in order. A +/// cancellable callable's `cancel:` keyword is reserved, so a parameter +/// named `cancel` there is `cancel_`. +fn formal_names(f: &FnBinding) -> Vec { + f.params + .iter() + .map(|p| { + let name = rb_param_name(&p.name); + if f.cancellable() && name == "cancel" { + "cancel_".to_string() + } else { + name + } + }) + .collect() } -/// The error a completion's `taken` triple becomes: the domain error for a -/// throwing function, the trap (`NativeBugError`, or `Cancelled`) -/// otherwise. -fn rb_error_from_taken(error: Option<&ErrorBinding>, f: &FnBinding) -> String { - match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => format!("{}(*taken)", rb_error_factory_name(eb)), - _ => "_wv_trap(*taken)".to_string(), +/// The Ruby formal parameters of `f`. Trailing optional parameters (`T?`, +/// `Cb?`) default to `nil`, so callers may leave them out; a cancellable +/// callable ends with the `cancel:` keyword. +fn formals(f: &FnBinding) -> Vec { + let names = formal_names(f); + let optional = f + .params + .iter() + .rev() + .take_while(|p| match &p.pass { + ArgPass::Callback { nullable, .. } => *nullable, + _ => matches!(p.ty.value(), Some(Ty::Optional(_))), + }) + .count(); + let first_optional = f.params.len() - optional; + let mut out: Vec = names + .into_iter() + .enumerate() + .map(|(i, name)| { + if i >= first_optional { + format!("{name} = nil") + } else { + name + } + }) + .collect(); + if f.cancellable() { + out.push("cancel: nil".to_string()); } + out } -/// Render the module-level completion trampoline of an async function (a -/// no-op for any other shape). It resolves the call's queue from -/// `context`, converts the error or result (copying and releasing any owned -/// buffer), and pushes it; nothing raised while converting escapes into the -/// C frame, so the waiting call always wakes up. -pub(crate) fn render_async_trampoline( - w: &mut CodeWriter, - ctx: &RbCtx, - error: Option<&ErrorBinding>, - f: &FnBinding, -) { - let CallShape::Async(a) = &f.shape else { - return; - }; - let mut formals = vec!["ctx".to_string(), "err".to_string()]; - formals.extend(a.callback_params.iter().skip(2).map(|p| p.name.clone())); - w.blank(); - w.line("# @api private"); - w.line(format!("# Completion trampoline for {}.", f.name)); - w.block( - format!( - "{} = FFI::Function.new(:void, [{}]) do |{}|", - rb_async_const(a), - rb_abi_types(&a.callback_params).join(", "), - formals.join(", ") - ), - "end", - |w| { - w.line("queue = _wv_async_finish(ctx)"); - w.line("begin"); - w.scope(|w| { - w.line("taken = _wv_take_boxed_error(err)"); - w.line("if taken"); - w.scope(|w| { - w.line(format!("queue << {}", rb_error_from_taken(error, f))); - }); - w.line("else"); - w.scope(|w| { - let ptr = match RetPass::of(f.ret.as_ref()) { - RetPass::String | RetPass::Bytes | RetPass::Buffer => "result_ptr", - _ => "result", - }; - let value = receive_value(ctx, f.ret.as_ref(), ptr, "result_len", ""); - w.line(format!("value = {value}")); - w.line("queue << value"); - }); - w.line("end"); - }); - w.line("rescue Exception => e # rubocop:disable Lint/RescueException"); - w.scope(|w| { - w.line("queue << e"); - }); - w.line("end"); - }, - ); +/// What converting a callable's arguments produced: the statements to run +/// first, the value-buffer writers the call helper seals, the objects to +/// pin, the callback registrations to make immediately before the call, +/// and the C arguments in slot order. +#[derive(Default)] +struct Prepared { + /// Checks and conversions, run before anything is pinned or sealed. + lines: Vec, + /// The locals holding value-buffer writers; the helper yields their + /// sealed encodings under the same names. + writers: Vec, + /// `(Ruby expression, pinned pointer local)` pairs. + pins: Vec<(String, String)>, + /// Callback registrations, which never raise, so nothing can strand + /// one between it and the call that hands it over. + registers: Vec, + /// The C arguments, receiver first. + args: Vec, } -/// The expression turning a received value into its Ruby value: a sync -/// return (`ptr` names the C result, `len` the length read from `out_len`), -/// an async result, or an iterator element. Owned strings, bytes, and -/// buffers are copied and released; objects are adopted. -fn receive_value(ctx: &RbCtx, ty: Option<&Ty>, ptr: &str, len: &str, q: &str) -> String { - match RetPass::of(ty) { - RetPass::Void => "nil".to_string(), - RetPass::Direct => ptr.to_string(), - RetPass::String => format!("{q}_wv_take_string({ptr}, {len})"), - RetPass::Bytes => format!("{q}_wv_take_bytes({ptr}, {len})"), - RetPass::Buffer => { - let ty = ty.expect("buffered value has a type"); - format!( - "{q}_wv_decode({ptr}, {len}) {{ |r| {} }}", - ctx.codecs.read(ty, "r", q) - ) - } - RetPass::Object { nullable } => { - let class = ty - .and_then(Ty::interface_name) - .expect("object value names an interface"); - if nullable { - format!("{ptr}.null? ? nil : {class}._from_ptr({ptr})") - } else { - format!("{class}._from_ptr({q}_wv_nonnull({ptr}))") - } +/// Check and convert every argument of `f` per its [`ArgPass`]. +fn prepare(f: &FnBinding, scope: &RbScope) -> Prepared { + let mut out = Prepared::default(); + if let Some(receiver) = &f.receiver { + if scope.kind == ScopeKind::Method { + let local = rb_slot(receiver); + out.pins.push(("self".to_string(), local.clone())); + out.args.push(local); } } -} - -/// The objects a call borrows, as `(Ruby expression, pinned pointer local)` -/// pairs: the receiver for an instance method, then each object parameter. -fn pinned_objects(f: &FnBinding, scope: &RbScope) -> Vec<(String, String)> { - let mut pins = Vec::new(); - if scope.kind == ScopeKind::Method { - pins.push(("self".to_string(), "_wv_self".to_string())); - } - for p in &f.params { - if matches!(p.arg_pass(), ArgPass::Object { .. }) { - let name = rb_param_name(&p.name); - pins.push((name.clone(), format!("_wv_{name}"))); + for (p, formal) in f.params.iter().zip(formal_names(f)) { + match &p.pass { + ArgPass::Direct { slot } => { + let local = rb_slot(slot); + let ty = p.ty.value().expect("a direct parameter is a value"); + if let Some(kind) = rb_checked_int(ty) { + out.lines + .push(format!("{local} = Bridge.scalar({formal}, {kind})")); + out.args.push(local); + } else if matches!(ty, Ty::Prim(Prim::Bool)) { + out.args.push(format!("({formal} ? true : false)")); + } else { + out.args.push(formal); + } + } + ArgPass::OptDirect { has, value, inner } => { + let (has, value) = (rb_slot(has), rb_slot(value)); + out.lines.push(format!( + "{has}, {value} = Bridge.opt_arg({formal}, {})", + rb_scalar(inner) + )); + out.args.extend([has, value]); + } + ArgPass::Slice { ptr, len, elem } => { + let (ptr, len) = (rb_slot(ptr), rb_slot(len)); + out.lines.push(format!( + "{ptr}, {len} = Bridge.slice_arg({formal}, {})", + rb_elem(*elem) + )); + out.args.extend([ptr, len]); + } + ArgPass::String { ptr, .. } | ArgPass::Bytes { ptr, .. } => { + let helper = if matches!(p.pass, ArgPass::String { .. }) { + "string_arg" + } else { + "bytes_arg" + }; + let ptr = rb_slot(ptr); + out.lines.push(format!("{ptr} = Bridge.{helper}({formal})")); + out.args.extend([ptr.clone(), format!("{ptr}.bytesize")]); + } + ArgPass::Buffer { ptr, .. } => { + let ty = p.ty.value().expect("a buffered parameter is a value"); + let ptr = rb_slot(ptr); + out.lines + .push(format!("{ptr} = Bridge.encode({}, {formal})", rb_wire(ty))); + out.writers.push(ptr.clone()); + out.args.extend([ptr.clone(), format!("{ptr}.bytesize")]); + } + ArgPass::Object { + slot, + nullable, + interface, + } => { + let nullable = if *nullable { ", nullable: true" } else { "" }; + out.lines.push(format!( + "Bridge.check_object({formal}, {}, '{formal}'{nullable})", + rb_const(interface) + )); + let local = rb_slot(slot); + out.pins.push((formal, local.clone())); + out.args.push(local); + } + ArgPass::Callback { + ctx, + vtable, + nullable, + interface, + } => { + if !nullable { + out.lines + .push(format!("Bridge.present!({formal}, '{formal}')")); + } + let (ctx, vtable) = (rb_slot(ctx), rb_slot(vtable)); + out.registers.push(format!( + "{ctx}, {vtable} = Bridge.register({formal}, {})", + rb_const(interface) + )); + out.args.extend([ctx, vtable]); + } } } - pins + out } -/// Run `body` inside a `_wv_pin` block over `pins` (each object stays alive, -/// and its wrapper can't release its reference, until the block returns), -/// or directly when nothing is borrowed. `lead` prefixes the block opener -/// (an assignment of the block's value, or empty). +/// Run `body` inside a `Bridge.pin` block over `pins` (each object stays +/// alive, and its wrapper can't release its reference, until the block +/// returns), or directly when nothing is borrowed. `lead` prefixes the +/// opener (an assignment of the block's value, or empty). fn with_pins( w: &mut CodeWriter, - q: &str, lead: &str, pins: &[(String, String)], body: impl FnOnce(&mut CodeWriter), @@ -351,370 +301,262 @@ fn with_pins( return; } let exprs: Vec<&str> = pins.iter().map(|(e, _)| e.as_str()).collect(); - let vars: Vec<&str> = pins.iter().map(|(_, v)| v.as_str()).collect(); + let locals: Vec<&str> = pins.iter().map(|(_, l)| l.as_str()).collect(); w.block( format!( - "{lead}{q}_wv_pin({}) do |{}|", + "{lead}Bridge.pin({}) do |{}|", exprs.join(", "), - vars.join(", ") + locals.join(", ") ), "end", body, ); } -/// Emit the statements validating the parameters and converting them into -/// the locals the C call borrows. Strings and bytes become private binary -/// copies and buffered values are encoded; object arguments are checked -/// against their interface and a required callback implementation against -/// `nil`. Callback implementations are registered later (see -/// [`render_handoff`]), so nothing that can raise runs between a -/// registration and the call that hands it to the producer. -fn render_param_prep(w: &mut CodeWriter, ctx: &RbCtx, f: &FnBinding, q: &str) { - for p in &f.params { - let name = rb_param_name(&p.name); - match p.arg_pass() { - ArgPass::String { .. } => { - w.line(format!("{name}_s = {q}_wv_str({name})")); - } - ArgPass::Bytes { .. } => { - w.line(format!("{name}_s = {q}_wv_bytes({name})")); - } - ArgPass::Buffer { .. } if ctx.codecs.carries_objects(&p.ty) => { - w.line(format!("{name}_w = WvBufferWriter.new")); - w.line(ctx.codecs.write(&p.ty, &format!("{name}_w"), &name, q)); - } - ArgPass::Buffer { .. } => { - w.line(format!( - "{name}_s = {q}_wv_encode {{ |w| {} }}", - ctx.codecs.write(&p.ty, "w", &name, q) - )); - } - ArgPass::Object { nullable, .. } => { - let class = - p.ty.interface_name() - .expect("object plan names an interface"); - let optional = if nullable { ", true" } else { "" }; - w.line(format!( - "{q}_wv_object!({name}, {class}, '{name}'{optional})" - )); - } - ArgPass::Callback { - nullable: false, .. - } => { - w.line(format!("{q}_wv_present!({name}, '{name}')")); - } - ArgPass::Callback { .. } | ArgPass::Direct { .. } => {} - } - } -} +/// The longest line kept on one line before a call's arguments wrap. +const MAX_LINE: usize = 100; -/// Emit the statements that hand the producer what it adopts, immediately -/// before the call: seal the object-carrying buffers (minting the -/// references their tokens carry) and then register every callback -/// implementation. Registration can't raise, so nothing between here and -/// the call can strand a minted reference or a registration. -fn render_handoff(w: &mut CodeWriter, ctx: &RbCtx, f: &FnBinding, q: &str) { - let sealed: Vec = f - .params - .iter() - .filter(|p| matches!(p.arg_pass(), ArgPass::Buffer { .. })) - .filter(|p| ctx.codecs.carries_objects(&p.ty)) - .map(|p| rb_param_name(&p.name)) - .collect(); - match sealed.as_slice() { - [] => {} - [one] => { - w.line(format!("{one}_s = {q}_wv_seal({one}_w).first")); - } - many => { - let locals: Vec = many.iter().map(|n| format!("{n}_s")).collect(); - let writers: Vec = many.iter().map(|n| format!("{n}_w")).collect(); - w.line(format!( - "{} = {q}_wv_seal({})", - locals.join(", "), - writers.join(", ") - )); - } +/// Emit `{head}({args}){tail}` on one line, or with one argument per line +/// when that's too long. +fn emit_call(w: &mut CodeWriter, head: &str, args: &[String], tail: &str) { + let one_line = format!("{head}({}){tail}", args.join(", ")); + if w.indent_str().len() + one_line.len() <= MAX_LINE { + w.line(one_line); + return; } - for p in &f.params { - if matches!(p.arg_pass(), ArgPass::Callback { .. }) { - let name = rb_param_name(&p.name); - let cb = - p.ty.callback_interface_name() - .expect("callback plan names a callback interface"); - w.line(format!( - "{name}_ctx, {name}_vtable = {q}_wv_cb_register({name}, {})", - rb_vtable_const(cb) - )); + w.line(format!("{head}(")); + w.scope(|w| { + for a in args { + w.line(format!("{a},")); } - } + }); + w.line(format!("){tail}")); } -/// The Ruby argument expressions one parameter contributes to the C call, -/// per its [`ArgPass`] contract. -fn rb_call_args(p: &ParamBinding) -> Vec { - let name = rb_param_name(&p.name); - match p.arg_pass() { - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { - vec![format!("{name}_s"), format!("{name}_s.bytesize")] - } - ArgPass::Object { .. } => vec![format!("_wv_{name}")], - ArgPass::Callback { .. } => vec![format!("{name}_ctx"), format!("{name}_vtable")], - ArgPass::Direct { .. } if matches!(p.ty, Ty::Prim(Prim::Bool)) => { - vec![format!("({name} ? true : false)")] - } - ArgPass::Direct { .. } => vec![name], - } +/// Emit a helper call with a block: `{head}({args}) do |params|`, the +/// block's body, and its `end`. +fn emit_block_call( + w: &mut CodeWriter, + head: &str, + args: &[String], + params: &[String], + body: impl FnOnce(&mut CodeWriter), +) { + emit_call(w, head, args, &format!(" do{}", block_params(params))); + w.scope(body); + w.line("end"); } -/// The leading C arguments of a call: the pinned receiver, then every -/// parameter's slots. -fn call_args(f: &FnBinding, scope: &RbScope) -> Vec { - let mut args = Vec::new(); - if scope.kind == ScopeKind::Method { - args.push("_wv_self".to_string()); +/// Emit the C call itself, after the callback registrations that must +/// immediately precede it. +fn emit_native_call(w: &mut CodeWriter, f: &FnBinding, prep: &Prepared, args: &[String]) { + for r in &prep.registers { + w.line(r); } - for p in &f.params { - args.extend(rb_call_args(p)); - } - args + emit_call(w, &format!("Native.{}", f.abi.symbol), args, ""); } -/// The Ruby formal parameters of `f`. Trailing optional parameters (`T?`, -/// `Cb?`) default to `nil`, so callers may leave them out. -fn formals(f: &FnBinding) -> Vec { - let optional = f - .params - .iter() - .rev() - .take_while(|p| matches!(p.ty, Ty::Optional(_))) - .count(); - let first_optional = f.params.len() - optional; - f.params - .iter() - .enumerate() - .map(|(i, p)| { - let name = rb_param_name(&p.name); - if i >= first_optional { - format!("{name} = nil") - } else { - name - } - }) - .collect() +/// The block parameter list `|a, b|` of a helper call. +fn block_params(params: &[String]) -> String { + if params.is_empty() { + String::new() + } else { + format!(" |{}|", params.join(", ")) + } } -/// Warn, pointing at the caller, when a deprecated callable is used (the -/// warning is silenced with `$VERBOSE = nil`, like any Ruby warning). -fn render_deprecation(w: &mut CodeWriter, f: &FnBinding, scope: &RbScope) { - if let Some(msg) = &f.deprecated { - w.line(format!( - "warn('{} is deprecated: {}', uplevel: 1)", - scope.display(f), - rb_str_literal(msg) - )); - } +/// A helper call's arguments: the leading ones, then the writers it seals. +fn helper_args(lead: Vec, writers: &[String]) -> Vec { + lead.into_iter().chain(writers.iter().cloned()).collect() } -/// Render the sync wrapper: convert the parameters, pin the borrowed -/// objects, make the C call with a fresh `ErrorStruct`, route the out-err -/// slot through the function's checker, then receive the result per the -/// scope. -fn render_sync( - w: &mut CodeWriter, - ctx: &RbCtx, - error: Option<&ErrorBinding>, - f: &FnBinding, - symbol: &str, - scope: &RbScope, -) { - let q = scope.qualifier(); - let checker = rb_checker_name(f, error); - let has_out_len = matches!( - RetPass::of(f.ret.as_ref()), - RetPass::String | RetPass::Bytes | RetPass::Buffer - ); +/// Render one callable: a free function or an interface member. +pub(crate) fn render_callable(w: &mut CodeWriter, ctx: &RbCtx, f: &FnBinding, scope: &RbScope) { + render_doc(w, ctx, f, scope); + let prepared = prepare(f, scope); w.block(scope.def_open(f, &formals(f)), "end", |w| { - render_deprecation(w, f, scope); - render_param_prep(w, ctx, f, &q); - with_pins(w, &q, "", &pinned_objects(f, scope), |w| { - w.line("err = ErrorStruct.new"); - let mut args = call_args(f, scope); - if has_out_len { - w.line("out_len = FFI::MemoryPointer.new(:size_t)"); - args.push("out_len".into()); - } - args.push("err".into()); - render_handoff(w, ctx, f, &q); - let call = format!("{q}{symbol}({})", args.join(", ")); - if f.ret.is_some() { - w.line(format!("result = {call}")); - } else { - w.line(call); - } - w.line(format!("{q}{checker}(err)")); - match scope.kind { - ScopeKind::Init => { - w.line(format!("_wv_init({q}_wv_nonnull(result))")); - } - ScopeKind::Factory => { - w.line(format!("_from_ptr({q}_wv_nonnull(result))")); - } - _ if f.ret.is_some() => { - w.line(receive_value( - ctx, - f.ret.as_ref(), - "result", - "out_len.read(:size_t)", - &q, - )); - } - _ => { - w.line("nil"); - } - } - }); + if f.deprecated.is_some() { + let msg = Doc::new(&f.doc, &f.deprecated) + .deprecation(|i| docs::spell(&ctx.names, i)) + .unwrap_or_default(); + w.line(format!( + "warn('{} is deprecated: {}', uplevel: 1, category: :deprecated)", + scope.display(ctx.module, f), + rb_str_literal(&msg) + )); + } + for line in &prepared.lines { + w.line(line); + } + match &f.shape { + CallShape::Async(_) => render_async(w, f, &prepared), + CallShape::Sync if f.iterator().is_some() => render_iterator(w, f, &prepared), + CallShape::Sync => render_sync(w, f, scope, &prepared), + } }); } -/// Render the async wrapper: launch the call with the function's completion -/// trampoline and block on a `Queue` until it fires (`Queue#pop` releases -/// the GVL; the ffi gem runs the trampoline on a Ruby thread). Blocking is -/// the idiomatic Ruby surface; callers wanting concurrency run the call in a -/// Thread. -/// -/// A cancellable function takes a `cancel:` keyword. Without one, the -/// wrapper still passes a private token, so interrupting the wait -/// (`Thread#raise`, `Timeout`) cancels the producer's work too. -fn render_async(w: &mut CodeWriter, ctx: &RbCtx, f: &FnBinding, a: &AsyncBinding, scope: &RbScope) { - let q = scope.qualifier(); - let mut params = formals(f); - if f.cancellable { - params.push("cancel: nil".into()); +/// The YARD comment of a callable: its doc, a note on its shape, every +/// parameter, the return, and what it raises. +fn render_doc(w: &mut CodeWriter, ctx: &RbCtx, f: &FnBinding, scope: &RbScope) { + let mut extra = CallableDoc::default(); + if f.is_async() { + extra.notes.push( + "Blocks until the call completes on a library thread (under a Fiber \ + scheduler, only the calling Fiber waits)." + .to_string(), + ); } - w.block(scope.def_open(f, ¶ms), "end", |w| { - render_deprecation(w, f, scope); - render_param_prep(w, ctx, f, &q); - if f.cancellable { - w.line("own_token = CancelToken.new if cancel.nil?"); - w.line("token = cancel || own_token"); - } - let mut args = call_args(f, scope); - if f.cancellable { - args.push("token._wv_ptr".into()); - } - args.push(rb_async_const(a)); - args.push("ctx".into()); - let launch = format!("{q}{}({})", a.launch.symbol, args.join(", ")); - let pins = pinned_objects(f, scope); - if pins.is_empty() { - render_handoff(w, ctx, f, &q); - w.line(format!("ctx, queue = {q}_wv_async_begin")); - w.line(launch); - } else { - with_pins(w, &q, "queue = ", &pins, |w| { - render_handoff(w, ctx, f, &q); - w.line(format!("ctx, pending = {q}_wv_async_begin")); - w.line(launch); - w.line("pending"); - }); + if f.iterator().is_some() { + extra.notes.push( + "Returns a lazy Enumerator: each enumeration starts its own native \ + iterator on the first pull and releases it when iteration finishes \ + or stops early." + .to_string(), + ); + } + for (p, name) in f.params.iter().zip(formal_names(f)) { + extra.params.push(ParamDoc { + name, + ty: rb_param_doc_type(&p.ty), + doc: p.doc.clone(), + }); + } + if f.cancellable() { + extra.params.push(ParamDoc { + name: "cancel".to_string(), + ty: "CancelToken, nil".to_string(), + doc: Some("cancels the call, which then raises Cancelled".to_string()), + }); + } + if scope.kind != ScopeKind::Init { + extra.ret = f.ret.as_ref().map(rb_ret_doc_type); + } + match &f.error { + ErrorStrategy::Trap => {} + ErrorStrategy::Untyped => { + extra + .raises + .push(("Error".to_string(), "when the call fails".to_string())); } - if f.cancellable { - w.line(format!("{q}_wv_async_wait(queue, token)")); - w.dedent(); - w.line("ensure"); - w.indent(); - w.line("own_token&.close"); - } else { - w.line(format!("{q}_wv_async_wait(queue)")); + ErrorStrategy::Domain(name) => { + extra.raises.push(( + crate::targets::ruby::types::rb_domain(name), + "for a domain error".to_string(), + )); } + } + if f.cancellable() { + extra.raises.push(( + "Cancelled".to_string(), + "when `cancel` fires first".to_string(), + )); + } + docs::emit(w, &ctx.names, &Doc::new(&f.doc, &f.deprecated), &extra); +} + +/// The name of the error slot (the last C parameter of a sync call, an +/// iterator launcher, or `_next`). +fn err_slot(f: &FnBinding) -> String { + rb_slot(f.abi.params.last().expect("a call carries out_err")) +} + +/// Render a sync wrapper body: pin the borrowed objects, then hand the C +/// call to `Bridge.call`. +fn render_sync(w: &mut CodeWriter, f: &FnBinding, scope: &RbScope, prep: &Prepared) { + let kind = if scope.kind == ScopeKind::Init { + Some(":pointer".to_string()) + } else { + rb_return_kind(&f.ret_pass, f.ret.as_ref().and_then(|r| r.value())) + }; + let mut lead = vec![rb_error_arg(&f.error)]; + match kind { + Some(kind) => lead.push(kind), + None if !prep.writers.is_empty() => lead.push("nil".to_string()), + None => {} + } + let mut params = prep.writers.clone(); + params.extend(f.ret_pass.out_slots().into_iter().map(rb_slot)); + params.push(err_slot(f)); + let mut args = prep.args.clone(); + args.extend(f.ret_pass.out_slots().into_iter().map(rb_slot)); + args.push(err_slot(f)); + let assign = if scope.kind == ScopeKind::Init { + "ptr = " + } else { + "" + }; + let call = |w: &mut CodeWriter, lead_text: &str| { + emit_block_call( + w, + &format!("{lead_text}Bridge.call"), + &helper_args(lead.clone(), &prep.writers), + ¶ms, + |w| emit_native_call(w, f, prep, &args), + ); + }; + if prep.pins.is_empty() { + call(w, assign); + } else { + with_pins(w, assign, &prep.pins, |w| call(w, "")); + } + if scope.kind == ScopeKind::Init { + w.line("_wv_init(ptr)"); + } +} + +/// Render an async wrapper body: launch the call through `Bridge.await` +/// (or `Bridge.await_cancellable`), which blocks on a queue until the +/// completion delivers the result or the error. +fn render_async(w: &mut CodeWriter, f: &FnBinding, prep: &Prepared) { + let a = f.async_binding().expect("an async shape has a binding"); + let ret = f.ret.as_ref().and_then(|r| r.value()); + let mut lead = vec![rb_error_arg(&f.error), rb_result_kind(&a.result, ret)]; + let mut params = prep.writers.clone(); + let mut args = prep.args.clone(); + let helper = if let Some(token) = &a.cancel_token { + lead.push("cancel".to_string()); + params.push(rb_slot(token)); + args.push(rb_slot(token)); + "await_cancellable" + } else { + "await" + }; + // The launcher's last two slots are the completion and its context. + let tail = &f.abi.params[f.abi.params.len() - 2..]; + params.extend(tail.iter().map(rb_slot)); + args.extend(tail.iter().map(rb_slot)); + with_pins(w, "", &prep.pins, |w| { + emit_block_call( + w, + &format!("Bridge.{helper}"), + &helper_args(lead, &prep.writers), + ¶ms, + |w| emit_native_call(w, f, prep, &args), + ); }); } -/// Render the iterator wrapper: a lazy `Enumerator` per the pull contract of -/// [`weaveffi_model::plan::IteratorProtocol`]. -/// -/// The producer iterator launches inside the enumerator block, on the first -/// pull, so nothing leaks when the enumerator is never started (launch -/// errors therefore raise on the first pull). Each step issues exactly one -/// `next` call, each element is received like a return of its type, and -/// `destroy` runs exactly once from an `ensure`, so an early `break` or an -/// error mid-iteration still releases the handle. -fn render_iterator( - w: &mut CodeWriter, - ctx: &RbCtx, - error: Option<&ErrorBinding>, - f: &FnBinding, - it: &IteratorBinding, - scope: &RbScope, -) { - let q = scope.qualifier(); - let checker = rb_checker_name(f, error); - let elem_pass = RetPass::of(Some(&it.elem)); - let needs_len = matches!( - elem_pass, - RetPass::String | RetPass::Bytes | RetPass::Buffer +/// Render an iterator wrapper body: `Bridge.iterate` returns a lazy +/// Enumerator whose every enumeration runs the launch block (pinning, +/// sealing, and registering then) and pulls elements with `_next`. +fn render_iterator(w: &mut CodeWriter, f: &FnBinding, prep: &Prepared) { + let it = f.iterator().expect("an iterator shape has a binding"); + let lead = vec![ + rb_error_arg(&f.error), + rb_item_kind(&it.item, &it.elem), + format!(":{}", it.next.symbol), + format!(":{}", it.destroy_symbol), + ]; + let mut params = prep.writers.clone(); + params.push(err_slot(f)); + let mut args = prep.args.clone(); + args.push(err_slot(f)); + emit_block_call( + w, + "Bridge.iterate", + &helper_args(lead, &prep.writers), + ¶ms, + |w| with_pins(w, "", &prep.pins, |w| emit_native_call(w, f, prep, &args)), ); - w.block(scope.def_open(f, &formals(f)), "end", |w| { - render_deprecation(w, f, scope); - render_param_prep(w, ctx, f, &q); - // The block closes over the converted arguments, so they stay - // referenced until the launch call runs. - w.block("Enumerator.new do |y|", "end", |w| { - w.line("err = ErrorStruct.new"); - let mut args = call_args(f, scope); - args.push("err".into()); - let launch = format!("{q}{}({})", it.launch.symbol, args.join(", ")); - let pins = pinned_objects(f, scope); - if pins.is_empty() { - render_handoff(w, ctx, f, &q); - w.line(format!("iter = {launch}")); - } else { - with_pins(w, &q, "iter = ", &pins, |w| { - render_handoff(w, ctx, f, &q); - w.line(launch); - }); - } - w.line("begin"); - w.scope(|w| { - w.line(format!("{q}{checker}(err)")); - let item_type = match elem_pass { - RetPass::Direct => rb_direct_type(&it.elem), - _ => ":pointer", - }; - w.line(format!("out_item = FFI::MemoryPointer.new({item_type})")); - let mut next_args = vec!["iter", "out_item"]; - if needs_len { - w.line("out_len = FFI::MemoryPointer.new(:size_t)"); - next_args.push("out_len"); - } - next_args.push("err"); - w.block("loop do", "end", |w| { - w.line(format!( - "has_item = {q}{}({})", - it.next.symbol, - next_args.join(", ") - )); - w.line(format!("{q}{checker}(err)")); - w.line("break if has_item.zero?"); - let item = match elem_pass { - RetPass::Direct => format!("out_item.read({item_type})"), - _ => "out_item.read_pointer".to_string(), - }; - w.line(format!("item = {item}")); - w.line(format!( - "value = {}", - receive_value(ctx, Some(&it.elem), "item", "out_len.read(:size_t)", &q) - )); - w.line("y << value"); - }); - }); - w.line("ensure"); - w.scope(|w| { - w.line(format!("{q}{}(iter) unless iter.null?", it.destroy_symbol)); - }); - w.line("end"); - }); - }); } diff --git a/crates/weaveffi-cli/src/targets/ruby/codec.rs b/crates/weaveffi-cli/src/targets/ruby/codec.rs deleted file mode 100644 index 66c87871..00000000 --- a/crates/weaveffi-cli/src/targets/ruby/codec.rs +++ /dev/null @@ -1,475 +0,0 @@ -//! Value-buffer codecs: the expressions that pack and unpack one value, the -//! per-record and per-rich-enum codec pairs (`_wv_write_{stem}`, -//! `_wv_read_{stem}`), and one pair per distinct composite type (`[Entry?]`, -//! `{string:i64}`, `Store?`) used anywhere in the API, so no call site -//! inlines a loop. -//! -//! Every dispatch here goes through [`Ty::wire`], so this module never -//! re-derives the wire folds (records and rich enums as one user-codec -//! shape, C-style enums as `i32`, interfaces as `u64` object tokens). -//! -//! An object token carries one strong reference. Writing one reserves the -//! token (`write_object`); the runtime's `_wv_seal` mints the references -//! (the interface's `_clone` symbol) just before the call, so the wrapper -//! keeps its own reference and an abandoned encoding strands none. Reading -//! one (`read_object`) adopts the pointer into a fresh wrapper whose -//! finalizer owes the `_destroy`. - -use std::collections::{HashMap, HashSet}; - -use crate::codegen::CodeWriter; -use heck::ToSnakeCase; -use weaveffi_model::model::{ - CallShape, EnumBinding, FieldBinding, FnBinding, Model, StructBinding, -}; -use weaveffi_model::ty::{Family, Ty, WireType}; - -use crate::targets::ruby::types::rb_field_name; - -/// The indefinite article for `word` ("a" or "an"), by its first letter. -fn article(word: &str) -> &'static str { - match word.chars().next().map(|c| c.to_ascii_lowercase()) { - Some('a' | 'e' | 'i' | 'o' | 'u') => "an", - _ => "a", - } -} - -/// The longest constructor call kept on one line. -const MAX_LINE: usize = 100; - -/// The snake_case stem naming a record's or rich enum's codec pair: -/// `Contact` becomes `contact`, naming `_wv_write_contact` and -/// `_wv_read_contact`. -pub(crate) fn wv_stem(name: &str) -> String { - name.to_snake_case() -} - -/// The structural stem of a composite type before collision handling: -/// `[Entry?]` is `list_opt_entry`, `{string:i64}` is `map_string_i64`. -fn natural_stem(ty: &Ty) -> String { - match ty { - Ty::Prim(p) => p.snake().to_string(), - Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => wv_stem(n), - Ty::Optional(inner) => format!("opt_{}", natural_stem(inner)), - Ty::List(inner) => format!("list_{}", natural_stem(inner)), - Ty::Map(k, v) => format!("map_{}_{}", natural_stem(k), natural_stem(v)), - Ty::CallbackInterface(_) | Ty::Iterator(_) => { - unreachable!("{ty} never appears inside a value buffer") - } - } -} - -/// The records and rich enums of `model` whose fields can hold an object, -/// directly or through another record or rich enum (a fixed point, so -/// recursive types terminate). -fn user_types_with_objects(model: &Model) -> HashSet { - let mut fields: Vec<(&str, Vec<&Ty>)> = Vec::new(); - for m in &model.modules { - for s in &m.structs { - fields.push((&s.name, s.fields.iter().map(|f| &f.ty).collect())); - } - for e in m.enums.iter().filter(|e| e.is_rich()) { - let tys = e.variants.iter().flat_map(|v| &v.fields).map(|f| &f.ty); - fields.push((&e.name, tys.collect())); - } - } - let mut found: HashSet = HashSet::new(); - loop { - let before = found.len(); - for (name, tys) in &fields { - if found.contains(*name) { - continue; - } - let carries = tys.iter().any(|ty| { - ty.any(&|t| match t { - Ty::Interface(_) => true, - Ty::Record(n) | Ty::RichEnum(n) => found.contains(n), - _ => false, - }) - }); - if carries { - found.insert((*name).to_string()); - } - } - if found.len() == before { - return found; - } - } -} - -/// The composite types (optionals, lists, and maps inside value buffers) an -/// API uses, each with the stem naming its codec pair. -#[derive(Debug, Default)] -pub(crate) struct Codecs { - order: Vec, - stems: HashMap, - /// The records and rich enums whose encoding can hold an object token. - with_objects: HashSet, -} - -impl Codecs { - /// Collect every composite type `model` encodes or decodes: in - /// parameters, returns, async results, iterator elements, record - /// fields, variant fields, error payload fields, and callback - /// parameters and returns. - pub(crate) fn collect(model: &Model) -> Self { - let mut taken: HashSet = HashSet::new(); - for m in &model.modules { - taken.extend(m.structs.iter().map(|s| wv_stem(&s.name))); - taken.extend( - m.enums - .iter() - .filter(|e| e.is_rich()) - .map(|e| wv_stem(&e.name)), - ); - } - let mut codecs = Codecs { - with_objects: user_types_with_objects(model), - ..Codecs::default() - }; - let mut add = |ty: &Ty, codecs: &mut Codecs| codecs.visit(ty, &mut taken); - for m in &model.modules { - for f in m.callables() { - for p in &f.params { - codecs.position(&p.ty, &mut add); - } - codecs.result(f, &mut add); - } - for s in &m.structs { - for f in &s.fields { - add(&f.ty, &mut codecs); - } - } - for e in &m.enums { - for f in e.variants.iter().flat_map(|v| &v.fields) { - add(&f.ty, &mut codecs); - } - } - if let Some(eb) = &m.errors { - for f in eb.codes.iter().flat_map(|c| &c.fields) { - add(&f.ty, &mut codecs); - } - } - for cb in &m.callback_interfaces { - for cm in &cb.methods { - for p in &cm.params { - codecs.position(&p.ty, &mut add); - } - if let Some(ret) = &cm.ret { - codecs.position(ret, &mut add); - } - } - } - } - codecs - } - - /// Visit a parameter or return type: only a buffered one is encoded - /// (`Store?` crosses as a pointer, not a buffer). - fn position(&mut self, ty: &Ty, add: &mut impl FnMut(&Ty, &mut Codecs)) { - if ty.family() == Family::Buffer { - add(ty, self); - } - } - - /// Visit a callable's result: the return of a sync or async call, or - /// the element of an iterator. - fn result(&mut self, f: &FnBinding, add: &mut impl FnMut(&Ty, &mut Codecs)) { - match &f.shape { - CallShape::Iterator(it) => self.position(&it.elem, add), - _ => { - if let Some(ret) = &f.ret { - self.position(ret, add); - } - } - } - } - - /// Register `ty` (when it's a composite) and every composite inside it. - fn visit(&mut self, ty: &Ty, taken: &mut HashSet) { - match ty.wire() { - WireType::Optional(inner) | WireType::List(inner) => { - self.register(ty, taken); - self.visit(inner, taken); - } - WireType::Map(k, v) => { - self.register(ty, taken); - self.visit(k, taken); - self.visit(v, taken); - } - _ => {} - } - } - - fn register(&mut self, ty: &Ty, taken: &mut HashSet) { - if self.stems.contains_key(ty) { - return; - } - let natural = natural_stem(ty); - let mut stem = natural.clone(); - let mut n = 2; - while !taken.insert(stem.clone()) { - stem = format!("{natural}_{n}"); - n += 1; - } - self.order.push(ty.clone()); - self.stems.insert(ty.clone(), stem); - } - - /// Whether an encoding of `ty` can hold an object token (directly, or - /// inside a record or rich enum), so its references must be minted by - /// `_wv_seal` right before the encoding is handed over. - pub(crate) fn carries_objects(&self, ty: &Ty) -> bool { - ty.any(&|t| match t { - Ty::Interface(_) => true, - Ty::Record(n) | Ty::RichEnum(n) => self.with_objects.contains(n), - _ => false, - }) - } - - fn stem(&self, ty: &Ty) -> &str { - self.stems - .get(ty) - .unwrap_or_else(|| panic!("no codec registered for {ty}")) - } - - /// The statement appending `expr` (a value of type `ty`) to the buffer - /// writer `w`. `q` is the receiver (`"Kvstore."` or `""`) qualifying the - /// module's codec methods inside class bodies. - pub(crate) fn write(&self, ty: &Ty, w: &str, expr: &str, q: &str) -> String { - match ty.wire() { - WireType::Prim(p) => format!("{w}.write_{}({expr})", p.snake()), - WireType::Enum(_) => format!("{w}.write_i32({expr})"), - WireType::Object(n) => format!("{w}.write_object({expr}, {n})"), - WireType::User(n) => format!("{q}_wv_write_{}({w}, {expr})", wv_stem(n)), - WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("{q}_wv_write_{}({w}, {expr})", self.stem(ty)) - } - } - } - - /// The expression reading one `ty` value from the buffer reader `r`. - pub(crate) fn read(&self, ty: &Ty, r: &str, q: &str) -> String { - match ty.wire() { - WireType::Prim(p) => format!("{r}.read_{}", p.snake()), - WireType::Enum(_) => format!("{r}.read_i32"), - WireType::Object(n) => format!("{r}.read_object({n})"), - WireType::User(n) => format!("{q}_wv_read_{}({r})", wv_stem(n)), - WireType::Optional(_) | WireType::List(_) | WireType::Map(..) => { - format!("{q}_wv_read_{}({r})", self.stem(ty)) - } - } - } - - /// Render the codec pair of every registered composite type. - pub(crate) fn render(&self, w: &mut CodeWriter) { - if self.order.is_empty() { - return; - } - w.blank(); - w.line("# === Value-buffer codecs of the composite types ==="); - for ty in &self.order { - let stem = self.stem(ty); - w.blank(); - w.line("# @api private"); - w.line(format!("# Packs `{ty}` into the value-buffer wire format.")); - w.block( - format!("def self._wv_write_{stem}(w, v)"), - "end", - |w| match ty.wire() { - WireType::Optional(inner) => { - w.line("if v.nil?"); - w.scope(|w| { - w.line("w.write_flag(false)"); - }); - w.line("else"); - w.scope(|w| { - w.line("w.write_flag(true)"); - w.line(self.write(inner, "w", "v", "")); - }); - w.line("end"); - } - WireType::List(elem) => { - w.line("w.write_len(v.length)"); - w.line(format!( - "v.each {{ |e| {} }}", - self.write(elem, "w", "e", "") - )); - } - WireType::Map(k, val) => { - w.line("w.write_len(v.length)"); - w.block("v.each do |k, e|", "end", |w| { - w.line(self.write(k, "w", "k", "")); - w.line(self.write(val, "w", "e", "")); - }); - } - _ => unreachable!("only composites are registered"), - }, - ); - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Unpacks `{ty}` from the value-buffer wire format." - )); - w.block( - format!("def self._wv_read_{stem}(r)"), - "end", - |w| match ty.wire() { - WireType::Optional(inner) => { - w.line(format!("r.read_flag ? {} : nil", self.read(inner, "r", ""))); - } - WireType::List(elem) => { - w.line(format!( - "Array.new(r.read_len) {{ {} }}", - self.read(elem, "r", "") - )); - } - WireType::Map(k, val) => { - w.line(format!( - "Array.new(r.read_len) {{ [{}, {}] }}.to_h", - self.read(k, "r", ""), - self.read(val, "r", "") - )); - } - _ => unreachable!("only composites are registered"), - }, - ); - } - } - - /// Emit `{lead}{class}.new(...)` with one keyword argument per field, - /// each read from `r` in wire order (Ruby evaluates arguments left to - /// right). `positional` comes first when set (an error's message). - pub(crate) fn emit_new( - &self, - w: &mut CodeWriter, - lead: &str, - class: &str, - positional: Option<&str>, - fields: &[FieldBinding], - q: &str, - ) { - let mut args: Vec = positional.map(str::to_string).into_iter().collect(); - args.extend( - fields - .iter() - .map(|f| format!("{}: {}", rb_field_name(&f.name), self.read(&f.ty, "r", q))), - ); - let one_line = format!("{lead}{class}.new({})", args.join(", ")); - match args.as_slice() { - [] => { - w.line(format!("{lead}{class}.new")); - } - _ if w.indent_str().len() + one_line.len() <= MAX_LINE => { - w.line(one_line); - } - many => { - w.line(format!("{lead}{class}.new(")); - w.scope(|w| { - for a in many { - w.line(format!("{a},")); - } - }); - w.line(")"); - } - } - } - - /// Render the private codec pair of one record: module singleton methods - /// `_wv_write_{stem}(w, v)` and `_wv_read_{stem}(r)` serializing the - /// fields in declaration (wire) order. Writing is duck-typed: any object - /// with the field readers packs. - pub(crate) fn render_struct(&self, w: &mut CodeWriter, s: &StructBinding) { - let stem = wv_stem(&s.name); - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Packs {} {} into the value-buffer wire format.", - article(&s.name), - s.name - )); - let writer = if s.fields.is_empty() { - "_w, _v" - } else { - "w, v" - }; - w.block(format!("def self._wv_write_{stem}({writer})"), "end", |w| { - for f in &s.fields { - let field = rb_field_name(&f.name); - w.line(self.write(&f.ty, "w", &format!("v.{field}"), "")); - } - }); - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Unpacks {} {} from the value-buffer wire format.", - article(&s.name), - s.name - )); - let reader = if s.fields.is_empty() { "_r" } else { "r" }; - w.block(format!("def self._wv_read_{stem}({reader})"), "end", |w| { - self.emit_new(w, "", &s.name, None, &s.fields, ""); - }); - } - - /// Render the private codec pair of one rich enum: `_wv_write_{stem}` - /// dispatches on the variant class and writes the `i32` tag followed by - /// the variant's fields; `_wv_read_{stem}` switches on the decoded tag. - pub(crate) fn render_rich_enum(&self, w: &mut CodeWriter, e: &EnumBinding) { - let stem = wv_stem(&e.name); - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Packs {} {} into the value-buffer wire format.", - article(&e.name), - e.name - )); - w.block(format!("def self._wv_write_{stem}(w, v)"), "end", |w| { - w.line("case v"); - for v in &e.variants { - w.line(format!("when {}::{}", e.name, v.name)); - w.scope(|w| { - w.line(format!("w.write_i32({})", v.value)); - for f in &v.fields { - let field = rb_field_name(&f.name); - w.line(self.write(&f.ty, "w", &format!("v.{field}"), "")); - } - }); - } - w.line("else"); - w.scope(|w| { - w.line(format!( - "raise TypeError, \"expected a {}, got #{{v.class}}\"", - e.name - )); - }); - w.line("end"); - }); - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Unpacks {} {} from the value-buffer wire format.", - article(&e.name), - e.name - )); - w.block(format!("def self._wv_read_{stem}(r)"), "end", |w| { - w.line("tag = r.read_i32"); - w.line("case tag"); - for v in &e.variants { - w.line(format!("when {}", v.value)); - w.scope(|w| { - let class = format!("{}::{}", e.name, v.name); - self.emit_new(w, "", &class, None, &v.fields, ""); - }); - } - w.line("else"); - w.scope(|w| { - w.line(format!( - "raise _wv_malformed(\"unknown {} tag #{{tag}}\")", - e.name - )); - }); - w.line("end"); - }); - } -} diff --git a/crates/weaveffi-cli/src/targets/ruby/docs.rs b/crates/weaveffi-cli/src/targets/ruby/docs.rs index a4380ba2..f2ee55fd 100644 --- a/crates/weaveffi-cli/src/targets/ruby/docs.rs +++ b/crates/weaveffi-cli/src/targets/ruby/docs.rs @@ -1,27 +1,93 @@ -//! Doc-comment emission: YARD `@param` and `@return` tags for IDL -//! parameters and returns. +//! YARD documentation: doc and deprecation text in Ruby spelling, and the +//! `@param`, `@return`, `@raise`, and `@deprecated` tags of callables. +use heck::ToSnakeCase; +use weaveffi_model::errors::pascal; + +use crate::codegen::common::{wrap, DocCommentStyle}; +use crate::codegen::docs::{ApiNames, Doc, IdentKind}; use crate::codegen::CodeWriter; -use weaveffi_model::model::ParamBinding; -use weaveffi_model::ty::Ty; +use crate::targets::ruby::types::{rb_const, rb_domain}; + +/// How the bindings spell an identifier an IDL doc names in backticks: +/// callables, parameters, and fields in snake_case, error domains and +/// codes by their class names, and types by their (escaped) constants. +/// Variants and modules keep their IDL spelling. +pub(crate) fn spell(names: &ApiNames, ident: &str) -> Option { + match names.kind(ident)? { + IdentKind::Function + | IdentKind::Member + | IdentKind::CallbackMethod + | IdentKind::Param + | IdentKind::Field => Some(ident.to_snake_case()), + IdentKind::ErrorDomain => Some(rb_domain(ident)), + IdentKind::ErrorCode => Some(pascal(ident)), + IdentKind::Type => Some(rb_const(ident)), + IdentKind::Variant | IdentKind::Module => None, + } +} -use crate::targets::ruby::types::{rb_doc_type, rb_param_name}; +/// One `@param` tag: the Ruby name, its YARD type, and its doc. +pub(crate) struct ParamDoc { + /// The parameter's Ruby name. + pub(crate) name: String, + /// Its YARD type (`Integer`, `Store, nil`). + pub(crate) ty: String, + /// Its IDL doc, if any. + pub(crate) doc: Option, +} + +/// Everything one callable's YARD comment says. +#[derive(Default)] +pub(crate) struct CallableDoc { + /// Notes appended to the doc text (async and iterator behavior). + pub(crate) notes: Vec, + /// The `@param` tags, in order. + pub(crate) params: Vec, + /// The `@return` type, if the callable returns a value. + pub(crate) ret: Option, + /// The `@raise` tags: `(class, when)`. + pub(crate) raises: Vec<(String, String)>, +} -/// Emit one `# @param name [Type] doc` tag per documented parameter, naming -/// each parameter by its emitted Ruby spelling. Continuation lines indent -/// under the tag; blank doc lines become `#`. -pub(crate) fn emit_param_docs(w: &mut CodeWriter, params: &[ParamBinding]) { - for p in params { - let Some(doc) = p.doc.as_deref().map(str::trim).filter(|d| !d.is_empty()) else { - continue; - }; - let mut lines = doc.lines(); - if let Some(first) = lines.next() { - w.line(format!( - "# @param {} [{}] {first}", - rb_param_name(&p.name), - rb_doc_type(&p.ty) - )); +/// Emit `doc`'s text (rewritten through [`spell`]) followed by `extra`'s +/// notes and tags, then `@deprecated` with the rewritten message. +pub(crate) fn emit(w: &mut CodeWriter, names: &ApiNames, doc: &Doc<'_>, extra: &CallableDoc) { + let spelling = |ident: &str| spell(names, ident); + let text = doc.text(spelling); + let has_text = text.is_some(); + w.doc(&text, DocCommentStyle::Hash); + for (i, note) in extra.notes.iter().enumerate() { + if i == 0 && has_text { + w.line("#"); + } + let width = 78usize.saturating_sub(w.indent_str().len() + 2).max(40); + w.doc(&Some(wrap(note, width)), DocCommentStyle::Hash); + } + let has_tags = !extra.params.is_empty() + || extra.ret.is_some() + || !extra.raises.is_empty() + || doc.deprecation(spelling).is_some(); + if has_tags && (has_text || !extra.notes.is_empty()) { + w.line("#"); + } + for p in &extra.params { + let mut lines = p + .doc + .as_deref() + .map(|d| crate::codegen::docs::rewrite(d.trim(), spelling)) + .unwrap_or_default() + .lines() + .map(str::to_string) + .collect::>() + .into_iter(); + match lines.next() { + Some(first) if !first.is_empty() => { + w.line(format!("# @param {} [{}] {first}", p.name, p.ty)); + } + _ => { + w.line(format!("# @param {} [{}]", p.name, p.ty)); + } } for line in lines { if line.is_empty() { @@ -31,11 +97,19 @@ pub(crate) fn emit_param_docs(w: &mut CodeWriter, params: &[ParamBinding]) { } } } + if let Some(ret) = &extra.ret { + w.line(format!("# @return [{ret}]")); + } + for (class, when) in &extra.raises { + w.line(format!("# @raise [{class}] {when}")); + } + if let Some(msg) = doc.deprecation(spelling) { + w.line(format!("# @deprecated {msg}")); + } } -/// Emit the `# @return [Type]` tag of a return value. -pub(crate) fn emit_return_doc(w: &mut CodeWriter, ret: Option<&Ty>) { - if let Some(ty) = ret { - w.line(format!("# @return [{}]", rb_doc_type(ty))); - } +/// Emit a declaration's doc text and `@deprecated` tag (records, enums, +/// interfaces, callback interfaces). +pub(crate) fn emit_plain(w: &mut CodeWriter, names: &ApiNames, doc: &Doc<'_>) { + emit(w, names, doc, &CallableDoc::default()); } diff --git a/crates/weaveffi-cli/src/targets/ruby/entities.rs b/crates/weaveffi-cli/src/targets/ruby/entities.rs index ce294e31..8369281e 100644 --- a/crates/weaveffi-cli/src/targets/ruby/entities.rs +++ b/crates/weaveffi-cli/src/targets/ruby/entities.rs @@ -1,96 +1,46 @@ -//! Entity renderers: plain enums, records, rich enums, interfaces, and the -//! typed error surface of a module's declared error domain. +//! Declaration renderers: error domains, C-style enums, records (`Data` +//! classes), rich enums (a module of `Data` variants), interface wrapper +//! classes, and the registrations that tell the runtime how each record, +//! variant, and error payload is laid out in a value buffer and which C +//! functions clone and release each interface's objects. -use crate::codegen::common::DocCommentStyle; -use crate::codegen::CodeWriter; -use heck::{ToShoutySnakeCase, ToSnakeCase}; +use heck::ToShoutySnakeCase; use weaveffi_model::errors::pascal; -use weaveffi_model::model::{ - EnumBinding, ErrorBinding, FieldBinding, FnBinding, InterfaceBinding, ModuleBinding, - StructBinding, -}; -use weaveffi_model::plan::ErrorStrategy; +use weaveffi_model::model::{EnumBinding, FieldBinding, InterfaceBinding, Model, StructBinding}; -use crate::targets::ruby::calls::{render_attach_function, render_callable, RbScope, ScopeKind}; -use crate::targets::ruby::types::{rb_field_name, rb_str_literal}; +use crate::codegen::common::{wrap, DocCommentStyle}; +use crate::codegen::docs::Doc; +use crate::codegen::errors::ErrorTable; +use crate::codegen::CodeWriter; +use crate::targets::ruby::calls::{render_callable, RbScope, ScopeKind}; +use crate::targets::ruby::docs; +use crate::targets::ruby::types::{ + rb_const, rb_doc_type, rb_error_field_name, rb_field_name, rb_str_literal, rb_wire, +}; use crate::targets::ruby::RbCtx; -/// The snake_case stem of a domain's generated helpers: `KvError` becomes -/// `kv_error`, naming `_wv_kv_error_from`, `_wv_check_kv_error!`, and -/// `_wv_kv_error_payload`. Domain type names are globally unique -/// (validated), so the helpers can't collide. -fn rb_error_stem(eb: &ErrorBinding) -> String { - eb.type_name.to_snake_case() -} - -/// `_wv_{stem}_from`: builds the domain error matching an ABI code. -pub(crate) fn rb_error_factory_name(eb: &ErrorBinding) -> String { - format!("_wv_{}_from", rb_error_stem(eb)) -} +/// The longest line kept on one line before wrapping a member list. +const MAX_LINE: usize = 100; -/// `_wv_check_{stem}!`: raises the typed domain error for a non-zero -/// out-err slot. -fn rb_error_checker_name(eb: &ErrorBinding) -> String { - format!("_wv_check_{}!", rb_error_stem(eb)) -} - -/// `_wv_{stem}_payload`: the code and encoded payload of a domain error a -/// callback implementation raised. -pub(crate) fn rb_error_payload_name(eb: &ErrorBinding) -> String { - format!("_wv_{}_payload", rb_error_stem(eb)) -} - -/// The error-check call a callable's out-err slot goes through, per the -/// function's [`ErrorStrategy`]: the module domain's typed checker for -/// [`ErrorStrategy::Throws`], the runtime's trap (`_wv_check!`, raising -/// `NativeBugError`) for [`ErrorStrategy::Trap`]. -pub(crate) fn rb_checker_name(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => rb_error_checker_name(eb), - _ => "_wv_check!".to_string(), - } -} - -/// Emit `attr_reader` lines for `fields`, each with its doc comment, -/// separated by blank lines. -fn emit_readers(w: &mut CodeWriter, fields: &[FieldBinding]) { - for (idx, f) in fields.iter().enumerate() { - if idx > 0 { - w.blank(); - } - w.doc(&f.doc, DocCommentStyle::Hash); - w.line(format!("attr_reader :{}", rb_field_name(&f.name))); - } -} - -/// Render one module's declared error domain: a domain class subclassing -/// the root `Error`, one nested subclass per code carrying its stable -/// `CODE` constant, default message, and any declared payload fields as -/// attributes, then the private helpers: the factory and checker throwing -/// wrappers route their out-err slots through, and (when a callback method -/// may raise the domain, `payload`) the encoder reporting a raised domain -/// error back to the producer. Nesting the code classes keeps -/// `KvError::KeyNotFound` spellable and unambiguous even across domains. -/// -/// Domain codes are validated positive-only; the negative range is reserved -/// for the runtime. The factory therefore maps only declared codes onto -/// typed classes and lets everything else fall through to `_wv_error` (the -/// root `Error`, or `Cancelled`). -pub(crate) fn render_error( - w: &mut CodeWriter, - ctx: &RbCtx, - module: &ModuleBinding, - eb: &ErrorBinding, - payload: bool, -) { - let domain = &eb.type_name; +/// Render one error domain: a class under the library's `Error` with one +/// nested class per code, each carrying its `CODE`, its documented +/// `MESSAGE`, and readers for its payload fields (which the runtime's +/// `Error#initialize` takes as required keywords). A code these bindings +/// don't know (the library is newer) raises the domain class itself, with +/// the code and message. +pub(crate) fn render_error(w: &mut CodeWriter, table: &ErrorTable<'_>) { + let domain = rb_const(&table.type_name); w.blank(); - w.line(format!( - "# Base error for the `{}` module's error domain.", - module.dot_path - )); + let about = format!( + "The `{}` error domain of the `{}` module. A code these bindings don't \ + know (from a newer library) raises this class itself, with its code \ + and message.", + table.domain.name, table.module.dot_path + ); + w.doc(&Some(wrap(&about, 76)), DocCommentStyle::Hash); w.block(format!("class {domain} < Error"), "end", |w| { - for (idx, c) in eb.codes.iter().enumerate() { + for (idx, row) in table.codes.iter().enumerate() { + let c = row.code; if idx > 0 { w.blank(); } @@ -101,130 +51,25 @@ pub(crate) fn render_error( "end", |w| { w.line(format!("CODE = {}", c.value)); - if !c.fields.is_empty() { + w.line(format!("MESSAGE = '{}'", rb_str_literal(&c.message))); + for f in &c.fields { w.blank(); - emit_readers(w, &c.fields); + w.doc(&f.doc, DocCommentStyle::Hash); + w.line(format!("# @return [{}]", rb_doc_type(&f.ty))); + w.line(format!("attr_reader :{}", rb_error_field_name(&f.name))); } - w.blank(); - let kw: String = c - .fields - .iter() - .map(|f| format!(", {}: nil", rb_field_name(&f.name))) - .collect(); - w.block(format!("def initialize(message = nil{kw})"), "end", |w| { - for f in &c.fields { - let field = rb_field_name(&f.name); - w.line(format!("@{field} = {field}")); - } - w.line(format!( - "super(CODE, message || '{}')", - rb_str_literal(&c.message) - )); - }); }, ); } }); - - w.blank(); - w.line("# @api private"); - w.line(format!( - "# The {domain} for a domain `code`, with its payload fields decoded;" - )); - w.line("# the root Error (or Cancelled) for any other code."); - w.block( - format!( - "def self.{}(code, message, payload = nil)", - rb_error_factory_name(eb) - ), - "end", - |w| { - w.line("message = nil if message.empty?"); - w.line("r = WvBufferReader.new(payload)"); - w.line("error ="); - w.scope(|w| { - w.line("case code"); - for c in &eb.codes { - w.line(format!("when {}", c.value)); - w.scope(|w| { - let class = format!("{domain}::{}", pascal(&c.name)); - ctx.codecs - .emit_new(w, "", &class, Some("message"), &c.fields, ""); - }); - } - w.line("else"); - w.scope(|w| { - w.line("return _wv_error(code, message.to_s)"); - }); - w.line("end"); - }); - w.line("r.expect_end!"); - w.line("error"); - }, - ); - - w.blank(); - w.line("# @api private"); - w.line(format!("# Raises the {domain} for a non-zero error slot.")); - w.block( - format!("def self.{}(err)", rb_error_checker_name(eb)), - "end", - |w| { - w.line("taken = _wv_take_error(err)"); - w.line(format!( - "raise {}(*taken) unless taken.nil?", - rb_error_factory_name(eb) - )); - }, - ); - - if payload { - w.blank(); - w.line("# @api private"); - w.line(format!( - "# The code and value-buffer payload of a {domain} a callback" - )); - w.line("# implementation raised, or nil for an error of no declared code."); - w.block( - format!("def self.{}(error)", rb_error_payload_name(eb)), - "end", - |w| { - w.line("w = WvBufferWriter.new"); - w.line("code ="); - w.scope(|w| { - w.line("case error"); - for c in &eb.codes { - let class = format!("{domain}::{}", pascal(&c.name)); - if c.fields.is_empty() { - w.line(format!("when {class} then {}", c.value)); - continue; - } - w.line(format!("when {class}")); - w.scope(|w| { - for f in &c.fields { - let field = rb_field_name(&f.name); - w.line(ctx.codecs.write(&f.ty, "w", &format!("error.{field}"), "")); - } - w.line(c.value.to_string()); - }); - } - w.line("end"); - }); - w.line("code && [code, w.bytes]"); - }, - ); - } } -/// Render one plain C-style enum as a module of integer constants, one +/// Render one C-style enum as a module of integer constants, one /// `SHOUTY_SNAKE` constant per variant. -pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { +pub(crate) fn render_enum(w: &mut CodeWriter, ctx: &RbCtx, e: &EnumBinding) { w.blank(); - w.doc(&e.doc, DocCommentStyle::Hash); - if let Some(msg) = &e.deprecated { - w.line(format!("# @deprecated {msg}")); - } - w.block(format!("module {}", e.name), "end", |w| { + docs::emit_plain(w, &ctx.names, &Doc::new(&e.doc, &e.deprecated)); + w.block(format!("module {}", rb_const(&e.name)), "end", |w| { for v in &e.variants { w.doc(&v.doc, DocCommentStyle::Hash); w.line(format!("{} = {}", v.name.to_shouty_snake_case(), v.value)); @@ -232,200 +77,234 @@ pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { }); } -/// Emit a value class body: documented `attr_reader`s, a keyword-argument -/// `initialize`, and structural `==` over `class`'s fields. -fn emit_value_class(w: &mut CodeWriter, class: &str, fields: &[FieldBinding]) { - if !fields.is_empty() { - emit_readers(w, fields); - w.blank(); - let kw: Vec = fields - .iter() - .map(|f| format!("{}:", rb_field_name(&f.name))) - .collect(); - let one_line = format!("def initialize({})", kw.join(", ")); - if w.indent_str().len() + one_line.len() <= 100 { - w.line(one_line); - } else { - w.line("def initialize("); - w.scope(|w| { - for (i, k) in kw.iter().enumerate() { - let sep = if i + 1 == kw.len() { "" } else { "," }; - w.line(format!("{k}{sep}")); +/// The YARD `@!attribute` tags documenting a `Data` class's members. +fn emit_attributes(w: &mut CodeWriter, fields: &[FieldBinding]) { + for f in fields { + w.line(format!("# @!attribute [r] {}", rb_field_name(&f.name))); + let doc = f.doc.as_deref().map(str::trim).filter(|d| !d.is_empty()); + match doc { + Some(doc) => { + let mut lines = doc.lines(); + w.line(format!( + "# @return [{}] {}", + rb_doc_type(&f.ty), + lines.next().unwrap_or_default() + )); + for line in lines { + w.line(format!("# {line}").trim_end()); } - }); - w.line(")"); - } - w.scope(|w| { - for f in fields { - let field = rb_field_name(&f.name); - w.line(format!("@{field} = {field}")); } - }); - w.line("end"); - w.blank(); - } - w.line("# Structural equality over every field."); - w.block("def ==(other)", "end", |w| { - let mut terms = vec![format!("other.is_a?({class})")]; - terms.extend(fields.iter().map(|f| { - let field = rb_field_name(&f.name); - format!("{field} == other.{field}") - })); - match terms.as_slice() { - [one] => { - w.line(one.clone()); - } - [first, rest @ ..] => { - w.line(format!("{first} &&")); - w.scope(|w| { - for (i, t) in rest.iter().enumerate() { - if i + 1 == rest.len() { - w.line(t.clone()); - } else { - w.line(format!("{t} &&")); - } - } - }); + None => { + w.line(format!("# @return [{}]", rb_doc_type(&f.ty))); } - [] => unreachable!("the class test is always present"), } - }); + } } -/// Render one record as a plain Ruby value class: one documented -/// `attr_reader` per field, a keyword-argument `initialize`, and structural -/// `==`. Records are value types: they own no C symbols; they cross the ABI -/// packed into value buffers by the module's `_wv_write_*`/`_wv_read_*` -/// codec helpers. -pub(crate) fn render_struct_class(w: &mut CodeWriter, s: &StructBinding) { - w.blank(); - w.doc(&s.doc, DocCommentStyle::Hash); - if let Some(msg) = &s.deprecated { - w.line(format!("# @deprecated {msg}")); +/// `Data.define(:a, :b)`, wrapped one member per line when long. `lead` +/// precedes it on the first line (`Item = `, `class Circle < `). +fn emit_data_define(w: &mut CodeWriter, lead: &str, trail: &str, fields: &[FieldBinding]) { + let members: Vec = fields + .iter() + .map(|f| format!(":{}", rb_field_name(&f.name))) + .collect(); + let args = if members.is_empty() { + String::new() + } else { + format!("({})", members.join(", ")) + }; + let one_line = format!("{lead}Data.define{args}{trail}"); + if w.indent_str().len() + one_line.len() <= MAX_LINE { + w.line(one_line); + return; } - w.block(format!("class {}", s.name), "end", |w| { - emit_value_class(w, &s.name, &s.fields); + w.line(format!("{lead}Data.define(")); + w.scope(|w| { + for m in &members { + w.line(format!("{m},")); + } }); + w.line(format!("){trail}")); } -/// Render one rich (algebraic) enum as a tagged class hierarchy: a base -/// class exposing `tag`, plus one nested value class per variant carrying -/// that variant's fields. Rich enums own no C symbols; they cross the ABI -/// packed into value buffers as an `i32` tag followed by the active -/// variant's fields in declaration order. -pub(crate) fn render_rich_enum_class(w: &mut CodeWriter, e: &EnumBinding) { +/// Render one record as a `Data` class: immutable, constructed with +/// keywords (or positionally), with value equality and hashing, `with`, +/// `to_h`, and pattern matching. +pub(crate) fn render_struct(w: &mut CodeWriter, ctx: &RbCtx, s: &StructBinding) { w.blank(); - w.doc(&e.doc, DocCommentStyle::Hash); - if let Some(msg) = &e.deprecated { - w.line(format!("# @deprecated {msg}")); + let doc = Doc::new(&s.doc, &s.deprecated); + docs::emit_plain(w, &ctx.names, &doc); + if !s.fields.is_empty() && (s.doc.is_some() || s.deprecated.is_some()) { + w.line("#"); } - w.block(format!("class {}", e.name), "end", |w| { - w.line("# The active variant's integer tag."); + emit_attributes(w, &s.fields); + emit_data_define(w, &format!("{} = ", rb_const(&s.name)), "", &s.fields); +} + +/// Render one rich enum as a module of `Data` variant classes. Each variant +/// includes the module (so `shape.is_a?(Shape)` holds) and carries its wire +/// `TAG`, which `#tag` returns. +pub(crate) fn render_rich_enum(w: &mut CodeWriter, ctx: &RbCtx, e: &EnumBinding) { + let name = rb_const(&e.name); + w.blank(); + docs::emit_plain(w, &ctx.names, &Doc::new(&e.doc, &e.deprecated)); + w.block(format!("module {name}"), "end", |w| { + w.line("# The active variant's wire tag."); + w.line("# @return [Integer]"); w.block("def tag", "end", |w| { w.line("self.class::TAG"); }); for v in &e.variants { w.blank(); w.doc(&v.doc, DocCommentStyle::Hash); - w.block(format!("class {} < {}", v.name, e.name), "end", |w| { - w.line(format!("TAG = {}", v.value)); + if !v.fields.is_empty() && v.doc.is_some() { + w.line("#"); + } + emit_attributes(w, &v.fields); + emit_data_define(w, &format!("class {} < ", rb_const(&v.name)), "", &v.fields); + w.scope(|w| { + w.line(format!("include {name}")); w.blank(); - emit_value_class(w, &v.name, &v.fields); + w.line(format!("TAG = {}", v.value)); }); + w.line("end"); } }); } -/// Declare the FFI bindings for one interface: the clone and destroy -/// lifecycle symbols (which keep the GVL; see [`render_attach_function`]) -/// plus every constructor, method, and static. -pub(crate) fn render_interface_ffi(w: &mut CodeWriter, i: &InterfaceBinding) { - w.line(format!( - "attach_function :{}, [:pointer], :pointer", - i.clone_symbol - )); - w.line(format!( - "attach_function :{}, [:pointer], :void", - i.destroy_symbol - )); - for f in i - .constructors - .iter() - .chain(i.methods.iter()) - .chain(i.statics.iter()) - { - render_attach_function(w, f); - } -} - -/// Render one interface as a reference-counted wrapper class on the -/// runtime's `WvObject` base (which supplies `handle`, `close`, `closed?`, -/// `dup`/`clone`, and the call pinning). A `{Name}Ptr < FFI::AutoPointer` -/// subclass owns exactly one strong reference and releases it through the -/// interface's `_destroy` symbol, either from `close` or, as a backstop, -/// from the GC finalizer. A constructor named `new` becomes `initialize`; -/// every other constructor becomes a class-method factory; methods borrow -/// the wrapper's pointer as the leading C argument; statics are class -/// methods. `_from_ptr` adopts a reference the producer handed over (a -/// return, an async result, an iterator element, a buffer token, a callback -/// argument) without re-running `initialize`. -pub(crate) fn render_interface_class( - w: &mut CodeWriter, - ctx: &RbCtx, - error: Option<&ErrorBinding>, - i: &InterfaceBinding, -) { - let ptr_class = format!("{}Ptr", i.name); - let module = ctx.module; - w.blank(); - w.line("# @api private"); - w.line(format!( - "# Owns one strong reference to a {}; releases it exactly once.", - i.name - )); - w.block( - format!("class {ptr_class} < FFI::AutoPointer"), - "end", - |w| { - w.block("def self.release(ptr)", "end", |w| { - w.line(format!("{module}.{}(ptr)", i.destroy_symbol)); - }); - }, - ); +/// Render one interface as a wrapper class on the runtime's `Handle` +/// (which supplies `close`, `closed?`, `==`/`hash` by native object, +/// `dup`/`clone`, `inspect`, and the GC backstop). A constructor named +/// `new` becomes `initialize`; every other constructor is a class-method +/// factory, and an interface without a `new` hides it; methods pin the +/// wrapper's pointer for the call; statics are class methods. +pub(crate) fn render_interface(w: &mut CodeWriter, ctx: &RbCtx, i: &InterfaceBinding) { + let class = rb_const(&i.name); w.blank(); - w.doc(&i.doc, DocCommentStyle::Hash); - if let Some(msg) = &i.deprecated { - w.line(format!("# @deprecated {msg}")); - } - w.block(format!("class {} < WvObject", i.name), "end", |w| { - w.line(format!("WV_PTR = {ptr_class}")); + docs::emit_plain(w, &ctx.names, &Doc::new(&i.doc, &i.deprecated)); + w.block(format!("class {class} < Bridge::Handle"), "end", |w| { + let mut first = true; if !i.constructors.iter().any(|c| c.name == "new") { w.line("private_class_method :new"); + first = false; } - w.blank(); - w.line("# @api private"); - w.block("def self._wv_clone(ptr)", "end", |w| { - w.line(format!("{module}.{}(ptr)", i.clone_symbol)); - }); - // Members render at class depth through the shared callable paths, - // so sync, async, and iterator members reuse the free-function - // marshalling. + let mut gap = |w: &mut CodeWriter| { + if !std::mem::take(&mut first) { + w.blank(); + } + }; for c in &i.constructors { + gap(w); let kind = if c.name == "new" { ScopeKind::Init } else { ScopeKind::Factory }; - render_callable(w, ctx, error, c, &RbScope::member(kind, module, &i.name)); + let scope = RbScope { + kind, + class: Some(&class), + }; + render_callable(w, ctx, c, &scope); } - for f in i.methods.iter().chain(i.statics.iter()) { - let kind = if f.has_self { + for f in i.methods.iter().chain(&i.statics) { + gap(w); + let kind = if f.has_self() { ScopeKind::Method } else { ScopeKind::Static }; - let scope = RbScope::member(kind, module, &i.name); - render_callable(w, ctx, error, f, &scope); + let scope = RbScope { + kind, + class: Some(&class), + }; + render_callable(w, ctx, f, &scope); } }); } + +/// `Bridge.record(Class, field: type, ...)`, wrapped one field per line +/// when long. +fn emit_layout(w: &mut CodeWriter, class: &str, fields: &[(String, String)]) { + let parts: Vec = std::iter::once(class.to_string()) + .chain(fields.iter().map(|(n, t)| format!("{n}: {t}"))) + .collect(); + let one_line = format!("Bridge.record({})", parts.join(", ")); + if w.indent_str().len() + one_line.len() <= MAX_LINE { + w.line(one_line); + return; + } + w.line("Bridge.record("); + w.scope(|w| { + for p in &parts { + w.line(format!("{p},")); + } + }); + w.line(")"); +} + +/// The private registrations closing the bindings: each interface's +/// clone and destroy functions, then the wire layout of every record, +/// rich-enum variant (and the variants of each rich enum), and error code +/// with a payload. +pub(crate) fn render_layouts(w: &mut CodeWriter, model: &Model, tables: &[ErrorTable<'_>]) { + w.blank(); + w.line("# How the library clones and releases each interface's objects, and how"); + w.line("# each record, variant, and error payload is laid out in a value buffer."); + for m in &model.modules { + for i in &m.interfaces { + w.line(format!( + "Bridge.interface({}, :{}, :{})", + rb_const(&i.name), + i.clone_symbol, + i.destroy_symbol + )); + } + } + let record_fields = |fields: &[FieldBinding]| -> Vec<(String, String)> { + fields + .iter() + .map(|f| (rb_field_name(&f.name), rb_wire(&f.ty))) + .collect() + }; + for m in &model.modules { + for s in &m.structs { + emit_layout(w, &rb_const(&s.name), &record_fields(&s.fields)); + } + for e in m.enums.iter().filter(|e| e.is_rich()) { + let name = rb_const(&e.name); + let variants: Vec = e + .variants + .iter() + .map(|v| format!("{name}::{}", rb_const(&v.name))) + .collect(); + for (v, class) in e.variants.iter().zip(&variants) { + emit_layout(w, class, &record_fields(&v.fields)); + } + let one_line = format!("Bridge.union({name}, {})", variants.join(", ")); + if w.indent_str().len() + one_line.len() <= MAX_LINE { + w.line(one_line); + } else { + w.line("Bridge.union("); + w.scope(|w| { + w.line(format!("{name},")); + for v in &variants { + w.line(format!("{v},")); + } + }); + w.line(")"); + } + } + } + for table in tables { + let domain = rb_const(&table.type_name); + for row in table.codes.iter().filter(|r| !r.code.fields.is_empty()) { + let fields: Vec<(String, String)> = row + .code + .fields + .iter() + .map(|f| (rb_error_field_name(&f.name), rb_wire(&f.ty))) + .collect(); + emit_layout(w, &format!("{domain}::{}", pascal(&row.code.name)), &fields); + } + } +} diff --git a/crates/weaveffi-cli/src/targets/ruby/mod.rs b/crates/weaveffi-cli/src/targets/ruby/mod.rs index 521daad5..202d96b5 100644 --- a/crates/weaveffi-cli/src/targets/ruby/mod.rs +++ b/crates/weaveffi-cli/src/targets/ruby/mod.rs @@ -1,24 +1,25 @@ //! Ruby (ffi gem) binding generator. //! -//! Emits a gem that binds the C ABI (revision 4) with the `ffi` gem: -//! `lib/{prefix}.rb` (the generated bindings, required as `{prefix}`), +//! Emits a pure-Ruby gem that binds the C ABI (revision 5) with the `ffi` +//! gem: `lib/{prefix}.rb` (the generated bindings, required as `{prefix}`), //! `lib/{prefix}/runtime.rb` (the fixed runtime from `runtime/runtime.rb`), -//! a `{name}.gemspec`, and a README. Everything lives in one Ruby module, -//! `PascalCase(name)` unless configured. Loading checks the ABI revision and -//! every top-level module's contract table. Interfaces become -//! reference-counted wrapper classes (`close` plus a GC finalizer backstop, -//! `dup`/`clone` for a second reference), records and rich enums become -//! value classes packed into value buffers (one codec pair per record, rich -//! enum, and composite type), async functions block on a queue fed by a -//! module-level completion trampoline (cancellable ones take a `cancel:` -//! token), iterators are lazy `Enumerator`s, and callback interfaces are -//! duck-typed modules backed by one static vtable of pinned `FFI::Function` -//! trampolines per interface. Every call except the trivial runtime helpers -//! releases the GVL. +//! a `{name}.gemspec` (Ruby 3.2 or newer), and a README. Every IDL module's +//! declarations live in one Ruby module, `PascalCase(name)` unless +//! configured (type and function names are global, so nothing collides). +//! +//! The public surface is only the idiomatic API: error classes, enum +//! constants, records as `Data` classes, rich enums as modules of `Data` +//! variants, interface wrapper classes, callback-interface modules, and +//! module functions. The raw C functions live in the private `Native` +//! module and the shared marshalling in the private `Bridge` module, both +//! in the runtime: a wrapper only converts its arguments and hands the C +//! call to a `Bridge` helper as a block, and value buffers are encoded by a +//! generic codec from per-type layouts registered at the end of the file. +//! Loading checks the ABI revision and every top-level module's contract +//! table and raises `{Module}::LoadError` on a mismatch. mod callbacks; mod calls; -mod codec; mod docs; mod entities; mod package; @@ -28,22 +29,20 @@ mod types; #[cfg(test)] mod tests; -use crate::backend::{LanguageBackend, OutputFile}; -use crate::codegen::CodeWriter; +use crate::codegen::docs::ApiNames; +use crate::codegen::{contract, errors, CodeWriter, OutputFile}; use crate::package::{Artifact, ArtifactKind, GemSpec, PackageContext, PackagedFile}; +use crate::targets::Target; use crate::utils::{render_prelude, render_trailer, CommentStyle}; -use camino::Utf8Path; +use camino::Utf8PathBuf; +use miette::Result; use serde::{Deserialize, Serialize}; -use weaveffi_model::contract; -use weaveffi_model::model::{contract_symbol, ErrorBinding, Model}; +use weaveffi_model::model::Model; -use crate::targets::ruby::calls::{ - render_async_trampoline, render_attach_function, render_callable, RbScope, -}; -use crate::targets::ruby::codec::Codecs; +use crate::targets::ruby::callbacks::{render_callback_module, render_vtable}; +use crate::targets::ruby::calls::{render_attach, render_callable, RbScope, ScopeKind}; use crate::targets::ruby::entities::{ - render_enum, render_error, render_interface_class, render_interface_ffi, - render_rich_enum_class, render_struct_class, + render_enum, render_error, render_interface, render_layouts, render_rich_enum, render_struct, }; use crate::targets::ruby::package::{ gem_authors, render_gemspec, render_packaged_readme, render_readme, GemNames, FFI_REQUIREMENT, @@ -83,18 +82,34 @@ impl RubyConfig { /// Ruby backend: emits an `ffi`-gem package binding the C ABI exposed by the /// underlying cdylib. -pub struct RubyGenerator; +pub struct RubyGenerator { + config: RubyConfig, +} -impl LanguageBackend for RubyGenerator { - type Config = RubyConfig; +impl From for RubyGenerator { + fn from(config: RubyConfig) -> Self { + Self { config } + } +} +impl Target for RubyGenerator { fn name(&self) -> &'static str { "ruby" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { + /// `lib/native/`, where the runtime loads a bundled library first. + fn dev_bundle_dir(&self, _model: &Model) -> Option { + Some(Utf8PathBuf::from("lib/native")) + } + + fn fixed_files(&self) -> &'static [&'static str] { + &["*.gemspec", "README.md", "runtime.rb"] + } + + fn render(&self, model: &Model) -> Vec { + let config = &self.config; let names = config.names(model); - let dir = out_dir.join("ruby"); + let dir = Utf8PathBuf::new(); let lib_dir = dir.join("lib"); let lib_file = format!("{}.rb", names.require()); vec![ @@ -115,12 +130,8 @@ impl LanguageBackend for RubyGenerator { /// under `lib/native/` where the runtime loads it first. Platforms /// RubyGems has no platform string for (Android, iOS, `wasm32`) are /// skipped. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; let names = config.names(model); let id = names.identity; let lib_file = format!("{}.rb", names.require()); @@ -161,48 +172,28 @@ impl LanguageBackend for RubyGenerator { files, }); } - Some(artifacts) + Ok(artifacts) } } -/// What every renderer of one bindings file shares: the model, the Ruby -/// module name, and the composite codecs the API uses. +/// What every renderer of one bindings file shares. pub(crate) struct RbCtx<'a> { - /// The validated model being rendered. - pub(crate) model: &'a Model, - /// The top-level Ruby module name, which qualifies module singleton - /// calls from inside class bodies. + /// The top-level Ruby module name, which deprecation warnings name. pub(crate) module: &'a str, - /// The codec pairs of the API's composite types. - pub(crate) codecs: Codecs, + /// Every identifier the API declares, for rewriting doc text. + pub(crate) names: ApiNames, } -impl RbCtx<'_> { - /// Whether a callback method declared `throws` can raise `eb`, so the - /// bindings need its payload encoder. - fn raised_by_callbacks(&self, eb: &ErrorBinding) -> bool { - self.model.modules.iter().any(|m| { - m.callback_interfaces - .iter() - .any(|cb| cb.methods.iter().any(|cm| cm.throws)) - && self - .model - .error_domain(m) - .is_some_and(|d| d.type_name == eb.type_name) - }) - } -} - -/// Render `lib/{prefix}.rb`: the runtime require, the contract check, then -/// each module's typed error surface, entities, codecs, FFI attachments, -/// callback interfaces, interface classes, and free functions, and finally -/// the composite codecs. +/// Render `lib/{prefix}.rb`: the contract check, the `Native` attachments, +/// each module's declarations (errors, enums, records, callback +/// interfaces, interfaces, functions), and finally the private +/// registrations (interface lifecycles, wire layouts, vtables). fn render_bindings(model: &Model, names: &GemNames, lib_file: &str) -> String { let ctx = RbCtx { - model, module: &names.module, - codecs: Codecs::collect(model), + names: ApiNames::new(model), }; + let tables = errors::tables(model, "Error"); let mut w = CodeWriter::two_space(); w.raw(render_prelude(CommentStyle::Hash)); w.line("# frozen_string_literal: true"); @@ -212,89 +203,72 @@ fn render_bindings(model: &Model, names: &GemNames, lib_file: &str) -> String { w.line(format!("# Ruby bindings for {}.", names.identity.name)); w.block(format!("module {}", names.module), "end", |w| { render_contract(w, model); + render_native(w, model); for m in &model.modules { - let error = model.error_domain(m); w.blank(); w.line(format!("# === Module: {} ===", m.dot_path)); - // The typed error surface comes first so the domain class exists - // before any wrapper or trampoline references it. - if let Some(eb) = m.errors.as_ref() { - render_error(w, &ctx, m, eb, ctx.raised_by_callbacks(eb)); + for table in tables.iter().filter(|t| t.module.index == m.index) { + render_error(w, table); } for e in &m.enums { - // A plain C-style enum is a module of integer constants; a - // rich (algebraic) enum is a tagged value-class hierarchy. if e.is_rich() { - render_rich_enum_class(w, e); + render_rich_enum(w, &ctx, e); } else { - render_enum(w, e); + render_enum(w, &ctx, e); } } for s in &m.structs { - render_struct_class(w, s); - } - for s in &m.structs { - ctx.codecs.render_struct(w, s); - } - for e in m.enums.iter().filter(|e| e.is_rich()) { - ctx.codecs.render_rich_enum(w, e); + render_struct(w, &ctx, s); } - if !m.interfaces.is_empty() || !m.functions.is_empty() { - w.blank(); + for cb in &m.callback_interfaces { + render_callback_module(w, &ctx, cb); } for i in &m.interfaces { - render_interface_ffi(w, i); + render_interface(w, &ctx, i); } for f in &m.functions { - render_attach_function(w, f); - } - let members = m - .interfaces - .iter() - .flat_map(|i| i.constructors.iter().chain(&i.methods).chain(&i.statics)); - for f in members.chain(&m.functions) { - render_async_trampoline(w, &ctx, error, f); + w.blank(); + let scope = RbScope { + kind: ScopeKind::Free, + class: None, + }; + render_callable(w, &ctx, f, &scope); } - // Callback interfaces precede interfaces and functions because - // their static vtables are what members and free functions pass. + } + w.blank(); + w.line("# === Private registrations ==="); + render_layouts(w, model, &tables); + for m in &model.modules { for cb in &m.callback_interfaces { - callbacks::render_callback_interface(w, &ctx, error, cb); - } - for i in &m.interfaces { - render_interface_class(w, &ctx, error, i); - } - for f in &m.functions { - render_callable(w, &ctx, error, f, &RbScope::free(ctx.module)); + render_vtable(w, cb); } } - ctx.codecs.render(w); }); w.blank(); w.raw(render_trailer(CommentStyle::Hash, lib_file)); w.finish() } -/// The load-time contract check: for every top-level module, each -/// declaration's `[id, hash, path]` from [`contract::entries`], which the -/// runtime's `_wv_check_contract!` looks up in the library's own table. +/// The load-time contract check: for every top-level module, each row of +/// its contract table (`[id, hash, path]`, with the canonical signature as +/// a comment), which `Bridge.check_contract!` looks up in the library's +/// own table. fn render_contract(w: &mut CodeWriter, model: &Model) { w.line("# The declarations these bindings were generated with, per top-level"); - w.line("# module, as [id, hash, path]. Loading fails, naming the path, unless"); - w.line("# the library's contract table holds each one with an equal hash."); - w.line("_wv_check_contract!("); + w.line("# module, as [id, hash, path]. Loading raises LoadError, naming the path,"); + w.line("# unless the library's contract table holds each one with an equal hash."); + w.line("Bridge.check_contract!("); w.scope(|w| { - for root in model.roots() { - w.line(format!( - "{}: [", - contract_symbol(model.prefix(), &root.name) - )); + for table in contract::tables(model) { + w.line(format!("{}: [", table.symbol)); w.scope(|w| { - for e in contract::entries(model, root) { + for row in &table.rows { + w.line(format!("# {}", row.signature)); w.line(format!( - "[0x{:016x}, 0x{:016x}, '{}'],", - e.id, - e.hash, - rb_str_literal(&e.path) + "[{}, {}, '{}'],", + contract::hex(row.id), + contract::hex(row.hash), + rb_str_literal(&row.path) )); } }); @@ -303,3 +277,34 @@ fn render_contract(w: &mut CodeWriter, model: &Model) { }); w.line(")"); } + +/// The private `Native` module's attachments: every C function the +/// wrappers call, module by module. +fn render_native(w: &mut CodeWriter, model: &Model) { + w.blank(); + w.line("# The C functions the wrappers below call (private)."); + w.block("module Native", "end", |w| { + for (idx, m) in model.modules.iter().enumerate() { + if idx > 0 { + w.blank(); + } + w.line(format!("# {}", m.dot_path)); + for i in &m.interfaces { + w.line(format!( + "attach_function :{}, [:pointer], :pointer", + i.clone_symbol + )); + w.line(format!( + "attach_function :{}, [:pointer], :void", + i.destroy_symbol + )); + for f in i.members() { + render_attach(w, f); + } + } + for f in &m.functions { + render_attach(w, f); + } + } + }); +} diff --git a/crates/weaveffi-cli/src/targets/ruby/package.rs b/crates/weaveffi-cli/src/targets/ruby/package.rs index 03230cfb..29086a19 100644 --- a/crates/weaveffi-cli/src/targets/ruby/package.rs +++ b/crates/weaveffi-cli/src/targets/ruby/package.rs @@ -34,10 +34,10 @@ impl GemNames<'_> { } /// The `required_ruby_version` every gem declares. -pub(crate) const REQUIRED_RUBY_VERSION: &str = ">= 2.7"; +pub(crate) const REQUIRED_RUBY_VERSION: &str = ">= 3.2"; /// The `ffi` gem requirement every gem declares. -pub(crate) const FFI_REQUIREMENT: &str = "~> 1.15"; +pub(crate) const FFI_REQUIREMENT: &str = "~> 1.16"; /// The gem's authors. RubyGems requires a non-empty list, so an identity /// without authors falls back to the gem name. @@ -118,7 +118,7 @@ Ruby bindings for `{gem}` using the [ffi](https://github.com/ffi/ffi) gem. ## Prerequisites -- Ruby 2.7 or newer +- Ruby 3.2 or newer - The native library (`{linux}`, `{macos}`, or `{windows}`) on the library search path, or its path in the `{env}` environment variable. diff --git a/crates/weaveffi-cli/src/targets/ruby/runtime/runtime.rb b/crates/weaveffi-cli/src/targets/ruby/runtime/runtime.rb index 4ef79139..0caf0a48 100644 --- a/crates/weaveffi-cli/src/targets/ruby/runtime/runtime.rb +++ b/crates/weaveffi-cli/src/targets/ruby/runtime/runtime.rb @@ -3,73 +3,63 @@ require 'ffi' require 'monitor' -# The fixed runtime every generated {{MODULE}} binding builds on: the native -# library loader, the load-time ABI and contract checks, the error classes, -# the value-buffer reader and writer, cancel tokens, the handle table behind -# callback interfaces and async completions, and the object wrapper base. +# The fixed runtime every generated {{MODULE}} binding builds on. Its public +# part is the error hierarchy, CancelToken, and ABI_VERSION; everything else +# lives in two private modules: Native (the C library, attached through the +# ffi gem) and Bridge (the marshalling the generated wrappers share). module {{MODULE}} - extend FFI::Library - - # An explicit path in {{LIBRARY_ENV}} wins. Otherwise a library bundled - # under lib/native/ (a platform gem) is preferred over the system search - # path. - _wv_override = ENV['{{LIBRARY_ENV}}'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - _wv_name = - case FFI::Platform::OS - when /darwin/ then '{{LIB_MACOS}}' - when /mswin|mingw|windows/ then '{{LIB_WINDOWS}}' - else '{{LIB_LINUX}}' - end - _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) - ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) - end - - # The C ABI revision these bindings speak. Checked before anything else is - # attached, so a mismatched library fails at require time instead of - # misreading an error struct or a value buffer later. + # The C ABI revision these bindings speak. Loading fails unless the + # library reports the same revision. ABI_VERSION = {{ABI_VERSION}} - begin - attach_function :{{PREFIX}}_abi_version, [], :uint32 - rescue FFI::NotFoundError - raise LoadError, "the loaded {{LIBRARY}} library does not export {{PREFIX}}_abi_version " \ - "(these bindings expect ABI revision #{ABI_VERSION})" - end - _wv_abi = {{PREFIX}}_abi_version - unless _wv_abi == ABI_VERSION - raise LoadError, "{{LIBRARY}} ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ - "but the loaded library reports revision #{_wv_abi}" - end - # The `{{PREFIX}}_error` out-parameter every call reports through. - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end + # Raised by `require` when the native library can't be loaded, reports + # another ABI revision, or lacks a declaration these bindings were + # generated with (or has one whose signature changed). It's a LoadError, + # so a `rescue LoadError` around the require catches it too. + class LoadError < ::LoadError; end - # Codes reserved by the runtime. Positive codes belong to a module's error - # domain; these negative codes report runtime failures. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - # The call was cancelled through its CancelToken. - CANCELLED_ERROR_CODE = -5 - - # The base class of every error the library raises. + # The base class of every error the library raises. A call that declares + # `throws` raises its domain's error classes (subclasses of this one) for + # domain codes, and this class itself for runtime codes (negative, below). class Error < StandardError - # The ABI error code: positive for a domain error, negative for a runtime + # A generic failure, and every failure of a `throws: any` call. + GENERIC = -1 + # The library panicked. + PANIC = -2 + # An argument or a returned value couldn't be marshalled. + MARSHAL = -3 + # A callback implementation failed and the call couldn't report it any + # other way. + CALLBACK = -4 + # The call was cancelled through its CancelToken (see Cancelled). + CANCELLED = -5 + + # The ABI error code: positive for a domain code, negative for a runtime # failure. + # @return [Integer] attr_reader :code - def initialize(code, message) - @code = code + # @param message [String, nil] the message; an error code's class + # defaults to its documented message + # @param code [Integer, nil] the code, for a class without a fixed + # `CODE` (an error code's class has one) + # @param fields the payload fields of an error code that declares any, + # each required + def initialize(message = nil, code: nil, **fields) + code_class = Bridge.code_class(self.class) + if code_class.nil? + raise ArgumentError, "unknown keywords: #{fields.keys.join(', ')}" unless fields.empty? + + @code = code || GENERIC + else + unless code.nil? || code == code_class::CODE + raise ArgumentError, "#{self.class} has the fixed code #{code_class::CODE}" + end + + Bridge.assign_fields(self, code_class, fields) + @code = code_class::CODE + message ||= code_class::MESSAGE + end super(message) end end @@ -79,813 +69,1195 @@ def initialize(code, message) # it returned, a callback failure it let through), never an outcome to # handle; the message names the code and the library's message. class NativeBugError < Error - def initialize(code, message) - super(code, "native call failed with code #{code}: #{message}") + # @param message [String, nil] the library's message + # @param code [Integer] the runtime code + def initialize(message = nil, code: MARSHAL) + super("native call failed with code #{code}: #{message}", code: code) end end # Raised when a cancellable call completes because its CancelToken fired. class Cancelled < Error - def initialize(message = 'cancelled') - super(CANCELLED_ERROR_CODE, message) + # @param message [String, nil] the library's message + def initialize(message = nil) + super(message || 'cancelled', code: CANCELLED) end end - attach_function :{{PREFIX}}_error_set, [:pointer, :int32, :string], :void - attach_function :{{PREFIX}}_error_set_payload, [:pointer, :pointer, :size_t], :void - attach_function :{{PREFIX}}_error_clear, [:pointer], :void - attach_function :{{PREFIX}}_error_free, [:pointer], :void - attach_function :{{PREFIX}}_alloc, [:size_t], :pointer - attach_function :{{PREFIX}}_free_bytes, [:pointer, :size_t], :void - attach_function :{{PREFIX}}_cancel_token_create, [], :pointer - attach_function :{{PREFIX}}_cancel_token_cancel, [:pointer], :void - attach_function :{{PREFIX}}_cancel_token_is_cancelled, [:pointer], :bool - attach_function :{{PREFIX}}_cancel_token_destroy, [:pointer], :void - - # @api private - # One entry of a module's contract table. - class ContractEntry < FFI::Struct - layout :id, :uint64, - :hash, :uint64 - end - # @api private - # Checks, for every top-level module, that the library's contract table - # (`{{PREFIX}}_{module}_contract`) holds each declaration these bindings - # were generated with, as `[id, hash, path]`, with an equal signature - # hash. Declarations the library adds are fine. - def self._wv_check_contract!(tables) - tables.each do |symbol, expected| - begin - attach_function symbol, [:pointer], :pointer unless respond_to?(symbol) - rescue FFI::NotFoundError - raise LoadError, "the loaded {{LIBRARY}} library does not export #{symbol}" - end - out_len = FFI::MemoryPointer.new(:size_t) - table = public_send(symbol, out_len) - count = table.null? ? 0 : out_len.read(:size_t) - actual = Array.new(count) do |i| - entry = ContractEntry.new(table + (i * ContractEntry.size)) - [entry[:id], entry[:hash]] - end.to_h - expected.each do |id, hash, path| - found = actual[id] - raise LoadError, "{{LIBRARY}}: #{path} is missing from the library" if found.nil? - next if found == hash - - raise LoadError, "{{LIBRARY}}: #{path} changed since these bindings were generated" - end + # The native library and its runtime functions. The generated bindings + # attach every function they call here as well. + module Native + extend FFI::Library + + # An explicit path in {{LIBRARY_ENV}} wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system + # search path. + path = ENV.fetch('{{LIBRARY_ENV}}', '') + if path.empty? + name = + case FFI::Platform::OS + when /darwin/ then '{{LIB_MACOS}}' + when /mswin|mingw|windows/ then '{{LIB_WINDOWS}}' + else '{{LIB_LINUX}}' + end + bundled = File.expand_path(File.join('..', 'native', name), __dir__) + path = File.exist?(bundled) ? bundled : name + end + begin + ffi_lib(path) + rescue ::LoadError => e + raise {{MODULE}}::LoadError, "could not load the {{LIBRARY}} library (#{e.message})" end - end - - # @api private - # The error a throwing call raises for a code outside its domain: - # Cancelled for -5, otherwise the root Error. - def self._wv_error(code, message, _payload = nil) - return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE - - Error.new(code, message) - end - - # @api private - # The error a call that declares no errors raises when it fails anyway: - # Cancelled for -5, otherwise NativeBugError. - def self._wv_trap(code, message, _payload = nil) - return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE - - NativeBugError.new(code, message) - end - - # @api private - # The error for a malformed value the library handed over. - def self._wv_malformed(what) - NativeBugError.new(MARSHAL_ERROR_CODE, "malformed value buffer: #{what}") - end - - # @api private - # Copies the code, message, and payload out of a reported error and clears - # it. Returns nil when the struct reports success. - def self._wv_take_error(err) - code = err[:code] - return nil if code.zero? - - msg_ptr = err[:message] - message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - {{PREFIX}}_error_clear(err.to_ptr) - [code, message, payload] - end - - # @api private - # Like _wv_take_error for the heap-boxed error an async completion - # receives; the box is released with {{PREFIX}}_error_free. - def self._wv_take_boxed_error(err_ptr) - return nil if err_ptr.null? - - taken = _wv_take_error(ErrorStruct.new(err_ptr)) - {{PREFIX}}_error_free(err_ptr) - taken - end - - # @api private - # Raises NativeBugError for a failed call that declares no errors. - def self._wv_check!(err) - taken = _wv_take_error(err) - raise _wv_trap(*taken) unless taken.nil? - end - - # @api private - # The UTF-8 bytes of a String argument, as a private binary copy the - # library borrows for the call. - def self._wv_str(value) - str = String.try_convert(value) - raise TypeError, "expected a String, got #{value.class}" if str.nil? - - utf8 = str.encode(Encoding::UTF_8) - raise ArgumentError, 'string is not valid UTF-8' unless utf8.valid_encoding? - - utf8.b - end - - # @api private - # A private binary copy of a bytes argument. - def self._wv_bytes(value) - str = String.try_convert(value) - raise TypeError, "expected a String of bytes, got #{value.class}" if str.nil? - - str.b - end - - # The inclusive range of each fixed-width integer type. - INT_RANGES = { - i8: -0x80..0x7f, - u8: 0..0xff, - i16: -0x8000..0x7fff, - u16: 0..0xffff, - i32: -0x8000_0000..0x7fff_ffff, - u32: 0..0xffff_ffff, - i64: -0x8000_0000_0000_0000..0x7fff_ffff_ffff_ffff, - u64: 0..0xffff_ffff_ffff_ffff - }.freeze - - # @api private - # `value`, checked to be an Integer in the range of `kind` (`:i32`, ...): - # out-of-range values raise RangeError rather than wrap. - def self._wv_int(value, kind) - raise TypeError, "expected an Integer for #{kind}, got #{value.class}" unless value.is_a?(Integer) - raise RangeError, "#{value} is out of range for #{kind}" unless INT_RANGES[kind].cover?(value) - - value - end - - # @api private - # Raises TypeError unless `value` is a `cls` wrapper (or nil, when - # `optional`), so an object of the wrong interface never reaches the - # library. - def self._wv_object!(value, cls, name, optional = false) - return if value.is_a?(cls) || (optional && value.nil?) - - expected = optional ? "a #{cls.name} or nil" : "a #{cls.name}" - raise TypeError, "#{name} must be #{expected}, got #{value.class}" - end - - # @api private - # Raises TypeError for a nil callback implementation where one is required. - def self._wv_present!(value, name) - raise TypeError, "#{name} must not be nil" if value.nil? - end - - # @api private - # Copies a borrowed (ptr, len) run without releasing it. - def self._wv_borrow_bytes(ptr, len) - ptr.null? || len.zero? ? ''.b : ptr.read_string(len) - end - # @api private - # Copies a borrowed (ptr, len) UTF-8 run without releasing it. - def self._wv_borrow_string(ptr, len) - _wv_borrow_bytes(ptr, len).force_encoding(Encoding::UTF_8) - end + # Checked before anything else is attached, so a mismatched library + # fails at require time instead of misreading an error struct or a value + # buffer later. + begin + attach_function :{{PREFIX}}_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise {{MODULE}}::LoadError, + "the loaded {{LIBRARY}} library does not export {{PREFIX}}_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + revision = {{PREFIX}}_abi_version + unless revision == ABI_VERSION + raise {{MODULE}}::LoadError, + "{{LIBRARY}} ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{revision}" + end - # @api private - # Copies a returned (ptr, len) allocation, then releases it. - def self._wv_take_bytes(ptr, len) - return ''.b if ptr.null? + # The `{{PREFIX}}_error` out-parameter every call reports through. The + # message is UTF-8, not NUL-terminated, and NULL when empty. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message_ptr, :pointer, + :message_len, :size_t, + :payload_ptr, :pointer, + :payload_len, :size_t + end - data = len.zero? ? ''.b : ptr.read_string(len) - {{PREFIX}}_free_bytes(ptr, len) - data - end + # One entry of a module's contract table. + class ContractEntry < FFI::Struct + layout :id, :uint64, + :hash, :uint64 + end - # @api private - # Copies a returned UTF-8 string, then releases it. - def self._wv_take_string(ptr, len) - _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + attach_function :{{PREFIX}}_error_set, [:pointer, :int32, :pointer, :size_t], :void + attach_function :{{PREFIX}}_error_set_payload, [:pointer, :pointer, :size_t], :void + attach_function :{{PREFIX}}_error_clear, [:pointer], :void + attach_function :{{PREFIX}}_error_free, [:pointer], :void + attach_function :{{PREFIX}}_alloc, [:size_t], :pointer + attach_function :{{PREFIX}}_free_bytes, [:pointer, :size_t], :void + attach_function :{{PREFIX}}_cancel_token_create, [], :pointer + attach_function :{{PREFIX}}_cancel_token_cancel, [:pointer], :void + attach_function :{{PREFIX}}_cancel_token_is_cancelled, [:pointer], :bool + attach_function :{{PREFIX}}_cancel_token_destroy, [:pointer], :void end + private_constant :Native - # A cancellation signal for a cancellable async call. Pass one as the + # A cancellation signal for cancellable async calls. Pass one as the # `cancel:` keyword, then call #cancel from any thread; the call raises # Cancelled unless it already completed. A token may be shared by several # calls and stays cancelled once cancelled. class CancelToken - # @api private # Owns the consumer's reference to the native token. - class Ptr < FFI::AutoPointer + class Ref < FFI::AutoPointer + # @api private def self.release(ptr) - {{MODULE}}.{{PREFIX}}_cancel_token_destroy(ptr) + Native.{{PREFIX}}_cancel_token_destroy(ptr) end end + private_constant :Ref def initialize - @ptr = Ptr.new({{MODULE}}.{{PREFIX}}_cancel_token_create) + @ref = Ref.new(Native.{{PREFIX}}_cancel_token_create) end # Requests cancellation. Idempotent. + # @return [nil] def cancel - {{MODULE}}.{{PREFIX}}_cancel_token_cancel(@ptr) unless @ptr.nil? + Native.{{PREFIX}}_cancel_token_cancel(@ref) unless @ref.nil? nil end # Whether #cancel has been called. + # @return [Boolean] def cancelled? - !@ptr.nil? && {{MODULE}}.{{PREFIX}}_cancel_token_is_cancelled(@ptr) + !@ref.nil? && Native.{{PREFIX}}_cancel_token_is_cancelled(@ref) end # Releases the native token now rather than at GC time. Calls already # launched with it keep their own reference. + # @return [nil] def close - return if @ptr.nil? - - @ptr.free - @ptr = nil + @ref&.free + @ref = nil end - # @api private + private + def _wv_ptr - raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + raise Error, 'CancelToken used after close' if @ref.nil? - @ptr + @ref end end # @api private - # Appends values in the value-buffer wire format: little-endian, packed, - # no alignment. Integers out of their type's range raise RangeError rather - # than wrap. - class WvBufferWriter - def initialize - @buf = +''.b - @objects = [] + # The marshalling every generated wrapper shares: argument checks and + # conversions, the call helpers (errors, out slots, returns), async + # completions, iterators, interface wrappers, value buffers, and callback + # interfaces. + # + # Values are described by small Ruby literals. A *kind* says how a return, + # an async result, or an iterator element crosses: nil (void), a scalar + # (`:i32`, `:f64`, `:bool`; a C-style enum is `:i32`), `:string`, `:bytes`, + # `[:buffer, type]`, `[:slice, :i32]`, `[:opt, :i32]` (an optional scalar + # crossing as a flag and a value), `[:object, Class]`, `[:object?, Class]`, + # or `:pointer` (a constructor's raw pointer). A *type* says how a value + # is written into a value buffer: a scalar, `:string`, `:bytes`, + # `[:opt, type]`, `[:list, type]`, `[:map, key, value]`, or a class (a + # record, a rich enum's module, or an interface). + module Bridge + # The ffi gem's name for each scalar. + FFI_TYPES = { + bool: :bool, i8: :int8, u8: :uint8, i16: :int16, u16: :uint16, i32: :int32, + u32: :uint32, i64: :int64, u64: :uint64, f32: :float, f64: :double + }.freeze + + # The inclusive range of each integer scalar. + RANGES = { + i8: -0x80..0x7f, u8: 0..0xff, i16: -0x8000..0x7fff, u16: 0..0xffff, + i32: -0x8000_0000..0x7fff_ffff, u32: 0..0xffff_ffff, + i64: -0x8000_0000_0000_0000..0x7fff_ffff_ffff_ffff, u64: 0..0xffff_ffff_ffff_ffff + }.freeze + + # The size in bytes of each typed-array element. + SIZES = { i8: 1, i16: 2, u16: 2, i32: 4, u32: 4, i64: 8, u64: 8, f32: 4, f64: 8 }.freeze + + # --- Scalars, strings, and typed arrays --- + + # `value` checked for scalar `kind`: an Integer in range (RangeError + # rather than wrapping), a Numeric for a float, any truthiness for a + # bool. + def self.scalar(value, kind) + case kind + when :bool then value ? true : false + when :f32, :f64 + raise TypeError, "expected a Float, got #{value.class}" unless value.is_a?(Numeric) + + value.to_f + else + raise TypeError, "expected an Integer for #{kind}, got #{value.class}" unless value.is_a?(Integer) + raise RangeError, "#{value} is out of range for #{kind}" unless RANGES.fetch(kind).cover?(value) + + value + end end - # Reserves an object token for a `cls` wrapper. The strong reference it - # carries is minted by #seal, just before the buffer is handed to the - # library, so an encoding abandoned on the way never strands a - # reference. - def write_object(object, cls) - raise TypeError, "expected a #{cls.name}, got #{object.class}" unless object.is_a?(cls) - - @objects << [@buf.bytesize, object] - @buf << [0].pack('Q<') + # The `[has, value]` slots of an optional scalar argument. + def self.opt_arg(value, kind) + value.nil? ? [false, kind == :bool ? false : 0] : [true, scalar(value, kind)] end - # Mints the reference each reserved object token carries, recording it - # in `minted`, and returns the finished encoding. - def seal(minted) - @objects.each do |offset, object| - ptr = object._wv_clone_ptr - minted << [object.class, ptr] - @buf[offset, 8] = [ptr.address].pack('Q<') - end - @buf + # The UTF-8 bytes of a string argument, as a private binary copy the + # library borrows for the call. + def self.string_arg(value) + str = String.try_convert(value) + raise TypeError, "expected a String, got #{value.class}" if str.nil? + + bytes = + if str.encoding == Encoding::UTF_8 || str.encoding == Encoding::BINARY || str.ascii_only? + str.b + else + str.encode(Encoding::UTF_8).b + end + raise ArgumentError, 'string is not valid UTF-8' unless bytes.force_encoding(Encoding::UTF_8).valid_encoding? + + bytes.force_encoding(Encoding::BINARY) + rescue EncodingError => e + raise ArgumentError, "string can't be converted to UTF-8 (#{e.message})" end - # The finished encoding of a buffer that carries no objects. - def bytes - raise TypeError, 'this encoding cannot carry objects' unless @objects.empty? + # A private binary copy of a bytes argument. + def self.bytes_arg(value) + str = String.try_convert(value) + raise TypeError, "expected a String of bytes, got #{value.class}" if str.nil? - @buf + str.b end - def write_bool(value) - @buf << (value ? "\x01" : "\x00").b + # The `[pointer, count]` slots of a typed-array argument: the elements, + # range-checked, in an aligned native array (NULL when empty). + def self.slice_arg(values, kind) + ary = Array.try_convert(values) + raise TypeError, "expected an Array, got #{values.class}" if ary.nil? + return [nil, 0] if ary.empty? + + ptr = FFI::MemoryPointer.new(FFI_TYPES.fetch(kind), ary.length) + ptr.public_send(:"put_array_of_#{FFI_TYPES.fetch(kind)}", 0, ary.map { |v| scalar(v, kind) }) + [ptr, ary.length] end - def write_flag(value) - write_bool(value) + # Copies a borrowed (ptr, len) run without releasing it. + def self.borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_bytes(len) end - def write_i8(value) - @buf << [int(value, :i8)].pack('c') + # Copies a borrowed (ptr, len) UTF-8 run without releasing it. + def self.borrow_string(ptr, len) + borrow_bytes(ptr, len).force_encoding(Encoding::UTF_8) end - def write_u8(value) - @buf << [int(value, :u8)].pack('C') + # Copies a borrowed typed array of `len` elements without releasing it. + def self.borrow_slice(ptr, len, kind) + ptr.null? || len.zero? ? [] : ptr.public_send(:"get_array_of_#{FFI_TYPES.fetch(kind)}", 0, len) end - def write_i16(value) - @buf << [int(value, :i16)].pack('s<') + # Copies a returned (ptr, len) run, then releases it. + def self.take_bytes(ptr, len) + data = borrow_bytes(ptr, len) + Native.{{PREFIX}}_free_bytes(ptr, len) unless ptr.null? + data end - def write_u16(value) - @buf << [int(value, :u16)].pack('S<') + # Copies a returned UTF-8 run, then releases it. + def self.take_string(ptr, len) + take_bytes(ptr, len).force_encoding(Encoding::UTF_8) end - def write_i32(value) - @buf << [int(value, :i32)].pack('l<') + # Copies a returned typed array, then releases it. + def self.take_slice(ptr, len, kind) + values = borrow_slice(ptr, len, kind) + Native.{{PREFIX}}_free_bytes(ptr, len * SIZES.fetch(kind)) unless ptr.null? + values end - def write_u32(value) - @buf << [int(value, :u32)].pack('L<') + # --- Errors --- + + # Copies the code, message, and payload out of a reported error and + # clears it, or returns nil when it reports success. + def self.take_error(err) + code = err[:code] + return nil if code.zero? + + message = borrow_string(err[:message_ptr], err[:message_len]) + payload = err[:payload_ptr].null? ? nil : borrow_bytes(err[:payload_ptr], err[:payload_len]) + Native.{{PREFIX}}_error_clear(err.to_ptr) + [code, message.empty? ? nil : message, payload] end - def write_len(value) - write_u32(value) + # Raises the error a non-zero error slot reports, per `error`: nil for a + # call that declares no errors (NativeBugError), Error for `throws: + # any`, or a domain class. + def self.check!(err, error) + taken = take_error(err) + raise failure(error, *taken) unless taken.nil? end - def write_i64(value) - @buf << [int(value, :i64)].pack('q<') + # The exception for a reported failure. A positive code of a domain is + # its code class (fields decoded from the payload), or the domain class + # itself for a code these bindings don't know. + def self.failure(error, code, message, payload) + return Cancelled.new(message) if code == Error::CANCELLED + return NativeBugError.new(message, code: code) if error.nil? + return Error.new(message, code: code) unless code.positive? && error != Error + + code_class = codes(error)[code] + return error.new(message, code: code) if code_class.nil? + + fields = RECORDS.key?(code_class) ? decode_bytes(payload.to_s, code_class, fields: true) : {} + code_class.new(message, **fields) + rescue NativeBugError => e + e end - def write_u64(value) - @buf << [int(value, :u64)].pack('Q<') + # The error-code classes of a domain, by code: the classes nested in it + # that carry a `CODE`. + def self.codes(domain) + @codes[domain] ||= domain.constants(false).filter_map do |name| + cls = domain.const_get(name, false) + [cls::CODE, cls] if cls.is_a?(Class) && cls < domain && cls.const_defined?(:CODE, false) + end.to_h end + @codes = {} - def write_f32(value) - @buf << [value].pack('e') + # The class whose `CODE` an error class carries, or nil. + def self.code_class(cls) + cls.ancestors.find { |a| a.is_a?(Class) && a <= Error && a.const_defined?(:CODE, false) } end - def write_f64(value) - @buf << [value].pack('E') + # Sets an error code's payload fields from `fields`, each required. + def self.assign_fields(error, code_class, fields) + names = RECORDS.fetch(code_class, {}).keys + missing = names - fields.keys + raise ArgumentError, "missing keywords: #{missing.join(', ')}" unless missing.empty? + + unknown = fields.keys - names + raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty? + + fields.each { |name, value| error.instance_variable_set(:"@#{name}", value) } end - def write_string(value) - write_bytes({{MODULE}}._wv_str(value)) + # --- Calls --- + + # Makes one synchronous call. Seals `writers` (the call's value-buffer + # arguments), allocates the out slots `kind` needs, yields the sealed + # encodings, the out slots, and the error slot, raises the reported + # error, and returns the result received per `kind`. + def self.call(error, kind = nil, *writers) + err = Native::ErrorStruct.new + outs = return_slots(kind) + result = yield(*seal(*writers), *outs, err) + check!(err, error) + take_return(kind, result, outs) end - def write_bytes(value) - bytes = {{MODULE}}._wv_bytes(value) - write_u32(bytes.bytesize) - @buf << bytes + # The out slots a synchronous return of `kind` needs. + def self.return_slots(kind) + case kind + when :string, :bytes then [FFI::MemoryPointer.new(:size_t)] + when Array + case kind[0] + when :buffer, :slice then [FFI::MemoryPointer.new(:size_t)] + when :opt then [FFI::MemoryPointer.new(FFI_TYPES.fetch(kind[1]))] + else [] + end + else [] + end end - private + # A synchronous call's result, received per `kind`. + def self.take_return(kind, result, outs) + case kind + when :pointer then nonnull(result) + when :string, :bytes then take_value(kind, result, outs[0].read(:size_t)) + when Array + case kind[0] + when :buffer, :slice then take_value(kind, result, outs[0].read(:size_t)) + when :opt then result ? outs[0].read(FFI_TYPES.fetch(kind[1])) : nil + else take_value(kind, result) + end + else result + end + end - def int(value, kind) - {{MODULE}}._wv_int(value, kind) + # A value the library handed over (an async result or an iterator + # element) from its slot values, received per `kind`: runs are copied + # and released, objects adopted. + def self.take_value(kind, *slots) + case kind + when nil then nil + when :string then take_string(*slots) + when :bytes then take_bytes(*slots) + when Symbol then slots[0] + else + case kind[0] + when :buffer then decode(slots[0], slots[1], kind[1]) + when :slice then take_slice(slots[0], slots[1], kind[1]) + when :opt then slots[0] ? slots[1] : nil + when :object then adopt(kind[1], nonnull(slots[0])) + when :object? then slots[0].null? ? nil : adopt(kind[1], slots[0]) + end + end end - end - # @api private - # Reads values in the value-buffer wire format, raising NativeBugError on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 + # The C types of the slots a value of `kind` occupies as an async + # result. + def self.slot_types(kind) + case kind + when nil then [] + when :string, :bytes then %i[pointer size_t] + when Symbol then [FFI_TYPES.fetch(kind)] + else + case kind[0] + when :buffer, :slice then %i[pointer size_t] + when :opt then [:bool, FFI_TYPES.fetch(kind[1])] + else [:pointer] + end + end end - def take(count, what) - raise {{MODULE}}._wv_malformed("truncated #{what}") if @pos + count > @data.bytesize + # A returned object pointer that must not be NULL. + def self.nonnull(ptr) + raise NativeBugError.new('null object pointer', code: Error::MARSHAL) if ptr.null? - bytes = @data.byteslice(@pos, count) - @pos += count - bytes + ptr end - def read_bool - byte = take(1, 'bool').unpack1('C') - raise {{MODULE}}._wv_malformed('bool byte out of range') if byte > 1 - - byte == 1 + # --- Async calls --- + + @completions = {} + @completions_lock = Mutex.new + + # The completion function for async results of `kind`, one per result + # slot signature for the life of the process (the library may call it + # from any thread). It finds the pending call by `context`, receives the + # error or the result, and delivers it to the waiting caller; nothing + # raised while receiving escapes into the C frame. + def self.completion(kind) + types = slot_types(kind) + @completions_lock.synchronize do + @completions[types] ||= FFI::Function.new(:void, [:pointer, :pointer, *types]) do |context, err, *slots| + entry = HANDLES.take(context) + next if entry.nil? + + _, queue, error, result_kind = entry + begin + taken = take_boxed_error(err) + queue << (taken.nil? ? take_value(result_kind, *slots) : failure(error, *taken)) + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + end end - def read_flag - byte = take(1, 'option flag').unpack1('C') - raise {{MODULE}}._wv_malformed('option flag out of range') if byte > 1 + # Like take_error, for the heap-boxed error a completion receives; the + # box is released. + def self.take_boxed_error(err) + return nil if err.null? - byte == 1 + taken = take_error(Native::ErrorStruct.new(err)) + Native.{{PREFIX}}_error_free(err) + taken end - def read_i8 - take(1, 'i8').unpack1('c') + # Launches an async call and blocks until it completes: yields the + # sealed `writers`, the completion function, and the completion context + # to the launch, then waits for the result or raises the error. + def self.await(error, kind, *writers) + pending(error, kind, writers) { |sealed, callback, context| yield(*sealed, callback, context) } end - def read_u8 - take(1, 'u8').unpack1('C') + # Like await for a cancellable call: yields the native token (the + # caller's `cancel`, or a private one) before the completion, so an + # interrupted wait (Thread#raise, Timeout) cancels the library's work + # too. + def self.await_cancellable(error, kind, cancel, *writers) + raise TypeError, "cancel must be a CancelToken, got #{cancel.class}" unless cancel.nil? || cancel.is_a?(CancelToken) + + own = CancelToken.new if cancel.nil? + token = cancel || own + pending(error, kind, writers, token) do |sealed, callback, context| + yield(*sealed, token.__send__(:_wv_ptr), callback, context) + end + ensure + own&.close end - def read_i16 - take(2, 'i16').unpack1('s<') + # Registers a pending call, launches it, and waits on its queue. + def self.pending(error, kind, writers, token = nil) + queue = Thread::Queue.new + context = HANDLES.put([:async, queue, error, kind]) + launched = false + begin + yield seal(*writers), completion(kind), context + launched = true + ensure + HANDLES.take(context) unless launched + end + wait(queue, token) end - def read_u16 - take(2, 'u16').unpack1('S<') + # Blocks until a pending call completes (Thread::Queue#pop, which + # releases the GVL and, in a non-blocking Fiber under a Fiber scheduler, + # yields to the scheduler), raising its error. If the wait is + # interrupted, the call's token is cancelled before the interruption + # propagates. + def self.wait(queue, token) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value end - def read_i32 - take(4, 'i32').unpack1('l<') + # --- Iterators --- + + # A lazy Enumerator over a native iterator. Each enumeration launches + # its own iterator (on the first pull, through the block, which receives + # the sealed `writers` and the error slot), pulls one element per step + # with `next_fn`, and releases the iterator with `destroy_fn` exactly + # once: when iteration finishes, raises, or stops early, or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer, since Ruby + # never runs the `ensure` of an external enumeration (`next`) that's + # abandoned midway. + def self.iterate(error, kind, next_fn, destroy_fn, *writers, &launch) + Enumerator.new do |y| + err = Native::ErrorStruct.new + raw = launch.call(*seal(*writers), err) + iter = FFI::AutoPointer.new(raw, Native.method(destroy_fn)) unless raw.null? + begin + check!(err, error) + outs = item_slots(kind) + advance = Native.method(next_fn) + loop do + more = advance.call(iter, *outs, err) + check!(err, error) + break if more.zero? + + y << take_value(kind, *read_slots(kind, outs)) + end + ensure + iter&.free + end + end end - def read_u32 - take(4, 'u32').unpack1('L<') + # The out slots of one iterator element of `kind`: one pointer to each + # slot the element would occupy as an async result. + def self.item_slots(kind) + slot_types(kind).map { |type| FFI::MemoryPointer.new(type) } end - # A u32 element count. Counts are not checked against the remaining - # bytes, since an element can encode to zero bytes; a short buffer still - # fails on the first truncated read. - def read_len - read_u32 + # The values `_next` wrote to an element's out slots. + def self.read_slots(kind, outs) + slot_types(kind).zip(outs).map { |type, out| out.read(type) } end - def read_i64 - take(8, 'i64').unpack1('q<') + # --- Interfaces --- + + # The native lifecycle of each interface class: [clone, destroy]. + INTERFACES = {} + + # Registers the C functions that clone and release `cls`'s objects. + def self.interface(cls, clone, destroy) + INTERFACES[cls] = [Native.method(clone), Native.method(destroy)] end - def read_u64 - take(8, 'u64').unpack1('Q<') + # The base of every interface wrapper. A wrapper owns one strong + # reference to its native object and releases it exactly once, from + # #close or (as a backstop) from the GC finalizer of its + # FFI::AutoPointer. Calls borrow the pointer through Bridge.pin, so a + # #close that races an in-flight call on another thread only releases + # the reference once the call returns. + class Handle + # Whether #close has released this wrapper's reference. + # @return [Boolean] + def closed? + @_wv_ref.nil? || @_wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference + # anywhere (another wrapper, a record field, the library) goes. + # @return [nil] + def close + ref = @_wv_lock.synchronize do + next nil if closed? + next _wv_detach if @_wv_calls.zero? + + @_wv_closing = true + nil + end + ref&.free + nil + end + + # Two wrappers are equal when they're open and refer to the same + # native object. + # @return [Boolean] + def ==(other) + equal?(other) || + (other.instance_of?(self.class) && !closed? && !other.closed? && other._wv_address == _wv_address) + end + alias eql? == + + # @return [Integer] + def hash + [self.class, _wv_address].hash + end + + # @return [String] + def inspect + format('#<%s 0x%
016x%s>', cls: self.class.name, address: _wv_address, + state: closed? ? ' (closed)' : '') + end + + # dup and clone produce an independent wrapper with its own reference + # to the same object. + def initialize_copy(other) + super + _wv_init(Bridge.clone_ref(other)) + end + + protected + + attr_reader :_wv_address + + private + + def _wv_init(ptr) + @_wv_ref = FFI::AutoPointer.new(ptr, INTERFACES.fetch(self.class)[1]) + @_wv_address = ptr.address + @_wv_lock = Mutex.new + @_wv_calls = 0 + @_wv_closing = false + end + + # Yields the native pointer, keeping the reference alive until the + # block returns. + def _wv_call + ref = @_wv_lock.synchronize do + raise Error, "#{self.class.name} used after close" if closed? + + @_wv_calls += 1 + @_wv_ref + end + begin + yield ref + ensure + _wv_leave + end + end + + def _wv_leave + ref = @_wv_lock.synchronize do + @_wv_calls -= 1 + next nil unless @_wv_closing && @_wv_calls.zero? + + @_wv_closing = false + _wv_detach + end + ref&.free + end + + def _wv_detach + ref = @_wv_ref + @_wv_ref = nil + ref + end end - def read_f32 - take(4, 'f32').unpack1('e') + # Adopts one strong reference the library handed over into a new + # `cls` wrapper. + def self.adopt(cls, ptr) + cls.allocate.tap { |obj| obj.__send__(:_wv_init, ptr) } end - def read_f64 - take(8, 'f64').unpack1('E') + # Mints a new strong reference to `obj`'s native object, which the + # caller owns. + def self.clone_ref(obj) + obj.__send__(:_wv_call) { |ptr| INTERFACES.fetch(obj.class)[0].call(ptr) } end - def read_string - text = read_bytes.force_encoding(Encoding::UTF_8) - raise {{MODULE}}._wv_malformed('string is not valid UTF-8') unless text.valid_encoding? + # Raises TypeError unless `value` is a `cls` wrapper (or nil, when + # `nullable`), so an object of the wrong interface never reaches the + # library. + def self.check_object(value, cls, name, nullable: false) + return if value.is_a?(cls) || (nullable && value.nil?) - text + expected = nullable ? "a #{cls.name} or nil" : "a #{cls.name}" + raise TypeError, "#{name} must be #{expected}, got #{value.class}" end - def read_bytes - take(read_u32, 'byte run') + # Yields the native pointers of `objects` (NULL for nil), each kept + # alive until the block returns. + def self.pin(*objects, &block) + pin_from(objects, 0, [], &block) end - # An object token: a non-zero u64 carrying one strong reference, adopted - # into a new `cls` wrapper. - def read_object(cls) - addr = read_u64 - raise {{MODULE}}._wv_malformed('null object token') if addr.zero? + def self.pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + return pin_from(objects, index + 1, pointers + [nil], &block) if object.nil? - cls._from_ptr(FFI::Pointer.new(addr)) + object.__send__(:_wv_call) { |ptr| pin_from(objects, index + 1, pointers + [ptr], &block) } end - def expect_end! - return if @pos == @data.bytesize + # --- Value buffers --- - raise {{MODULE}}._wv_malformed('trailing bytes after value') + # The fields of each record, rich-enum variant, and error code with a + # payload: class => { field => type }, in wire order. + RECORDS = {} + # The variants of each rich enum: module => { tag => variant class }. + UNIONS = {} + + # Registers the wire layout of a record, a rich-enum variant, or an + # error code's payload. + def self.record(cls, **fields) + if cls.respond_to?(:members) && cls.members != fields.keys + raise ArgumentError, "#{cls} layout #{fields.keys} doesn't match its members #{cls.members}" + end + + RECORDS[cls] = fields.freeze end - end - # @api private - # Seals the writers of one call's object-carrying buffers (see - # WvBufferWriter#seal) and returns their encodings. If any reference can't - # be minted (a closed wrapper), the ones already minted are released before - # the error propagates. The library adopts every minted reference once the - # call is made. - def self._wv_seal(*writers) - minted = [] - writers.map { |writer| writer.seal(minted) } - rescue Exception # rubocop:disable Lint/RescueException - minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } - raise - end + # Registers a rich enum's variants (each with its `TAG`). + def self.union(mod, *variants) + UNIONS[mod] = variants.to_h { |v| [v::TAG, v] }.freeze + end - # @api private - # Encodes an argument that carries no objects: yields a writer and - # returns the finished encoding. - def self._wv_encode - writer = WvBufferWriter.new - yield writer - writer.bytes - end + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class Writer + # `Array#pack` directives of the fixed-width scalars. + PACK = { + i8: 'c', u8: 'C', i16: 's<', u16: 'S<', i32: 'l<', u32: 'L<', i64: 'q<', u64: 'Q<', f32: 'e', f64: 'E' + }.freeze + + def initialize + @buf = +''.b + @objects = [] + end - # @api private - # A returned object pointer that must not be NULL. - def self._wv_nonnull(ptr) - raise NativeBugError.new(MARSHAL_ERROR_CODE, 'null object pointer') if ptr.null? + # Appends `value` as `type`. + def write(type, value) + case type + when :bool then @buf << (value ? "\x01" : "\x00") + when :string then run(Bridge.string_arg(value)) + when :bytes then run(Bridge.bytes_arg(value)) + when Symbol then @buf << [Bridge.scalar(value, type)].pack(PACK.fetch(type)) + when Array then composite(type, value) + else user(type, value) + end + self + end - ptr - end + # Mints the reference each reserved object token carries (just before + # the encoding is handed over, so an encoding abandoned on the way + # never strands one), recording it in `minted`, and returns the + # finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = Bridge.clone_ref(object) + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end - # @api private - # Decodes a returned value buffer: copies and releases it, yields a reader - # to the block, and checks that the block consumed every byte. - def self._wv_decode(ptr, len) - reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) - value = yield reader - reader.expect_end! - value - end + private - # @api private - # Decodes a borrowed value buffer (a callback argument) without releasing - # it. - def self._wv_decode_borrowed(ptr, len) - reader = WvBufferReader.new(_wv_borrow_bytes(ptr, len)) - value = yield reader - reader.expect_end! - value - end + def run(bytes) + len(bytes.bytesize) + @buf << bytes + end - # Ruby objects the library refers to by an integer key: callback-interface - # implementations (the key is the vtable `ctx`) and pending async - # completions (the key is the completion `context`). The library never - # sees a Ruby object address. A Monitor rather than a Mutex, so a - # finalizer that runs while this thread holds the lock can't deadlock it. - @wv_handles = {} - @wv_next_handle = 0 - @wv_handles_lock = Monitor.new + def len(count) + raise RangeError, "#{count} elements don't fit a value buffer" if count > 0xffff_ffff - # @api private - def self._wv_handle_put(value) - @wv_handles_lock.synchronize do - @wv_next_handle += 1 - @wv_handles[@wv_next_handle] = value - FFI::Pointer.new(@wv_next_handle) - end - end + @buf << [count].pack('L<') + end - # @api private - def self._wv_handle_get(key) - @wv_handles_lock.synchronize { @wv_handles[key.address] } - end + def composite(type, value) + case type[0] + when :opt + @buf << (value.nil? ? "\x00" : "\x01") + write(type[1], value) unless value.nil? + when :list + ary = Array.try_convert(value) + raise TypeError, "expected an Array, got #{value.class}" if ary.nil? + + len(ary.length) + ary.each { |e| write(type[1], e) } + when :map + hash = Hash.try_convert(value) + raise TypeError, "expected a Hash, got #{value.class}" if hash.nil? + + len(hash.length) + hash.each do |k, v| + write(type[1], k) + write(type[2], v) + end + end + end - # @api private - def self._wv_handle_take(key) - @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } - end + def user(type, value) + raise TypeError, "expected a #{type.name}, got #{value.class}" unless value.is_a?(type) - # @api private - # Registers a callback-interface implementation and returns its `ctx` and - # `vtable` arguments: two NULLs for a nil (optional) implementation. Never - # raises, so nothing can strand the registration between this and the - # call that hands it to the library (an unusable implementation fails - # when the library calls it, with FOREIGN_ERROR_CODE). - def self._wv_cb_register(impl, vtable) - return [nil, nil] if impl.nil? - - [_wv_handle_put([:callback, impl]), vtable.to_ptr] - end + if type.is_a?(Class) && type < Handle + @objects << [@buf.bytesize, value] + @buf << [0].pack('Q<') + elsif UNIONS.key?(type) + write(:i32, value.class::TAG) + fields(value.class, value) + else + fields(type, value) + end + end - # @api private - # Resolves a trampoline's `ctx` back to the registered implementation. - def self._wv_cb_lookup(ctx) - kind, impl = _wv_handle_get(ctx) - raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + def fields(cls, value) + RECORDS.fetch(cls).each { |name, type| write(type, value.public_send(name)) } + end + end - impl - end + # Reads values in the value-buffer wire format, raising NativeBugError + # on any malformed buffer. + class Reader + # The byte width of each fixed-width scalar. + WIDTHS = { i8: 1, u8: 1, i16: 2, u16: 2, i32: 4, u32: 4, i64: 8, u64: 8, f32: 4, f64: 8 }.freeze - # @api private - # Drops an implementation when the library releases its last reference. - def self._wv_cb_free(ctx) - _wv_handle_take(ctx) - nil - end + def initialize(data) + @data = data + @pos = 0 + end - # @api private - # The message a callback failure reports: the exception's message (its - # class name when that's empty), as UTF-8 without NULs. - def self._wv_cb_message(exception) - message = exception.message.to_s - message = exception.class.name.to_s if message.empty? - message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") - end + # Reads one `type` value. + def read(type) + case type + when :bool then flag('bool') + when :string + text = run.force_encoding(Encoding::UTF_8) + raise Bridge.malformed('string is not valid UTF-8') unless text.valid_encoding? + + text + when :bytes then run + when Symbol then take(WIDTHS.fetch(type), type).unpack1(Writer::PACK.fetch(type)) + when Array then composite(type) + else user(type) + end + end - # @api private - # Reports a callback implementation's exception through `out_err` as - # FOREIGN_ERROR_CODE. The library copies the message. - def self._wv_cb_fail(out_err, exception) - {{PREFIX}}_error_set(out_err, FOREIGN_ERROR_CODE, _wv_cb_message(exception)) - nil - end + # The fields of a record layout as a Hash. + def fields(cls) + RECORDS.fetch(cls).transform_values { |type| read(type) } + end - # @api private - # Reports a domain error raised by a callback method declared to throw: - # the block returns its `[code, payload]`, or nil when it isn't one of the - # domain's codes, which then fails as FOREIGN_ERROR_CODE like any other - # exception. - def self._wv_cb_throw(out_err, exception) - code, payload = yield - return _wv_cb_fail(out_err, exception) if code.nil? - - {{PREFIX}}_error_set(out_err, code, _wv_cb_message(exception)) - {{PREFIX}}_error_set_payload(out_err, payload, payload.bytesize) unless payload.empty? - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(out_err, e) - end + def expect_end! + raise Bridge.malformed('trailing bytes after value') unless @pos == @data.bytesize + end - # @api private - # A callback method's floating-point return, checked to be numeric. - def self._wv_float(value) - raise TypeError, "expected a Float, got #{value.class}" unless value.is_a?(Numeric) + private - value.to_f - end + def take(count, what) + raise Bridge.malformed("truncated #{what}") if @pos + count > @data.bytesize - # @api private - # Hands a callback method's string, bytes, or buffer return to the - # library: a run allocated with {{PREFIX}}_alloc, written to the out slots. - # The library adopts and frees it. - def self._wv_cb_return_bytes(out_ptr, out_len, data) - len = data.bytesize - run = FFI::Pointer::NULL - unless len.zero? - run = {{PREFIX}}_alloc(len) - raise NoMemoryError, '{{PREFIX}}_alloc failed' if run.null? + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end - run.put_bytes(0, data) - end - out_ptr.write_pointer(run) - out_len.write(:size_t, len) - nil - end + def flag(what) + byte = take(1, what).unpack1('C') + raise Bridge.malformed("#{what} byte out of range") if byte > 1 - # @api private - # A callback method's object return: a fresh strong reference the library - # adopts, or NULL for nil (which the library rejects where the return - # isn't optional). - def self._wv_cb_return_object(value, cls) - return FFI::Pointer::NULL if value.nil? - raise TypeError, "expected a #{cls.name}, got #{value.class}" unless value.is_a?(cls) - - value._wv_clone_ptr - end + byte == 1 + end - # @api private - # Registers a pending async call; returns its completion context and the - # queue its completion is delivered on. - def self._wv_async_begin - queue = Queue.new - [_wv_handle_put([:async, queue]), queue] - end + # A u32 element count. Counts aren't checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer + # still fails on its first truncated read. + def len + take(4, 'length').unpack1('L<') + end - # @api private - # The queue of the pending call a completion belongs to. Each context - # completes exactly once, so its entry is removed here. - def self._wv_async_finish(ctx) - kind, queue = _wv_handle_take(ctx) - kind == :async ? queue : Queue.new - end + def run + take(len, 'byte run') + end - # @api private - # Blocks until a pending call completes, raising its error. If the wait is - # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is - # cancelled before the interruption propagates. - def self._wv_async_wait(queue, token = nil) - value = - begin - queue.pop - rescue Exception # rubocop:disable Lint/RescueException - token&.cancel - raise + def composite(type) + case type[0] + when :opt then flag('option flag') ? read(type[1]) : nil + when :list then Array.new(len) { read(type[1]) } + when :map then Array.new(len) { [read(type[1]), read(type[2])] }.to_h + end end - raise value if value.is_a?(Exception) - value - end + def user(type) + if type.is_a?(Class) && type < Handle + address = read(:u64) + raise Bridge.malformed('null object token') if address.zero? - # @api private - # The base of every interface wrapper. A wrapper owns one strong reference - # to its native object and releases it exactly once, from #close or (as a - # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the - # pointer through #_wv_call, so a #close that races an in-flight call on - # another thread only releases the reference once the call returns. - class WvObject - # @api private - # Adopts one strong reference the library handed over. - def self._from_ptr(ptr) - obj = allocate - obj._wv_init(ptr) - obj + Bridge.adopt(type, FFI::Pointer.new(address)) + elsif (variants = UNIONS[type]) + tag = read(:i32) + variant = variants[tag] + raise Bridge.malformed("unknown #{type.name} tag #{tag}") if variant.nil? + + variant.new(**fields(variant)) + else + type.new(**fields(type)) + end + end end - # @api private - def _wv_init(ptr) - @wv_handle = self.class::WV_PTR.new(ptr) - @wv_lock = Mutex.new - @wv_calls = 0 - @wv_closing = false + # The error for a malformed value the library handed over. + def self.malformed(what) + NativeBugError.new("malformed value buffer: #{what}", code: Error::MARSHAL) end - # The native object pointer (borrowed; valid until #close). - def handle - handle = @wv_handle - raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + # A writer holding `value` encoded as `type`, sealed just before the + # call (see #seal). + def self.encode(type, value) + Writer.new.write(type, value) + end - handle + # Seals the writers of one call's value-buffer arguments and returns + # their encodings. If any reference can't be minted (a closed wrapper), + # the ones already minted are released before the error propagates. The + # library adopts every minted reference once the call is made. + def self.seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| INTERFACES.fetch(cls)[1].call(ptr) } + raise end - # Whether #close has released this wrapper's reference. - def closed? - @wv_handle.nil? || @wv_closing + # Decodes a returned value buffer: copies and releases it, then reads + # one `type` value, which must consume every byte. + def self.decode(ptr, len, type) + decode_bytes(take_bytes(ptr, len), type) end - # Releases this wrapper's reference now rather than at GC time. - # Idempotent. The object itself lives until its last reference anywhere - # (another wrapper, a record field, the library) goes. - def close - handle = @wv_lock.synchronize do - next nil if @wv_handle.nil? || @wv_closing + # Decodes a borrowed value buffer (a callback argument) without + # releasing it. + def self.decode_borrowed(ptr, len, type) + decode_bytes(borrow_bytes(ptr, len), type) + end - if @wv_calls.zero? - taken = @wv_handle - @wv_handle = nil - taken - else - @wv_closing = true - nil - end - end - handle&.free - nil + # Reads one `type` value (or, with `fields`, a layout's fields) from a + # complete encoding. + def self.decode_bytes(bytes, type, fields: false) + reader = Reader.new(bytes) + value = fields ? reader.fields(type) : reader.read(type) + reader.expect_end! + value end - # @api private - # Yields the native pointer, keeping the reference alive until the block - # returns. - def _wv_call - handle = @wv_lock.synchronize do - raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + # --- Callback interfaces --- + + # Ruby objects the library refers to by an integer key: callback + # implementations (the vtable `ctx`) and pending async calls (the + # completion `context`). The library never sees a Ruby object address. + # A Monitor rather than a Mutex, so a finalizer that runs while this + # thread holds the lock can't deadlock it. + class Handles + def initialize + @entries = {} + @next = 0 + @lock = Monitor.new + end - @wv_calls += 1 - @wv_handle + def put(value) + @lock.synchronize do + @next += 1 + @entries[@next] = value + FFI::Pointer.new(@next) + end end - begin - yield handle - ensure - _wv_leave + + def get(key) + @lock.synchronize { @entries[key.address] } + end + + def take(key) + @lock.synchronize { @entries.delete(key.address) } end end + HANDLES = Handles.new + + # The vtable `free` every interface shares: drops the implementation + # when the library releases its last reference (on any library thread). + FREE = FFI::Function.new(:void, [:pointer]) { |ctx| HANDLES.take(ctx) } + + # The vtable of each callback interface module. + VTABLES = {} + + # Builds `iface`'s one process-wide vtable: the `{size, flags, free}` + # header (flags 0: methods may run on any thread), then one trampoline + # per method in declaration order. The trampolines live as long as the + # vtable, which is the process. + def self.vtable(iface, *methods) + word = FFI::Pointer.size + size = 8 + (word * (methods.length + 1)) + table = FFI::MemoryPointer.new(:uint8, size) + table.put_uint32(0, size) + table.put_uint32(4, 0) + table.put_pointer(8, FREE) + methods.each_with_index { |fn, i| table.put_pointer(8 + (word * (i + 1)), fn) } + VTABLES[iface] = [table, methods.freeze].freeze + end + + # Registers a callback implementation and returns its `ctx` and + # `vtable` arguments: two NULLs for a nil (optional) implementation. + # Never raises, so nothing can strand the registration between this + # and the call that hands it to the library (an unusable implementation + # fails when the library calls it). + def self.register(impl, iface) + return [nil, nil] if impl.nil? - # @api private - # Mints a new strong reference the caller owns (an object written into a - # value buffer or returned from a callback carries its own reference). - def _wv_clone_ptr - _wv_call { |ptr| self.class._wv_clone(ptr) } + [HANDLES.put([:callback, impl]), VTABLES.fetch(iface)[0]] end - # dup and clone produce an independent wrapper with its own reference to - # the same object. - def initialize_copy(other) - super - _wv_init(other._wv_clone_ptr) + # Raises TypeError for a nil callback implementation where one is + # required. + def self.present!(value, name) + raise TypeError, "#{name} must not be nil" if value.nil? end - private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self.impl(ctx) + kind, impl = HANDLES.get(ctx) + raise Error.new('callback context is not registered', code: Error::CALLBACK) unless kind == :callback + + impl + end + + # Runs a trampoline's body. Nothing unwinds through the C frame: an + # exception is reported through `out_err` (see report) and the method + # returns `default`. + def self.callback(out_err, error, default = nil) + yield + rescue Exception => e # rubocop:disable Lint/RescueException + report(out_err, e, error) + default + end + + # Reports a callback implementation's exception through `out_err`. A + # method that throws a domain (`error`) reports one of its errors with + # the error's code and fields as the payload; any other exception is -1 + # (or -4 for a method that declares no errors) with its message. The + # library copies both. + def self.report(out_err, exception, error) + code, payload = report_code(exception, error) + set_error(out_err, code, exception) + Native.{{PREFIX}}_error_set_payload(out_err, payload, payload.bytesize) unless payload.nil? || payload.empty? + rescue Exception => e # rubocop:disable Lint/RescueException + set_error(out_err, error.nil? ? Error::CALLBACK : Error::GENERIC, e) + end + + def self.report_code(exception, error) + return [Error::CALLBACK, nil] if error.nil? + return [Error::GENERIC, nil] unless error != Error && exception.is_a?(error) && exception.code.positive? - def _wv_leave - release = @wv_lock.synchronize do - @wv_calls -= 1 - next nil unless @wv_closing && @wv_calls.zero? + code_class = code_class(exception.class) + return [exception.code, nil] unless RECORDS.key?(code_class) - taken = @wv_handle - @wv_handle = nil - @wv_closing = false - taken + writer = Writer.new + RECORDS[code_class].each { |name, type| writer.write(type, exception.public_send(name)) } + [exception.code, seal(writer).first] + end + + def self.set_error(out_err, code, exception) + message = exception.message.to_s + message = exception.class.name.to_s if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).b + Native.{{PREFIX}}_error_set(out_err, code, message, message.bytesize) + end + + # Hands a callback's string, bytes, or buffer return to the library: a + # run allocated with {{PREFIX}}_alloc, written to the out slots. The + # library adopts and frees it. + def self.run_return(bytes, out_ptr, out_len) + out_ptr.write_pointer(alloc(bytes.bytesize) { |run| run.put_bytes(0, bytes) }) + out_len.write(:size_t, bytes.bytesize) + nil + end + + # A callback's string return. + def self.string_return(value, out_ptr, out_len) + run_return(string_arg(value), out_ptr, out_len) + end + + # A callback's bytes return. + def self.bytes_return(value, out_ptr, out_len) + run_return(bytes_arg(value), out_ptr, out_len) + end + + # A callback's value-buffer return. + def self.buffer_return(type, value, out_ptr, out_len) + run_return(seal(encode(type, value)).first, out_ptr, out_len) + end + + # A callback's typed-array return, in a run the library adopts. + def self.slice_return(values, kind, out_ptr, out_len) + ary = Array.try_convert(values) + raise TypeError, "expected an Array, got #{values.class}" if ary.nil? + + checked = ary.map { |v| scalar(v, kind) } + run = alloc(checked.length * SIZES.fetch(kind)) do |ptr| + ptr.public_send(:"put_array_of_#{FFI_TYPES.fetch(kind)}", 0, checked) end - release&.free + out_ptr.write_pointer(run) + out_len.write(:size_t, checked.length) + nil end - end - # @api private - # Yields the native pointers of `objects` (NULL for nil), each kept alive - # until the block returns. - def self._wv_pin(*objects, &block) - _wv_pin_from(objects, 0, [], &block) - end + # A callback's optional-scalar return: writes a present value to the + # out slot and returns whether there is one. + def self.opt_return(value, kind, out_value) + return false if value.nil? - # @api private - def self._wv_pin_from(objects, index, pointers, &block) - return yield(*pointers) if index == objects.length + out_value.put(FFI_TYPES.fetch(kind), 0, scalar(value, kind)) + true + end + + # A callback's object return: a fresh strong reference the library + # adopts, or NULL for nil (which the library rejects where the return + # isn't optional). + def self.object_return(value, cls) + return nil if value.nil? + raise TypeError, "expected a #{cls.name}, got #{value.class}" unless value.is_a?(cls) - object = objects[index] - if object.nil? - _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) - else - object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + clone_ref(value) end - end - # @api private - # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, - # 3 cancel tokens, 4 byte runs; -1 reports whether the library counts at - # all). Only libraries built with the `leak-check` feature count. - def self._wv_debug_live(kind) - unless respond_to?(:{{PREFIX}}_debug_live) - begin - attach_function :{{PREFIX}}_debug_live, [:int32], :uint64 - rescue FFI::NotFoundError - raise Error.new(GENERIC_ERROR_CODE, '{{LIBRARY}} does not export {{PREFIX}}_debug_live') + # A {{PREFIX}}_alloc run of `len` bytes, filled by the block (NULL when + # empty). + def self.alloc(len) + return nil if len.zero? + + run = Native.{{PREFIX}}_alloc(len) + raise NoMemoryError, '{{PREFIX}}_alloc failed' if run.null? + + yield run + run + end + + # --- Load-time checks and diagnostics --- + + # Checks, for every top-level module, that the library's contract table + # (`{{PREFIX}}_{module}_contract`) holds each declaration these bindings + # were generated with, as `[id, hash, path]`, with an equal signature + # hash. Declarations the library adds are fine. + def self.check_contract!(tables) + tables.each do |symbol, expected| + begin + Native.attach_function(symbol, [:pointer], :pointer) unless Native.respond_to?(symbol) + rescue FFI::NotFoundError + raise {{MODULE}}::LoadError, "the loaded {{LIBRARY}} library does not export #{symbol}" + end + out_len = FFI::MemoryPointer.new(:size_t) + table = Native.public_send(symbol, out_len) + count = table.null? ? 0 : out_len.read(:size_t) + actual = Array.new(count) do |i| + entry = Native::ContractEntry.new(table + (i * Native::ContractEntry.size)) + [entry[:id], entry[:hash]] + end.to_h + expected.each do |id, hash, path| + found = actual[id] + raise {{MODULE}}::LoadError, "{{LIBRARY}}: #{path} is missing from the library" if found.nil? + next if found == hash + + raise {{MODULE}}::LoadError, "{{LIBRARY}}: #{path} changed since these bindings were generated" + end + end + end + + # Live native resources of one kind (0 objects, 1 callbacks, 2 + # iterators, 3 cancel tokens, 4 byte runs; -1 reports whether the + # library counts at all). Only libraries built with the `leak-check` + # feature count. + def self.debug_live(kind) + unless Native.respond_to?(:{{PREFIX}}_debug_live) + begin + Native.attach_function(:{{PREFIX}}_debug_live, [:int32], :uint64) + rescue FFI::NotFoundError + raise Error, '{{LIBRARY}} does not export {{PREFIX}}_debug_live' + end end + Native.{{PREFIX}}_debug_live(kind) end - {{PREFIX}}_debug_live(kind) end + private_constant :Bridge end diff --git a/crates/weaveffi-cli/src/targets/ruby/tests.rs b/crates/weaveffi-cli/src/targets/ruby/tests.rs index 6d63f14c..1efe94d4 100644 --- a/crates/weaveffi-cli/src/targets/ruby/tests.rs +++ b/crates/weaveffi-cli/src/targets/ruby/tests.rs @@ -1,91 +1,65 @@ -//! Unit tests: render a small API that exercises every C ABI revision 4 -//! shape the Ruby bindings distinguish and assert the key pieces of each -//! contract. +//! Target-specific unit tests: the gem layout and configuration, the +//! runtime's substitutions, Ruby naming and escaping, the private +//! boundary, and packaging. The `kitchen_sink` snapshot pins everything +//! else. -use camino::Utf8Path; -use weaveffi_model::contract::entries; use weaveffi_model::ir::Api; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; use weaveffi_model::validate::validate; use super::{RubyConfig, RubyGenerator}; -use crate::backend::{LanguageBackend, OutputFile}; +use crate::codegen::OutputFile; use crate::package::{ArtifactKind, PackageContext}; use crate::platform::{BinarySet, NativeBinary, Platform}; +use crate::targets::Target; -/// One module with an error domain carrying a field; an interface with a -/// `new` constructor, a method, and a throwing static; nullable objects; an -/// iterator of objects; a cancellable async function; a record carrying -/// objects; a record whose name collides with a composite's codec stem; -/// and a callback interface whose methods take every argument family and -/// return a direct value, an enum, a string, a record, an object, and an -/// optional object, one of them `throws`, passed both required and -/// optional. +/// An interface, a callback interface, a record and an error code with +/// fields named like the methods records and errors rely on, types named +/// like constants the bindings use, members named like wrapper methods, and +/// a cancellable function with a `cancel` parameter. const FIXTURE: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: shop errors: - name: ShopError - codes: - - { name: OutOfStock, code: 1, message: "out of stock", fields: [{ name: sku, type: string }] } - - { name: Closed, code: 2, message: "closed" } - enums: - - name: Mood - variants: [{ name: Happy, value: 0 }, { name: Grumpy, value: 1 }] + - name: Error + codes: + - { name: Broken, code: 1, message: "broken", fields: [{ name: code, type: i32 }, { name: message, type: string }] } structs: - - name: Order + - name: Data fields: - - { name: cart, type: Cart } - - { name: history, type: "[Cart]" } - - { name: note, type: "string?" } - - name: ListI32 - fields: - - { name: items, type: "[i32]" } + - { name: hash, type: i64 } + - { name: end, type: i32 } interfaces: - name: Cart constructors: - - { name: new, params: [{ name: owner, type: string }] } + - { name: allocate, params: [] } methods: - - { name: add, params: [{ name: sku, type: string }], return: bool } - - { name: total, return: i64, deprecated: "use sum" } + - { name: close, params: [], return: bool } + - { name: initialize, params: [] } + - { name: name, params: [], return: string } statics: - - { name: restore, params: [{ name: id, type: i64 }], return: Cart, throws: true } + - { name: new, params: [], return: Cart } + - { name: label, params: [], return: string } callback_interfaces: - name: Watcher methods: - - name: on_event - params: - - { name: name, type: string } - - { name: count, type: i32 } - - { name: order, type: Order } - - { name: cart, type: Cart } - return: bool - - { name: mood, return: Mood } - - { name: label, return: string, throws: true } - - { name: latest, return: "Order?" } - - { name: favorite, return: Cart } - - { name: maybe, return: "Cart?" } - - { name: on_done } + - { name: hash, params: [{ name: end, type: i32 }], return: i64 } functions: - - { name: find_cart, params: [{ name: current, type: "Cart?" }], return: "Cart?" } - - { name: all_carts, return: "iter" } + - { name: name, params: [], return: string } - { name: watch, params: [{ name: watcher, type: Watcher }] } - - { name: maybe_watch, params: [{ name: watcher, type: "Watcher?" }] } - - { name: echo, params: [{ name: text, type: string }], return: string } - - { name: tally, params: [{ name: counts, type: "{string:[i32?]}" }], return: "[Order]" } - - { name: firsts, params: [{ name: lists, type: "[i32]" }], return: ListI32 } - - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } + - { name: wait, params: [{ name: cancel, type: i64 }], return: i64, async: true, cancellable: true } + - { name: fix, params: [{ name: d, type: Data }], return: Data, throws: Error } "#; fn model() -> Model { - let api: Api = serde_yaml::from_str(FIXTURE).unwrap(); + let api: Api = serde_saphyr::from_str(FIXTURE).unwrap(); validate(&api, &Identity::named("shop_kit"), None).unwrap() } fn generate(config: &RubyConfig) -> Vec { - RubyGenerator.files(&model(), Utf8Path::new("out"), config) + RubyGenerator::from(config.clone()).render(&model()) } fn file<'a>(files: &'a [OutputFile], path: &str) -> &'a str { @@ -102,19 +76,7 @@ fn file<'a>(files: &'a [OutputFile], path: &str) -> &'a str { } fn bindings() -> String { - file( - &generate(&RubyConfig::default()), - "out/ruby/lib/shop_kit.rb", - ) - .to_string() -} - -fn runtime() -> String { - file( - &generate(&RubyConfig::default()), - "out/ruby/lib/shop_kit/runtime.rb", - ) - .to_string() + file(&generate(&RubyConfig::default()), "lib/shop_kit.rb").to_string() } #[track_caller] @@ -122,25 +84,6 @@ fn assert_has(src: &str, needle: &str) { assert!(src.contains(needle), "missing {needle:?} in:\n{src}"); } -#[track_caller] -fn assert_lacks(src: &str, needle: &str) { - assert!(!src.contains(needle), "unexpected {needle:?} in:\n{src}"); -} - -/// The text of the generated item that starts at the first line containing -/// `start`, up to its closing `end` at the same indentation. -fn item<'a>(src: &'a str, start: &str) -> &'a str { - let at = src - .find(start) - .unwrap_or_else(|| panic!("no {start:?} in:\n{src}")); - let line_start = src[..at].rfind('\n').map_or(0, |i| i + 1); - let indent = &src[line_start..at]; - let indent = &indent[..indent.len() - indent.trim_start().len()]; - let close = format!("\n{indent}end\n"); - let end = src[at..].find(&close).expect("item has an end") + at + close.len(); - &src[line_start..end] -} - #[test] fn layout_follows_the_identity() { let files = generate(&RubyConfig::default()); @@ -148,17 +91,16 @@ fn layout_follows_the_identity() { assert_eq!( paths, [ - "out/ruby/lib/shop_kit.rb", - "out/ruby/lib/shop_kit/runtime.rb", - "out/ruby/shop_kit.gemspec", - "out/ruby/README.md", + "lib/shop_kit.rb", + "lib/shop_kit/runtime.rb", + "shop_kit.gemspec", + "README.md", ] ); - assert_has( - file(&files, "out/ruby/shop_kit.gemspec"), - "s.name = 'shop_kit'", - ); - let src = file(&files, "out/ruby/lib/shop_kit.rb"); + let spec = file(&files, "shop_kit.gemspec"); + assert_has(spec, "s.name = 'shop_kit'"); + assert_has(spec, "s.required_ruby_version = '>= 3.2'"); + let src = file(&files, "lib/shop_kit.rb"); assert_has(src, "require_relative 'shop_kit/runtime'"); assert_has(src, "module ShopKit\n"); // Nothing but the generated-file banner mentions WeaveFFI. @@ -183,335 +125,81 @@ fn config_overrides_gem_and_module_names() { }; let files = generate(&config); assert_has( - file(&files, "out/ruby/acme-shop.gemspec"), + file(&files, "acme-shop.gemspec"), "s.name = 'acme-shop'", ); // The require path stays the identity prefix. - let src = file(&files, "out/ruby/lib/shop_kit.rb"); - assert_has(src, "module Acme\n"); - assert_has(src, "Acme.shop_kit_shop_Cart_destroy(ptr)"); - assert_has( - file(&files, "out/ruby/lib/shop_kit/runtime.rb"), - "module Acme\n", - ); + assert_has(file(&files, "lib/shop_kit.rb"), "module Acme\n"); + let rt = file(&files, "lib/shop_kit/runtime.rb"); + assert_has(rt, "module Acme\n"); + assert_has(rt, "raise Acme::LoadError,"); } #[test] -fn runtime_speaks_abi_4() { - let rt = runtime(); - assert_has(&rt, "ENV['SHOP_KIT_LIBRARY']"); +fn runtime_substitutes_the_identity_and_abi_revision() { + let rt = file(&generate(&RubyConfig::default()), "lib/shop_kit/runtime.rb").to_string(); + assert_has(&rt, "ENV.fetch('SHOP_KIT_LIBRARY', '')"); assert_has(&rt, "'libshop_kit.dylib'"); assert_has(&rt, "'libshop_kit.so'"); assert_has(&rt, "'shop_kit.dll'"); - assert_has(&rt, &format!("ABI_VERSION = {}", crate::cabi::ABI_VERSION)); - assert_eq!(crate::cabi::ABI_VERSION, 4); - assert_has(&rt, "attach_function :shop_kit_abi_version, [], :uint32"); - assert_has(&rt, "attach_function :shop_kit_alloc, [:size_t], :pointer"); - assert_has( - &rt, - "attach_function :shop_kit_error_set_payload, [:pointer, :pointer, :size_t], :void", - ); + assert_eq!(crate::cabi::ABI_VERSION, 5); + assert_has(&rt, "ABI_VERSION = 5\n"); assert_has( &rt, - "attach_function :shop_kit_free_bytes, [:pointer, :size_t], :void", + "attach_function :shop_kit_error_set, [:pointer, :int32, :pointer, :size_t], :void", ); - assert_lacks(&rt, "dealloc"); - assert_lacks(&rt, "checksum"); - assert_lacks(&rt, "{{"); - // The load errors name the declaration path. - assert_has(&rt, "#{path} is missing from the library"); - assert_has(&rt, "#{path} changed since these bindings were generated"); - // The trap policy: a failure of a call that declares no errors is a - // NativeBugError naming the code and message; -5 is Cancelled. - assert_has(&rt, "class NativeBugError < Error"); - assert_has(&rt, "native call failed with code #{code}: #{message}"); - assert_has(&rt, "class Cancelled < Error"); - assert_has(&rt, "CANCELLED_ERROR_CODE = -5"); + assert!(!rt.contains("{{"), "an unsubstituted placeholder:\n{rt}"); } #[test] -fn load_checks_embed_every_contract_entry() { - let model = model(); +fn only_the_idiomatic_api_is_public() { let src = bindings(); - assert_has( - &src, - " _wv_check_contract!(\n shop_kit_shop_contract: [\n", - ); - let root = model.roots().next().unwrap(); - let table = entries(&model, root); - assert!(table.len() > 10); - for e in &table { - assert_has( - &src, - &format!("[0x{:016x}, 0x{:016x}, '{}'],", e.id, e.hash, e.path), + // Every C function is attached inside the private Native module, and + // no wrapper defines a helper of its own. + let native_start = src.find(" module Native\n").expect("a Native module"); + let native_end = native_start + src[native_start..].find("\n end\n").unwrap(); + for (at, _) in src.match_indices("attach_function") { + assert!( + (native_start..native_end).contains(&at), + "attach_function outside Native:\n{src}" ); } - assert_lacks(&src, "checksum"); -} - -#[test] -fn every_call_but_the_trivial_helpers_releases_the_gvl() { - let src = bindings(); - for line in src.lines().filter(|l| l.contains("attach_function")) { - let trivial = line.contains("_clone,") || line.contains("_destroy,"); - assert_eq!(!trivial, line.ends_with(", blocking: true"), "{line}"); - } - assert_has( - &src, - "attach_function :shop_kit_shop_echo, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true", - ); - assert_has( - &src, - "attach_function :shop_kit_shop_AllCartsIterator_destroy, [:pointer], :void\n", - ); -} - -#[test] -fn errors_follow_the_trap_policy() { - let src = bindings(); - // A throwing call raises the domain hierarchy; any other call traps. - assert_has( - item(&src, "def self.restore(id)"), - "ShopKit._wv_check_shop_error!(err)", - ); - assert_has(item(&src, "def self.echo(text)"), "_wv_check!(err)"); - assert_has(&src, "queue << _wv_trap(*taken)"); - assert_has(&src, "class ShopError < Error"); - assert_has(&src, "class OutOfStock < ShopError"); - assert_has(&src, "def initialize(message = nil, sku: nil)"); - assert_has(&src, "super(CODE, message || 'out of stock')"); - assert_has( - &src, - "ShopError::OutOfStock.new(message, sku: r.read_string)", - ); -} - -#[test] -fn vtables_carry_the_header_and_every_return_family() { - let src = bindings(); - assert_has( - &src, - "layout :wv_size, :uint32,\n :wv_flags, :uint32,\n :wv_free, :pointer,\n :on_event, :pointer,", - ); - assert_has(&src, "WV_WATCHER_VTABLE[:wv_size] = WvWatcherVtable.size"); - assert_has(&src, "WV_WATCHER_VTABLE[:wv_flags] = 0"); - assert_has(&src, "WV_WATCHER_VTABLE[:wv_free] = WV_WATCHER_FREE"); - assert_eq!(src.matches("= WvWatcherVtable.new").count(), 1); - - // Direct: by value, coerced inside the rescue. - let on_event = item(&src, "WV_WATCHER_ON_EVENT = "); - assert_has(on_event, "FFI::Function.new(:bool, [:pointer, :pointer, :size_t, :int32, :pointer, :size_t, :pointer, :pointer])"); - assert_has( - on_event, - "do |wv_ctx, name_ptr, name_len, count, order_ptr, order_len, cart_ptr, wv_err|", - ); - // Objects are adopted before anything that can raise. - let adopt = on_event.find("cart = Cart._from_ptr(cart_ptr)").unwrap(); - assert!(adopt < on_event.find("name = _wv_borrow_string").unwrap()); - assert!(adopt < on_event.find("order = _wv_decode_borrowed").unwrap()); - assert_has( - on_event, - "_wv_cb_lookup(wv_ctx).on_event(name, count, order, cart) ? true : false", - ); - assert_has(on_event, " false\n"); - assert_has( - item(&src, "WV_WATCHER_MOOD = "), - "_wv_int(_wv_cb_lookup(wv_ctx).mood, :i32)", - ); - // String: a {prefix}_alloc run in the out slots. - let label = item(&src, "WV_WATCHER_LABEL = "); - assert_has(label, "do |wv_ctx, wv_out_ptr, wv_out_len, wv_err|"); - assert_has( - label, - "_wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_str(_wv_cb_lookup(wv_ctx).label))", - ); - // A record carrying objects: its references are minted by the seal. - let latest = item(&src, "WV_WATCHER_LATEST = "); - assert_has(latest, "_wv_write_opt_order(writer, result)"); - assert_has( - latest, - "_wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_seal(writer).first)", - ); - // Objects: a fresh reference the producer adopts (NULL for nil). - let favorite = item(&src, "WV_WATCHER_FAVORITE = "); - assert_has( - favorite, - "FFI::Function.new(:pointer, [:pointer, :pointer])", - ); - assert_has( - favorite, - "_wv_cb_return_object(_wv_cb_lookup(wv_ctx).favorite, Cart)", - ); - assert_has(favorite, "FFI::Pointer::NULL\n"); - assert_has( - item(&src, "WV_WATCHER_MAYBE = "), - "_wv_cb_return_object(_wv_cb_lookup(wv_ctx).maybe, Cart)", - ); - assert_has(&runtime(), "def self._wv_cb_return_object(value, cls)"); -} - -#[test] -fn throwing_callback_methods_report_the_domain_error() { - let src = bindings(); - let label = item(&src, "WV_WATCHER_LABEL = "); - assert_has(label, "rescue ShopError => e"); - assert_has( - label, - "_wv_cb_throw(wv_err, e) { _wv_shop_error_payload(e) }", - ); - // Every other exception is -4, after the domain rescue. - assert!(label.find("rescue ShopError").unwrap() < label.find("rescue Exception").unwrap()); - assert_has(label, "_wv_cb_fail(wv_err, e)"); - // A method without `throws` never reports a domain code. - assert_lacks(item(&src, "WV_WATCHER_MOOD = "), "rescue ShopError"); - // The payload encoder writes each code's fields. - let payload = item(&src, "def self._wv_shop_error_payload(error)"); - assert_has(payload, "when ShopError::OutOfStock\n"); - assert_has(payload, "w.write_string(error.sku)"); - assert_has(payload, "when ShopError::Closed then 2"); - assert_has(payload, "code && [code, w.bytes]"); - let rt = runtime(); - assert_has( - &rt, - "shop_kit_error_set_payload(out_err, payload, payload.bytesize)", - ); - assert_has( - &rt, - "shop_kit_error_set(out_err, FOREIGN_ERROR_CODE, _wv_cb_message(exception))", - ); -} - -#[test] -fn callback_parameters_register_their_implementation() { - let src = bindings(); - let watch = item(&src, "def self.watch(watcher)"); - assert_has(watch, "_wv_present!(watcher, 'watcher')"); - assert_has( - watch, - "watcher_ctx, watcher_vtable = _wv_cb_register(watcher, WV_WATCHER_VTABLE)", - ); - assert_has( - watch, - "shop_kit_shop_watch(watcher_ctx, watcher_vtable, err)", - ); - // An optional callback defaults to nil, which passes two NULLs. - let maybe = item(&src, "def self.maybe_watch(watcher = nil)"); - assert_lacks(maybe, "_wv_present!"); - assert_has( - maybe, - "watcher_ctx, watcher_vtable = _wv_cb_register(watcher, WV_WATCHER_VTABLE)", - ); - assert_has(&runtime(), "return [nil, nil] if impl.nil?"); -} - -#[test] -fn objects_are_checked_pinned_and_adopted() { - let src = bindings(); - assert_has(&src, "class CartPtr < FFI::AutoPointer"); - assert_has(&src, "class Cart < WvObject\n WV_PTR = CartPtr\n"); - assert_has(&src, "ShopKit.shop_kit_shop_Cart_clone(ptr)"); - let find = item(&src, "def self.find_cart(current = nil)"); - assert_has(find, "_wv_object!(current, Cart, 'current', true)"); - assert_has(find, "_wv_pin(current) do |_wv_current|"); - assert_has(find, "result.null? ? nil : Cart._from_ptr(result)"); - let add = item(&src, "def add(sku)"); - assert_has(add, "ShopKit._wv_pin(self) do |_wv_self|"); - let init = item(&src, "def initialize(owner)"); - assert_has(init, "_wv_init(ShopKit._wv_nonnull(result))"); - assert_has( - item(&src, "def self.restore(id)"), - "_from_ptr(ShopKit._wv_nonnull(result))", - ); - assert_has( - item(&src, "def total"), - "warn('ShopKit::Cart#total is deprecated: use sum', uplevel: 1)", + assert!(!src.contains("def self._wv"), "a public helper:\n{src}"); + assert!( + !src.contains("FFI::Struct"), + "a per-interface struct:\n{src}" ); } #[test] -fn composite_types_get_one_codec_pair_each() { +fn names_never_replace_what_the_bindings_rely_on() { let src = bindings(); - for stem in [ - "map_string_list_opt_i32", - "list_opt_i32", - "opt_i32", - "list_order", - "list_cart", - "opt_string", - "opt_order", - "list_i32_2", + for needle in [ + // Types named like constants the bindings use. + "class Error_ < Error\n", + "Data_ = Data.define(:hash_, :end)\n", + "Bridge.record(Data_, hash_: :i64, end: :i32)\n", + // Error fields named like Error's own methods. + "attr_reader :code_\n", + "attr_reader :message_\n", + "Bridge.record(Error_::Broken, code_: :i32, message_: :string)\n", + // Members named like the wrapper's or the class's own methods. + "def self.allocate_\n", + "def close_\n", + "def initialize_\n", + "def name\n", + "def self.new_\n", + "def self.label\n", + // A module function named like Module#name. + "def self.name_\n", + // A callback method named like Object#hash. + "def hash_(end_)\n", + ".hash_(end_)", + // A parameter named like the cancellable keyword. + "def self.wait(cancel_, cancel: nil)\n", ] { - assert_eq!( - src.matches(&format!("def self._wv_write_{stem}(w, v)")) - .count(), - 1, - "{stem}" - ); + assert_has(&src, needle); } - // The record `ListI32` owns the `list_i32` stem, so the `[i32]` - // composite takes the next free one. - assert_has( - &src, - "# Packs a ListI32 into the value-buffer wire format.\n def self._wv_write_list_i32(w, v)", - ); - assert_has( - &src, - "# Packs `[i32]` into the value-buffer wire format.\n def self._wv_write_list_i32_2(w, v)", - ); - assert_has(&src, "items: _wv_read_list_i32_2(r)"); - assert_has( - &src, - "counts_s = _wv_encode { |w| _wv_write_map_string_list_opt_i32(w, counts) }", - ); - assert_has( - &src, - "_wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_list_order(r) }", - ); - let map = item(&src, "def self._wv_write_map_string_list_opt_i32(w, v)"); - assert_has(map, "w.write_len(v.length)"); - assert_has(map, "v.each do |k, e|"); - assert_has(map, "_wv_write_list_opt_i32(w, e)"); - assert_has( - &src, - "Array.new(r.read_len) { [r.read_string, _wv_read_list_opt_i32(r)] }.to_h", - ); - assert_has(&src, "r.read_flag ? r.read_i32 : nil"); - // Records carrying objects reserve tokens and seal them before the call. - assert_has(&src, "w.write_object(v.cart, Cart)"); - assert_has(&src, "cart: r.read_object(Cart)"); -} - -#[test] -fn iterator_is_a_lazy_enumerator_adopting_objects() { - let src = bindings(); - let all = item(&src, "def self.all_carts"); - assert_has(all, "Enumerator.new do |y|"); - assert_has( - all, - "has_item = shop_kit_shop_AllCartsIterator_next(iter, out_item, err)", - ); - assert_has(all, "value = Cart._from_ptr(_wv_nonnull(item))"); - assert_has( - all, - "shop_kit_shop_AllCartsIterator_destroy(iter) unless iter.null?", - ); -} - -#[test] -fn cancellable_async_takes_a_cancel_token() { - let src = bindings(); - let wait = item(&src, "def self.wait(ms, cancel: nil)"); - assert_has(wait, "own_token = CancelToken.new if cancel.nil?"); - assert_has( - wait, - "shop_kit_shop_wait(ms, token._wv_ptr, SHOP_KIT_SHOP_WAIT_CALLBACK, ctx)", - ); - assert_has(wait, "_wv_async_wait(queue, token)"); - assert_has(wait, "own_token&.close"); - assert_has( - &src, - "SHOP_KIT_SHOP_WAIT_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :int64]) do |ctx, err, result|", - ); } #[test] @@ -531,8 +219,8 @@ fn package_skips_platforms_without_a_gem_string() { binaries.insert(NativeBinary::new(platform, path)); } let ctx = PackageContext::new(&binaries); - let artifacts = RubyGenerator - .package(&model, &ctx, &RubyConfig::default()) + let artifacts = RubyGenerator::from(RubyConfig::default()) + .package(&model, &ctx) .expect("ruby supports packaging"); assert_eq!(artifacts.len(), 1, "{artifacts:?}"); let gem = &artifacts[0]; @@ -541,9 +229,10 @@ fn package_skips_platforms_without_a_gem_string() { panic!("a gem"); }; assert_eq!(spec.platform, "arm64-darwin"); + assert_eq!(spec.required_ruby_version, ">= 3.2"); assert_eq!( spec.dependencies, - [("ffi".to_string(), "~> 1.15".to_string())] + [("ffi".to_string(), "~> 1.16".to_string())] ); let paths: Vec<&str> = gem.files.iter().map(|f| f.path.as_str()).collect(); assert_eq!( @@ -556,40 +245,3 @@ fn package_skips_platforms_without_a_gem_string() { ] ); } - -#[test] -fn members_never_replace_the_wrapper_methods() { - let yaml = r#" -version: "0.11.0" -modules: - - name: kv - interfaces: - - name: Store - constructors: - - { name: allocate, params: [] } - methods: - - { name: close, params: [], return: bool } - - { name: handle, params: [], return: i64 } - - { name: initialize, params: [] } - - { name: name, params: [], return: string } - statics: - - { name: hash, params: [], return: i64 } - - { name: label, params: [], return: string } -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let model = validate(&api, &Identity::named("kv"), None).unwrap(); - let files = RubyGenerator.files(&model, Utf8Path::new("out"), &RubyConfig::default()); - let rb = file(&files, "out/ruby/lib/kv.rb"); - for def in [ - "def self.allocate_", - "def close_", - "def handle_", - "def initialize_", - "def name\n", - "def self.hash\n", - "def self.label\n", - ] { - assert!(rb.contains(def), "missing `{def}` in:\n{rb}"); - } - assert!(!rb.contains("def close\n") && !rb.contains("def handle\n")); -} diff --git a/crates/weaveffi-cli/src/targets/ruby/types.rs b/crates/weaveffi-cli/src/targets/ruby/types.rs index a3f62370..265d29ac 100644 --- a/crates/weaveffi-cli/src/targets/ruby/types.rs +++ b/crates/weaveffi-cli/src/targets/ruby/types.rs @@ -1,67 +1,225 @@ -//! Ruby type mapping and naming: the FFI-gem vocabulary for ABI slots, the -//! identifier policy for user-chosen IDL names, and string escaping for Ruby -//! literals. +//! Ruby naming and the value descriptors the generated code hands the +//! runtime: the ffi gem's C type symbols, the *kinds* that say how a return, +//! an async result, or an iterator element crosses, the wire *types* that +//! say how a value is packed into a value buffer, and the YARD type of each +//! value. -use crate::lang::{escape_ident, RUBY_KEYWORDS}; +use crate::lang::{escape_ident, escape_member, RUBY_KEYWORDS}; use heck::ToSnakeCase; use weaveffi_model::abi::{AbiParam, CType}; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::errors::type_name; +use weaveffi_model::plan::{ErrorStrategy, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty, WireType}; + +/// Constants the runtime and the generated code refer to inside the gem's +/// module (its own classes, Ruby core classes, and the ffi gem). A user +/// type with one of these names would shadow it, so it gains a trailing `_`. +const RESERVED_CONSTANTS: &[&str] = &[ + "ABI_VERSION", + "ArgumentError", + "Array", + "Bridge", + "CancelToken", + "Cancelled", + "Class", + "Comparable", + "Data", + "ENV", + "Encoding", + "EncodingError", + "Enumerator", + "Error", + "Exception", + "FFI", + "File", + "Float", + "Hash", + "Integer", + "Kernel", + "LoadError", + "Module", + "Monitor", + "Mutex", + "Native", + "NativeBugError", + "NoMemoryError", + "NotImplementedError", + "Numeric", + "Object", + "RangeError", + "StandardError", + "String", + "Struct", + "Symbol", + "Thread", + "TypeError", +]; + +/// The Ruby constant naming a user type (record, enum, interface, callback +/// interface, error domain, or variant): the IDL name, with a trailing `_` +/// when it would shadow a constant the bindings rely on (`Error`, `Data`, +/// `FFI`, ...). +pub(crate) fn rb_const(name: &str) -> String { + if RESERVED_CONSTANTS.contains(&name) { + format!("{name}_") + } else { + name.to_string() + } +} + +/// The Ruby class of an error domain: the shared type name +/// ([`type_name`], so `KitchenErrors` is `KitchenError`), escaped like any +/// other constant. +pub(crate) fn rb_domain(name: &str) -> String { + rb_const(&type_name(name, "Error")) +} /// The Ruby spelling of a user-chosen parameter name: snake_case via heck, /// then keyword-escaped (a reserved name like `end` gains a trailing `_`). -/// IDL names are usually already snake, so the case conversion is a safety -/// net for camelCase inputs. pub(crate) fn rb_param_name(name: &str) -> String { escape_ident(&name.to_snake_case(), RUBY_KEYWORDS) } -/// The Ruby spelling of a user-chosen field name: verbatim unless it's a -/// Ruby reserved word, in which case it gains a trailing `_` so keyword -/// arguments and locals derived from it stay parseable. +/// The Ruby local holding one C slot of a call: the slot's name, keyword +/// escaped (a receiver's `self` slot is `self_`). Slot names are unique per +/// callable (validated), so these locals never collide. +pub(crate) fn rb_slot(slot: &AbiParam) -> String { + escape_ident(&slot.name, RUBY_KEYWORDS) +} + +/// Methods every record (a `Data` class) relies on, from `Data` and +/// `Object`. A field with one of these names gains a trailing `_`. +const RECORD_METHODS: &[&str] = &[ + "class", + "clone", + "deconstruct", + "deconstruct_keys", + "display", + "dup", + "extend", + "freeze", + "hash", + "initialize", + "inspect", + "itself", + "members", + "method", + "methods", + "object_id", + "public_send", + "send", + "singleton_class", + "tap", + "then", + "to_h", + "to_s", + "with", +]; + +/// Methods every error class relies on, from `Exception` and the library's +/// `Error`. An error code's field with one of these names gains a +/// trailing `_`. +const ERROR_METHODS: &[&str] = &[ + "backtrace", + "backtrace_locations", + "cause", + "class", + "code", + "detailed_message", + "exception", + "full_message", + "hash", + "inspect", + "message", + "object_id", + "send", + "set_backtrace", + "to_s", +]; + +/// The Ruby name of a record or rich-enum variant field (a `Data` member): +/// verbatim, with a trailing `_` when it would replace a method the record +/// relies on. Keywords are fine as member names (`entry.end`). pub(crate) fn rb_field_name(name: &str) -> String { - escape_ident(name, RUBY_KEYWORDS) + escape_member(name, RECORD_METHODS) +} + +/// The Ruby name of an error code's payload field: verbatim, with a +/// trailing `_` when it would replace a method the error relies on +/// (`code`, `message`, ...). +pub(crate) fn rb_error_field_name(name: &str) -> String { + escape_member(name, ERROR_METHODS) } /// Instance methods every interface wrapper defines or relies on: the -/// runtime's `WvObject` (`close`, `handle`, `initialize_copy`), the -/// constructor hook `initialize`, and the `Object` methods the runtime and -/// Ruby's own object model call (`class`, `dup`, `clone`, `hash`, ...). -/// The runtime's private helpers start with `_wv_`, which a snake-cased IDL -/// name never does. +/// runtime's `Handle` (`close`, `==`, `hash`, `inspect`, `initialize_copy`), +/// the constructor hook `initialize`, and the `Object` methods Ruby's own +/// object model calls. The runtime's private helpers start with `_wv_`, +/// which a snake-cased IDL name never does. const OBJECT_METHODS: &[&str] = &[ "class", "clone", "close", + "display", "dup", "freeze", - "handle", "hash", "initialize", "initialize_copy", + "inspect", + "method", "object_id", "send", + "to_s", ]; -/// Class methods every interface wrapper class relies on: `allocate` -/// (adopting a returned object) and `name` (error messages). -const CLASS_METHODS: &[&str] = &["allocate", "name"]; +/// Class and module methods the bindings rely on: `allocate` and `new` +/// (constructing wrappers), `name` (error messages and `inspect`), and the +/// `Module` reflection the runtime uses on the gem's module. +const MODULE_METHODS: &[&str] = &[ + "allocate", + "ancestors", + "class", + "const_get", + "constants", + "freeze", + "hash", + "inspect", + "name", + "new", + "object_id", + "send", + "to_s", +]; /// The Ruby spelling of an interface member: snake_case, with a trailing /// `_` when it would replace a method the wrapper relies on, an instance -/// method for a `method`, else a class method (a static or factory). -/// Ruby allows keywords as method names, so no keyword escape applies. +/// method for a `method`, else a class method (a static or factory). Ruby +/// allows keywords as method names, so no keyword escape applies. pub(crate) fn rb_member_name(name: &str, method: bool) -> String { let members = if method { OBJECT_METHODS } else { - CLASS_METHODS + MODULE_METHODS }; - crate::lang::escape_member(&name.to_snake_case(), members) + escape_member(&name.to_snake_case(), members) +} + +/// The Ruby spelling of a module-level function (a singleton method of the +/// gem's module). +pub(crate) fn rb_function_name(name: &str) -> String { + escape_member(&name.to_snake_case(), MODULE_METHODS) } -/// Maps a shared ABI [`CType`] onto its Ruby FFI type symbol. Every pointer -/// (strings and buffers cross as `const uint8_t*`, objects and out-slots as -/// typed pointers) is `:pointer`; `bool` is the 1-byte C `bool`. +/// The Ruby spelling of a callback method, which an implementation defines +/// and the trampoline calls. +pub(crate) fn rb_callback_method_name(name: &str) -> String { + escape_member(&name.to_snake_case(), OBJECT_METHODS) +} + +/// The ffi gem's symbol for a C slot type. Every pointer (runs, objects, +/// out slots, vtables, contexts) is `:pointer`; `bool` is the 1-byte C +/// `bool`; a C-style enum is its `int32_t` typedef. pub(crate) fn rb_ffi_type(ty: &CType) -> &'static str { match ty { CType::Bool => ":bool", @@ -81,30 +239,130 @@ pub(crate) fn rb_ffi_type(ty: &CType) -> &'static str { } } -/// Map lowered ABI slots onto Ruby FFI type symbols. -pub(crate) fn rb_abi_types(params: &[AbiParam]) -> Vec { - params - .iter() - .map(|p| rb_ffi_type(&p.ty).to_string()) - .collect() +/// The ffi gem's symbols for a list of slots, as a Ruby array literal. +pub(crate) fn rb_ffi_types(params: &[AbiParam]) -> String { + let types: Vec<&str> = params.iter().map(|p| rb_ffi_type(&p.ty)).collect(); + format!("[{}]", types.join(", ")) } -/// The `FFI::MemoryPointer` type of a direct iterator element out-slot, read -/// back with `Pointer#read`. -pub(crate) fn rb_direct_type(ty: &Ty) -> &'static str { +/// The scalar kind of a direct value (`:i32`, `:f64`, `:bool`); a C-style +/// enum crosses as `:i32`. +pub(crate) fn rb_scalar(ty: &Ty) -> String { match ty { - Ty::Prim(Prim::Bool) => ":bool", - Ty::Prim(Prim::I8) => ":int8", - Ty::Prim(Prim::I16) => ":int16", - Ty::Prim(Prim::I32) | Ty::Enum(_) => ":int32", - Ty::Prim(Prim::U8) => ":uint8", - Ty::Prim(Prim::U16) => ":uint16", - Ty::Prim(Prim::U32) => ":uint32", - Ty::Prim(Prim::I64) => ":int64", - Ty::Prim(Prim::U64) => ":uint64", - Ty::Prim(Prim::F32) => ":float", - Ty::Prim(Prim::F64) => ":double", - other => unreachable!("{other} is not a direct type"), + Ty::Prim(p) => format!(":{}", p.snake()), + Ty::Enum(_) => ":i32".to_string(), + other => unreachable!("{other} isn't a scalar"), + } +} + +/// The scalar kind of a typed-array element. +pub(crate) fn rb_elem(p: Prim) -> String { + format!(":{}", p.snake()) +} + +/// The scalar a range check applies to, when `ty` is an integer or a +/// C-style enum (floats and bools are converted by the ffi gem itself). +pub(crate) fn rb_checked_int(ty: &Ty) -> Option { + match ty { + Ty::Prim(p) if p.is_integer() => Some(rb_scalar(ty)), + Ty::Enum(_) => Some(rb_scalar(ty)), + _ => None, + } +} + +/// The inner type of an optional. +fn optional_inner(ty: &Ty) -> &Ty { + match ty { + Ty::Optional(inner) => inner, + other => unreachable!("{other} isn't optional"), + } +} + +/// The wire type of a value inside a value buffer: `:i64`, `:string`, +/// `[:opt, Item]`, `[:list, :string]`, `[:map, :string, :i32]`, or the class +/// of a record, rich enum, or interface. A C-style enum is `:i32`. +pub(crate) fn rb_wire(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(p) => format!(":{}", p.snake()), + WireType::Enum(_) => ":i32".to_string(), + WireType::Object(n) | WireType::User(n) => rb_const(n), + WireType::Optional(inner) => format!("[:opt, {}]", rb_wire(inner)), + WireType::List(inner) => format!("[:list, {}]", rb_wire(inner)), + WireType::Map(k, v) => format!("[:map, {}, {}]", rb_wire(k), rb_wire(v)), + } +} + +/// The kind of an object: `[:object, Store]` or `[:object?, Store]`. +fn object_kind(interface: &str, nullable: bool) -> String { + let tag = if nullable { ":object?" } else { ":object" }; + format!("[{tag}, {}]", rb_const(interface)) +} + +/// The kind of a synchronous return (see the runtime's `Bridge` for the +/// vocabulary), or `None` for a void return. `ret` is the returned value +/// type. An iterator has no kind; its element does (see [`rb_item_kind`]). +pub(crate) fn rb_return_kind(pass: &RetPass, ret: Option<&Ty>) -> Option { + let ty = || ret.expect("a value return has a type"); + Some(match pass { + RetPass::Void | RetPass::Iterator(_) => return None, + RetPass::Direct => rb_scalar(ty()), + RetPass::OptDirect { .. } => format!("[:opt, {}]", rb_scalar(optional_inner(ty()))), + RetPass::Slice { elem, .. } => format!("[:slice, {}]", rb_elem(*elem)), + RetPass::String { .. } => ":string".to_string(), + RetPass::Bytes { .. } => ":bytes".to_string(), + RetPass::Buffer { .. } => format!("[:buffer, {}]", rb_wire(ty())), + RetPass::Object { + nullable, + interface, + .. + } => object_kind(interface, *nullable), + }) +} + +/// The kind of an async result, `nil` for void. +pub(crate) fn rb_result_kind(pass: &ResultPass, ret: Option<&Ty>) -> String { + let ty = || ret.expect("a value result has a type"); + match pass { + ResultPass::Void => "nil".to_string(), + ResultPass::Direct { .. } => rb_scalar(ty()), + ResultPass::OptDirect { .. } => format!("[:opt, {}]", rb_scalar(optional_inner(ty()))), + ResultPass::Slice { elem, .. } => format!("[:slice, {}]", rb_elem(*elem)), + ResultPass::String { .. } => ":string".to_string(), + ResultPass::Bytes { .. } => ":bytes".to_string(), + ResultPass::Buffer { .. } => format!("[:buffer, {}]", rb_wire(ty())), + ResultPass::Object { + nullable, + interface, + .. + } => object_kind(interface, *nullable), + } +} + +/// The kind of an iterator element of type `elem`. +pub(crate) fn rb_item_kind(pass: &ItemPass, elem: &Ty) -> String { + match pass { + ItemPass::Direct { .. } => rb_scalar(elem), + ItemPass::OptDirect { .. } => format!("[:opt, {}]", rb_scalar(optional_inner(elem))), + ItemPass::Slice { elem: p, .. } => format!("[:slice, {}]", rb_elem(*p)), + ItemPass::String { .. } => ":string".to_string(), + ItemPass::Bytes { .. } => ":bytes".to_string(), + ItemPass::Buffer { .. } => format!("[:buffer, {}]", rb_wire(elem)), + ItemPass::Object { + nullable, + interface, + .. + } => object_kind(interface, *nullable), + } +} + +/// The error class a call's failures are raised through: `nil` for a call +/// that declares no errors (a failure is a `NativeBugError`), `Error` for +/// `throws: any`, or the domain's class. +pub(crate) fn rb_error_arg(error: &ErrorStrategy) -> String { + match error { + ErrorStrategy::Trap => "nil".to_string(), + ErrorStrategy::Untyped => "Error".to_string(), + ErrorStrategy::Domain(name) => rb_domain(name), } } @@ -123,11 +381,35 @@ pub(crate) fn rb_doc_type(ty: &Ty) -> String { Ty::Prim(Prim::F32 | Prim::F64) => "Float".to_string(), Ty::Prim(Prim::String | Prim::Bytes) => "String".to_string(), Ty::Prim(_) | Ty::Enum(_) => "Integer".to_string(), - Ty::Record(n) | Ty::RichEnum(n) | Ty::Interface(n) | Ty::CallbackInterface(n) => n.clone(), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Interface(n) => rb_const(n), Ty::Optional(inner) => format!("{}, nil", rb_doc_type(inner)), Ty::List(inner) => format!("Array<{}>", rb_doc_type(inner)), Ty::Map(k, v) => format!("Hash{{{} => {}}}", rb_doc_type(k), rb_doc_type(v)), - Ty::Iterator(inner) => format!("Enumerator<{}>", rb_doc_type(inner)), + } +} + +/// The YARD type of a parameter: a value's, or a callback interface's +/// (`Listener`, `Listener, nil`). +pub(crate) fn rb_param_doc_type(ty: &ParamTy) -> String { + match ty { + ParamTy::Value(ty) => rb_doc_type(ty), + ParamTy::Callback { name, nullable } => { + let name = rb_const(name); + if *nullable { + format!("{name}, nil") + } else { + name + } + } + } +} + +/// The YARD type of a return: a value's, or an iterator's +/// (`Enumerator`). +pub(crate) fn rb_ret_doc_type(ty: &RetTy) -> String { + match ty { + RetTy::Value(ty) => rb_doc_type(ty), + RetTy::Iterator(elem) => format!("Enumerator<{}>", rb_doc_type(elem)), } } @@ -141,6 +423,40 @@ mod tests { assert_eq!(rb_member_name("initialize", true), "initialize_"); assert_eq!(rb_member_name("name", true), "name"); assert_eq!(rb_member_name("name", false), "name_"); + assert_eq!(rb_member_name("new", false), "new_"); assert_eq!(rb_member_name("close", false), "close"); } + + #[test] + fn fields_avoid_the_methods_records_and_errors_rely_on() { + assert_eq!(rb_field_name("end"), "end"); + assert_eq!(rb_field_name("hash"), "hash_"); + assert_eq!(rb_field_name("with"), "with_"); + assert_eq!(rb_field_name("message"), "message"); + assert_eq!(rb_error_field_name("message"), "message_"); + assert_eq!(rb_error_field_name("code"), "code_"); + assert_eq!(rb_error_field_name("key"), "key"); + } + + #[test] + fn type_names_never_shadow_what_the_bindings_use() { + assert_eq!(rb_const("Data"), "Data_"); + assert_eq!(rb_const("Error"), "Error_"); + assert_eq!(rb_const("Store"), "Store"); + assert_eq!(rb_domain("Error"), "Error_"); + assert_eq!(rb_domain("KitchenErrors"), "KitchenError"); + assert_eq!(rb_domain("Failure"), "FailureError"); + } + + #[test] + fn wire_types_nest() { + let ty = Ty::Map( + Box::new(Ty::Prim(Prim::String)), + Box::new(Ty::List(Box::new(Ty::Optional(Box::new(Ty::Record( + "Data".into(), + )))))), + ); + assert_eq!(rb_wire(&ty), "[:map, :string, [:list, [:opt, Data_]]]"); + assert_eq!(rb_wire(&Ty::Enum("Color".into())), ":i32"); + } } diff --git a/crates/weaveffi-cli/src/targets/swift/callbacks.rs b/crates/weaveffi-cli/src/targets/swift/callbacks.rs index 39a129cb..e7d7ed6c 100644 --- a/crates/weaveffi-cli/src/targets/swift/callbacks.rs +++ b/crates/weaveffi-cli/src/targets/swift/callbacks.rs @@ -2,27 +2,28 @@ //! and the process-wide vtable of `@convention(c)` trampolines the library //! calls through. //! -//! Every clause of [`CallbackProtocol`] is rendered here: one static vtable -//! per interface with the `{size, flags, free}` header, a context that -//! retains the implementation (released by `free`), arguments received per -//! [`RetPass`] (borrowed strings, bytes, and buffers are copied or decoded; -//! object arguments are adopted), returns handed back per family (a direct -//! value by value, an object as a fresh reference, a string, bytes, or -//! buffer as a `{prefix}_alloc` run in the out slots), and failures reported -//! through `out_err` instead of unwinding through the C frame: a method -//! declared `throws` reports its module's domain error with its code and -//! payload, and any other error is a callback failure (`-4`). +//! One static vtable per interface carries the `{size, flags, free}` header +//! (`flags` is 0: any thread may call), a context that retains the +//! implementation (released by `free`), and one trampoline per method. +//! Arguments arrive per the method's [`ArgPass`]es (borrowed strings, bytes, +//! typed arrays, and buffers are copied; objects are adopted), results go +//! back per its [`CallbackRetPass`] (a scalar by value, an optional scalar as +//! a presence flag plus out slot, an object as a fresh reference, and a +//! string, bytes, typed array, or buffer as a `{prefix}_alloc` run in the out +//! slots), and failures are reported through `out_err` instead of unwinding +//! through the C frame, per its [`ErrorStrategy`]. use crate::cabi::c_param_name; -use crate::codegen::common::DocCommentStyle; +use crate::codegen::common::{wrap, DocCommentStyle}; use crate::codegen::CodeWriter; use crate::lang; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding}; -use weaveffi_model::plan::{CallbackProtocol, ErrorStrategy, RetPass}; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; use weaveffi_model::ty::{Prim, Ty}; -use crate::targets::swift::docs::emit_fn_doc; -use crate::targets::swift::types::{callback_vtable_name, deprecated_attr, swift_ident, SwiftCtx}; +use crate::targets::swift::calls::{receive, Recv}; +use crate::targets::swift::docs::{emit_decl_doc, emit_fn_doc}; +use crate::targets::swift::types::{callback_vtable_name, swift_ident, SwiftCtx}; /// The Swift spelling of one trampoline closure formal: the ABI slot name, /// keyword-escaped (a param named `in` yields a slot named `in`, which can't @@ -31,109 +32,113 @@ fn slot_ident(name: &str) -> String { lang::escape_ident(name, lang::SWIFT_KEYWORDS) } -/// Clone a callback method with its parameter names camel-cased and -/// keyword-escaped, so the protocol requirement's argument labels and the -/// trampoline's call site agree. The ABI slot names are left untouched. -fn camel_method(m: &CallbackMethodBinding) -> CallbackMethodBinding { - let mut m = m.clone(); - for p in &mut m.params { - p.name = swift_ident(&p.name); - } - m +/// The expression handing one trampoline argument to the implementation: +/// borrowed strings, bytes, typed arrays, and buffers are copied before the +/// implementation runs (the library owns them only for the call); object +/// arguments carry one strong reference the new wrapper adopts. +fn receive_arg(ty: &Ty, pass: &ArgPass, ctx: &SwiftCtx) -> String { + let (recv, raw, len) = match pass { + ArgPass::Direct { slot } => (Recv::Direct, slot_ident(&slot.name), String::new()), + ArgPass::OptDirect { has, value, .. } => { + let has = slot_ident(&has.name); + let value = slot_ident(&value.name); + return receive(ty, &Recv::Opt(&has), &value, "", false, ctx); + } + ArgPass::Slice { ptr, len, .. } => { + (Recv::Slice, slot_ident(&ptr.name), slot_ident(&len.name)) + } + ArgPass::String { ptr, len } => { + (Recv::String, slot_ident(&ptr.name), slot_ident(&len.name)) + } + ArgPass::Bytes { ptr, len } => (Recv::Bytes, slot_ident(&ptr.name), slot_ident(&len.name)), + ArgPass::Buffer { ptr, len } => { + (Recv::Buffer, slot_ident(&ptr.name), slot_ident(&len.name)) + } + ArgPass::Object { + slot, + nullable, + interface, + } => { + let recv = Recv::Object { + nullable: *nullable, + interface, + }; + return receive(ty, &recv, &slot_ident(&slot.name), "", true, ctx); + } + ArgPass::Callback { .. } => unreachable!("a callback method never takes a callback"), + }; + receive(ty, &recv, &raw, &len, false, ctx) } -/// The expression handing one trampoline argument to the implementation, -/// received per its [`RetPass`]: borrowed strings, bytes, and buffers are -/// copied or decoded before the implementation runs (the library owns them -/// only for the call); object arguments carry one strong reference the new -/// wrapper adopts. -fn receive_arg(p: &ParamBinding, rp: &RetPass, ctx: &SwiftCtx) -> String { - let n0 = slot_ident(&p.abi[0].name); - let n1 = || slot_ident(&p.abi[1].name); - match rp { - RetPass::Direct => match &p.ty { - Ty::Enum(name) => format!( - "wvEnumCase({}.self, numericCast({n0}.rawValue))", - ctx.ty_name(name) - ), - _ => n0, - }, - RetPass::String => format!("wvBorrowString({n0}, {})", n1()), - RetPass::Bytes => format!("wvBorrowBytes({n0}, {})", n1()), - RetPass::Buffer => format!( - "wvBorrowBuffer({n0}, {}, as: {}.self)", - n1(), - ctx.swift_type(&p.ty) +/// The `- Throws:` callout of a protocol requirement. +fn throws_doc(m: &CallbackMethodBinding, ctx: &SwiftCtx) -> Option { + match &m.error { + ErrorStrategy::Trap => None, + ErrorStrategy::Untyped => Some( + "- Throws: Any error, which fails the library's call with the error's description." + .to_string(), ), - RetPass::Object { nullable } => { - let wrapper = ctx.ty_name( - p.ty.interface_name() - .expect("object argument names an interface"), - ); - if *nullable { - format!("{n0}.map {{ {wrapper}(ptr: $0) }}") - } else { - format!("{wrapper}(ptr: wvNonNull({n0}))") - } - } - RetPass::Void => unreachable!("a parameter is never void"), + ErrorStrategy::Domain(name) => Some(format!( + "- Throws: ``{}`` to fail the library's call with that error and its fields; any \ + other error fails it with the error's description.", + ctx.ty_name(&ctx.model.error_domain(name).type_name) + )), } } -/// Render the `public protocol` the consumer conforms to: one `throws` -/// requirement per method with idiomatic labels and types. Implementations -/// are classes (`AnyObject`) the library may call from any thread -/// (`Sendable`). -fn render_protocol( - w: &mut CodeWriter, - cb: &CallbackInterfaceBinding, - domain: Option<&str>, - ctx: &SwiftCtx, -) { +/// Render the `public protocol` the consumer conforms to: one requirement +/// per method with idiomatic labels and types, `throws` when the IDL method +/// declares errors. Implementations are classes (`AnyObject`) the library +/// may call from any thread (`Sendable`). +fn render_protocol(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: &SwiftCtx) { let name = ctx.ty_name(&cb.name); - if cb.doc.is_some() { - w.doc(&cb.doc, DocCommentStyle::TripleSlash); + let doc = ctx.doc(&cb.doc); + if doc.is_some() { + emit_decl_doc(w, ctx, &cb.doc, &None); w.line("///"); } w.line("/// Implement this protocol with a class and pass an instance where the API"); w.line("/// expects it. The library keeps the instance until it no longer needs it and"); - w.line("/// may call any method from any thread. A method that throws fails the"); - w.line("/// library's call in progress: a method documented to throw a module error"); - w.line("/// reports that error, with its code and fields, and any other error reaches"); - w.line("/// the library as a callback failure (code -4)."); - if let Some(msg) = &cb.deprecated { - w.line(deprecated_attr(msg)); + w.line("/// may call any method from any thread."); + if let Some(attr) = ctx.deprecated_attr(&cb.deprecated) { + w.line(attr); } w.line(format!("public protocol {name}: AnyObject, Sendable {{")); w.indent(); for (i, m) in cb.methods.iter().enumerate() { - let m = camel_method(m); if i > 0 { w.blank(); } - let throws = match (m.throws, domain) { - (true, Some(ty)) => vec![format!( - "- Throws: ``{ty}`` to report a declared failure with its fields, or any other error to fail the call." - )], - _ => Vec::new(), - }; - emit_fn_doc(w, &m.doc, &m.params, &throws); - if let Some(msg) = &m.deprecated { - w.line(deprecated_attr(msg)); + let labels: Vec<_> = m.params.iter().map(|p| swift_ident(&p.name)).collect(); + let param_docs: Vec<_> = labels + .iter() + .cloned() + .zip(m.params.iter().map(|p| &p.doc)) + .collect(); + emit_fn_doc( + w, + ctx, + &m.doc, + ¶m_docs, + &throws_doc(m, ctx).into_iter().collect::>(), + ); + if let Some(attr) = ctx.deprecated_attr(&m.deprecated) { + w.line(attr); } - let params = m - .params + let params = labels .iter() - .map(|p| format!("{}: {}", p.name, ctx.swift_type(&p.ty))) + .zip(&m.params) + .map(|(label, p)| format!("{label}: {}", ctx.swift_type(&p.ty))) .collect::>() .join(", "); + let throws = if m.error.throws() { " throws" } else { "" }; let ret = m .ret .as_ref() .map(|t| format!(" -> {}", ctx.swift_type(t))) .unwrap_or_default(); w.line(format!( - "func {}({params}) throws{ret}", + "func {}({params}){throws}{ret}", swift_ident(&m.name) )); } @@ -145,91 +150,106 @@ fn render_protocol( /// How one method's result crosses back: the implementation call wrapped /// into the trampoline's return expression, and the value returned on /// failure. -fn return_shape( - m: &CallbackMethodBinding, - rp: &RetPass, - call: &str, - ctx: &SwiftCtx, -) -> (String, String) { - let out = || { - let slots = &m.abi_params[m.abi_params.len() - 3..m.abi_params.len() - 1]; - (slot_ident(&slots[0].name), slot_ident(&slots[1].name)) - }; - match rp { - RetPass::Void => (format!("try {call}"), "()".to_string()), - RetPass::Direct => match &m.ret { - Some(Ty::Enum(name)) => { - let c_type = ctx.c_enum_type(name); - ( - format!("try {c_type}(rawValue: numericCast({call}.rawValue))"), - format!("{c_type}(rawValue: 0)"), - ) - } - Some(Ty::Prim(Prim::Bool)) => (format!("try {call}"), "false".to_string()), - _ => (format!("try {call}"), "0".to_string()), +fn return_shape(m: &CallbackMethodBinding, call: &str) -> (String, &'static str) { + let optional_enum = matches!( + &m.ret, + Some(Ty::Optional(inner)) if matches!(**inner, Ty::Enum(_)) + ); + let out = |ptr: &str, len: &str| (slot_ident(ptr), slot_ident(len)); + match &m.ret_pass { + CallbackRetPass::Void => (call.to_string(), "()"), + CallbackRetPass::Direct => match &m.ret { + Some(Ty::Enum(_)) => (format!("{call}.rawValue"), "0"), + Some(Ty::Prim(Prim::Bool)) => (call.to_string(), "false"), + _ => (call.to_string(), "0"), }, + CallbackRetPass::OptDirect { out_value } => { + let value = if optional_enum { + format!("{call}?.rawValue") + } else { + call.to_string() + }; + ( + format!("wvReturnOptional({value}, {})", slot_ident(&out_value.name)), + "false", + ) + } // One strong reference the library adopts. - RetPass::Object { nullable } => { + CallbackRetPass::Object { nullable, .. } => { let q = if *nullable { "?" } else { "" }; - (format!("try {call}{q}.clonePtr()"), "nil".to_string()) + (format!("{call}{q}.clonePtr()"), "nil") + } + CallbackRetPass::Slice { + out_ptr, out_len, .. } - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - let helper = match rp { - RetPass::String => "wvReturnString", - RetPass::Bytes => "wvReturnBytes", + | CallbackRetPass::String { out_ptr, out_len } + | CallbackRetPass::Bytes { out_ptr, out_len } + | CallbackRetPass::Buffer { out_ptr, out_len } => { + let helper = match &m.ret_pass { + CallbackRetPass::Slice { .. } => "wvReturnSlice", + CallbackRetPass::String { .. } => "wvReturnString", + CallbackRetPass::Bytes { .. } => "wvReturnBytes", _ => "wvReturnBuffer", }; - let (ptr, len) = out(); - ( - format!("try {helper}({call}, {ptr}, {len})"), - "()".to_string(), - ) + let (ptr, len) = out(&out_ptr.name, &out_len.name); + (format!("{helper}({call}, {ptr}, {len})"), "()") } } } /// Render one trampoline closure literal (the vtable entry for `m`) as a -/// labeled argument of the vtable struct initializer. `i` indexes the -/// method in `cb` and `protocol`; `report` names the domain reporter a -/// throwing method passes along. +/// labeled argument of the vtable struct initializer; `last` drops the +/// trailing comma. fn render_trampoline( w: &mut CodeWriter, cb: &CallbackInterfaceBinding, - protocol: &CallbackProtocol<'_>, - i: usize, - report: Option<&str>, + m: &CallbackMethodBinding, + last: bool, ctx: &SwiftCtx, ) { - let m = &cb.methods[i]; let proto = ctx.ty_name(&cb.name); - let report = report.filter(|_| protocol.method_errors[i] == ErrorStrategy::Throws); // No trailing comma: argument lists accept one only from Swift 6.1. - let sep = if i + 1 == cb.methods.len() { "" } else { "," }; - let camel = camel_method(m); - let formals = m - .abi_params + let sep = if last { "" } else { "," }; + let slots = &m.abi.params; + let formals = slots .iter() .map(|s| slot_ident(&s.name)) .collect::>() .join(", "); - let ctx_slot = slot_ident(&m.abi_params[0].name); - let err_slot = slot_ident(&m.abi_params[m.abi_params.len() - 1].name); - let call_args = camel + let ctx_slot = slot_ident(&slots[0].name); + let err_slot = slot_ident(&slots[slots.len() - 1].name); + let call_args = m .params .iter() - .zip(&protocol.method_args[i]) - .map(|(p, rp)| format!("{}: {}", p.name, receive_arg(p, rp, ctx))) + .map(|p| { + format!( + "{}: {}", + swift_ident(&p.name), + receive_arg(&p.ty, &p.pass, ctx) + ) + }) .collect::>() .join(", "); let call = format!("wvImpl.{}({call_args})", swift_ident(&m.name)); - let (body, fallback) = return_shape(m, &protocol.method_returns[i], &call, ctx); - let domain = report.map(|r| format!(", domain: {r}")).unwrap_or_default(); + let (body, fallback) = return_shape(m, &call); + let body = if m.error.throws() { + format!("try {body}") + } else { + body + }; + let throwing = match &m.error { + ErrorStrategy::Domain(name) => format!( + ", throwing: {}.self", + ctx.ty_name(&ctx.model.error_domain(name).type_name) + ), + ErrorStrategy::Trap | ErrorStrategy::Untyped => String::new(), + }; // The vtable field is spelled exactly as the C header declares it, so a // method whose name is a C keyword carries the header's escape. - w.line(format!("{}: {{ {formals} in", c_param_name(&m.name))); + w.line(format!("{}: {{ {formals} in", c_param_name(&m.abi.symbol))); w.scope(|w| { w.line(format!( - "wvInvoke({ctx_slot}, {err_slot}, as: (any {proto}).self, fallback: {fallback}{domain}) {{ wvImpl in" + "wvInvoke({ctx_slot}, {err_slot}, as: (any {proto}).self{throwing}, fallback: {fallback}) {{ wvImpl in" )); w.scope(|w| { w.line(body); @@ -242,37 +262,29 @@ fn render_trampoline( /// Render the process-wide vtable namespace: `shared` holds the C struct /// (its header, then capture-free trampolines, implicitly `@convention(c)`) /// at a stable address the library can hold for the process lifetime. -fn render_vtable( - w: &mut CodeWriter, - cb: &CallbackInterfaceBinding, - protocol: &CallbackProtocol<'_>, - report: Option<&str>, - ctx: &SwiftCtx, -) { +fn render_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: &SwiftCtx) { let proto = ctx.ty_name(&cb.name); let vtable_name = callback_vtable_name(&cb.name); let vtable_tag = &cb.vtable_tag; - w.line(format!( - "/// The process-wide vtable the library calls `{proto}` implementations" - )); - w.line(format!( - "/// through (`{vtable_tag}`). Every entry recovers the implementation" - )); - w.line("/// from `ctx`, copies or adopts its arguments, hands its result back, and"); - w.line("/// reports a thrown error through `out_err` instead of unwinding."); + let doc = format!( + "The process-wide vtable the library calls `{proto}` implementations through \ + (a `{vtable_tag}`). Every entry recovers the implementation from `ctx`, copies \ + or adopts its arguments, hands its result back, and reports a thrown error \ + through `out_err` instead of unwinding." + ); + w.doc(&Some(wrap(&doc, 76)), DocCommentStyle::TripleSlash); w.line(format!("enum {vtable_name} {{")); w.indent(); w.line(format!("static let shared = WvVtable({vtable_tag}(")); w.indent(); w.line(format!("size: UInt32(MemoryLayout<{vtable_tag}>.stride),")); w.line("flags: 0,"); - w.line("free: { ctx in"); - w.scope(|w| { - w.line(format!("wvRelease(ctx, as: (any {proto}).self)")); - }); - w.line("},"); - for i in 0..cb.methods.len() { - render_trampoline(w, cb, protocol, i, report, ctx); + w.line(format!( + "free: {{ ctx in wvRelease(ctx, as: (any {proto}).self) }}{}", + if cb.methods.is_empty() { "" } else { "," } + )); + for (i, m) in cb.methods.iter().enumerate() { + render_trampoline(w, cb, m, i + 1 == cb.methods.len(), ctx); } w.dedent(); w.line("))"); @@ -283,17 +295,12 @@ fn render_vtable( /// Render everything one callback interface contributes at file scope: the /// public protocol and the vtable namespace whose `shared.pointer` every -/// call site passing this interface hands to the library. `domain` is the -/// `(type name, stem)` of the error domain in scope for its module, which -/// throwing methods report through `wvReport{Stem}`. +/// call site passing this interface hands to the library. pub(crate) fn render_swift_callback_interface( w: &mut CodeWriter, cb: &CallbackInterfaceBinding, - domain: Option<(&str, &str)>, ctx: &SwiftCtx, ) { - let protocol = cb.protocol(); - let report = domain.map(|(_, stem)| format!("wvReport{stem}")); - render_protocol(w, cb, domain.map(|d| d.0), ctx); - render_vtable(w, cb, &protocol, report.as_deref(), ctx); + render_protocol(w, cb, ctx); + render_vtable(w, cb, ctx); } diff --git a/crates/weaveffi-cli/src/targets/swift/calls.rs b/crates/weaveffi-cli/src/targets/swift/calls.rs index ca4bfaf8..138de0a0 100644 --- a/crates/weaveffi-cli/src/targets/swift/calls.rs +++ b/crates/weaveffi-cli/src/targets/swift/calls.rs @@ -1,112 +1,166 @@ -//! Callable rendering: sync and async wrapper functions and the lazy -//! sequence classes backing `iter` returns. +//! Callable rendering: sync, async, and iterator-returning wrapper +//! functions. //! -//! Parameter marshalling dispatches on the shared [`ArgPass`] plan and -//! return handling on [`RetPass`], so how a value crosses the ABI is decided -//! centrally; this module only renders the Swift spelling. +//! How a value crosses is read from the model's passing contracts +//! ([`ArgPass`], [`RetPass`], [`ResultPass`], [`ItemPass`]) and how a +//! failure surfaces from its [`ErrorStrategy`]; this module only renders the +//! Swift spelling. use crate::codegen::CodeWriter; -use weaveffi_model::model::{CallShape, FnBinding, IteratorBinding, ParamBinding}; -use weaveffi_model::plan::{ArgPass, ErrorStrategy, RetPass}; -use weaveffi_model::ty::{Prim, Ty}; +use crate::lang; +use weaveffi_model::model::{AsyncBinding, FnBinding, IteratorBinding, ParamBinding}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy, ItemPass, ResultPass, RetPass}; +use weaveffi_model::ty::{Prim, RetTy, Ty}; use crate::targets::swift::docs::emit_fn_doc; -use crate::targets::swift::types::{ - callback_vtable_name, deprecated_attr, iterator_class_name, scalar_swift_type, swift_ident, - SwiftCtx, -}; +use crate::targets::swift::types::{callback_vtable_name, prim_swift_type, swift_ident, SwiftCtx}; -/// How a wrapper reports a filled error slot. +/// Locals a wrapper body declares next to its parameters. A parameter +/// spelled like one keeps its argument label and gets a `_`-suffixed local +/// name (`err err_: Int32`), so the body never confuses the two. +const BODY_LOCALS: &[&str] = &[ + "err", + "rv", + "outLen", + "outValue", + "token", + "context", + "continuation", +]; + +/// One parameter of a wrapper: its argument label and the local name the +/// body uses (they differ only when the label is a body local). +pub(crate) struct Param<'a> { + /// The argument label (lowerCamelCase, keyword-escaped). + pub(crate) label: String, + /// The name the body refers to the argument by. + pub(crate) local: String, + /// The parameter as lowered. + pub(crate) binding: &'a ParamBinding, +} + +impl Param<'_> { + /// The declaration in a signature: `label: Type`, or `label local: Type`. + fn decl(&self, ctx: &SwiftCtx) -> String { + let ty = ctx.param_type(&self.binding.ty); + if self.label == self.local { + format!("{}: {ty}", self.label) + } else { + format!("{} {}: {ty}", self.label, self.local) + } + } +} + +/// The wrapper parameters of `f`. +pub(crate) fn swift_params(f: &FnBinding) -> Vec> { + f.params + .iter() + .map(|binding| { + let label = swift_ident(&binding.name); + let local = lang::escape_member(&label, BODY_LOCALS); + Param { + label, + local, + binding, + } + }) + .collect() +} + +/// How a wrapper reports a failure, from the callable's [`ErrorStrategy`] +/// and cancellability. /// -/// A callable declaring `throws` maps codes through its domain's typed -/// mapper (`wvCheckKv`/`wvMapKv`) and throws; a cancellable async callable -/// also throws (`CancellationError` for code `-5`). Any other callable has a -/// plain signature and stops the process with `fatalError`, naming the code -/// and message, since a reported error can only be a producer bug. -#[derive(Clone, Copy)] -pub(crate) struct ErrCtx<'a> { - /// `true` when the Swift signature `throws`. - throws: bool, - /// `true` when the IDL declares `throws` (domain codes are typed). - typed: bool, - /// `true` for a cancellable async callable. +/// A callable declared `throws: Domain` throws the domain's enum for its +/// positive codes; one declared `throws: any` throws the runtime error; a +/// cancellable async callable also throws (`CancellationError` for code +/// `-5`). Any other callable has a plain signature and stops the process +/// with `fatalError`, naming the code and message, since a reported error +/// can only be a producer bug. +pub(crate) struct ErrCtx { + error: ErrorStrategy, cancellable: bool, - /// The domain error type in effect, when the callable is typed. - domain_type: Option<&'a str>, - /// PascalCase stem of the domain in effect (`Kv` names `wvCheckKv` and - /// `wvMapKv`); `None` falls back to the runtime helpers. - domain: Option<&'a str>, + /// The Swift type of the domain, for `throws: Domain`. + domain_type: Option, } -impl<'a> ErrCtx<'a> { - /// Build the error context for `f` from its [`ErrorStrategy`] and - /// cancellability, given the `(type name, stem)` of the domain in scope. - pub(crate) fn for_fn(f: &FnBinding, domain: Option<(&'a str, &'a str)>) -> Self { - let typed = f.error_strategy() == ErrorStrategy::Throws; - let cancellable = f.is_async() && f.cancellable; +impl ErrCtx { + /// The error context of `f`. + pub(crate) fn for_fn(f: &FnBinding, ctx: &SwiftCtx) -> Self { Self { - throws: typed || cancellable, - typed, - cancellable, - domain_type: domain.map(|d| d.0), - domain: domain.map(|d| d.1), + domain_type: f + .error + .domain() + .map(|d| ctx.ty_name(&ctx.model.error_domain(d).type_name)), + error: f.error.clone(), + cancellable: f.cancellable(), } } - /// The mapper turning an error slot into a Swift error, when the wrapper - /// throws: the domain's `wvMap{Stem}`, else the runtime's. - fn mapper(&self) -> String { - match (self.typed, self.domain) { - (true, Some(stem)) => format!("wvMap{stem}"), - (true, None) => "wvRuntimeError".to_string(), - (false, _) => "wvCancelledOrTrap".to_string(), - } + /// `true` when the Swift signature `throws`. + pub(crate) fn throws(&self) -> bool { + self.error.throws() || self.cancellable } /// The statement checking the error slot of a synchronous call. fn check_stmt(&self) -> String { - match (self.typed, self.domain) { - (true, Some(stem)) => format!("try wvCheck{stem}(&err)"), - (true, None) => "try wvCheck(&err)".to_string(), - (false, _) => "wvTrap(&err)".to_string(), + match (&self.error, &self.domain_type) { + (ErrorStrategy::Domain(_), Some(ty)) => format!("try wvCheck(&err, {ty}.self)"), + (ErrorStrategy::Trap, _) => "wvTrap(&err)".to_string(), + _ => "try wvCheck(&err)".to_string(), + } + } + + /// The statement a completion runs for its boxed error `err`: resume + /// the continuation by throwing, or stop the process. + fn completion_failure(&self) -> Vec { + let take = match (&self.error, &self.domain_type) { + (ErrorStrategy::Domain(_), Some(ty)) => format!("wvTakeError(err, {ty}.self)"), + (ErrorStrategy::Trap, _) if self.cancellable => "wvTakeCancellation(err)".to_string(), + (ErrorStrategy::Trap, _) => return vec!["wvFatal(err.pointee)".to_string()], + _ => "wvTakeError(err)".to_string(), + }; + vec![ + format!("cont.resume(throwing: {take})"), + "return".to_string(), + ] + } + + /// The closure a sequence maps a mid-stream failure through. + fn sequence_failure(&self) -> String { + match (&self.error, &self.domain_type) { + (ErrorStrategy::Domain(_), Some(ty)) => format!("{{ wvDomainError($0, {ty}.self) }}"), + (ErrorStrategy::Trap, _) => "{ wvFatal($0) }".to_string(), + _ => "wvRuntimeError".to_string(), } } /// The `- Throws:` doc callout of a throwing wrapper. fn throws_doc(&self, runtime_error: &str) -> Option { - if !self.throws { - return None; - } - let mut doc = match (self.typed, self.domain_type) { - (true, Some(ty)) => { + let mut doc = match (&self.error, &self.domain_type) { + (ErrorStrategy::Domain(_), Some(ty)) => { format!("- Throws: ``{ty}`` for a declared failure, ``{runtime_error}`` otherwise") } - (true, None) => format!("- Throws: ``{runtime_error}``"), - (false, _) => "- Throws: `CancellationError`".to_string(), + (ErrorStrategy::Trap, _) if self.cancellable => { + "- Throws: `CancellationError` when the calling task is cancelled, which \ + cancels the native call." + .to_string() + } + (ErrorStrategy::Trap, _) => return None, + _ => format!("- Throws: ``{runtime_error}`` with the library's message"), }; - if self.cancellable && self.typed { - doc.push_str(", and `CancellationError`"); - } - if self.cancellable { - doc.push_str(" when the calling task is cancelled, which cancels the native call"); + if self.error.throws() { + if self.cancellable { + doc.push_str( + ", and `CancellationError` when the calling task is cancelled, which \ + cancels the native call", + ); + } + doc.push('.'); } - doc.push('.'); Some(doc) } } -/// Clone a callable with its parameter names camel-cased and keyword-escaped, -/// so the Swift argument labels, bound locals, and every staged -/// `_ptr`/`_len` variable derived from them agree (and never collide with a -/// reserved word). -pub(crate) fn camel_params(f: &FnBinding) -> FnBinding { - let mut f = f.clone(); - for p in &mut f.params { - p.name = swift_ident(&p.name); - } - f -} - /// Where a wrapper sits, which decides how it reaches its object and whether /// it runs the load-time checks. #[derive(Clone, Copy, PartialEq, Eq)] @@ -126,29 +180,32 @@ pub(crate) fn render_callable( f: &FnBinding, swift_name: &str, receiver: Receiver, - err: ErrCtx, ctx: &SwiftCtx, ) { + let err = ErrCtx::for_fn(f, ctx); + let params = swift_params(f); let mut callouts = Vec::new(); - if let CallShape::Iterator(_) = &f.shape { + if f.iterator().is_some() { callouts.push( "- Returns: A lazy sequence that pulls one element per step from the library." .to_string(), ); } callouts.extend(err.throws_doc(&ctx.runtime_error)); - emit_fn_doc(w, &f.doc, &f.params, &callouts); - if let Some(msg) = &f.deprecated { - w.line(deprecated_attr(msg)); + let param_docs: Vec<_> = params + .iter() + .map(|p| (p.label.clone(), &p.binding.doc)) + .collect(); + emit_fn_doc(w, ctx, &f.doc, ¶m_docs, &callouts); + if let Some(attr) = ctx.deprecated_attr(&f.deprecated) { + w.line(attr); } - let sig = f - .params + let sig = params .iter() - .map(|p| format!("{}: {}", p.name, ctx.swift_type(&p.ty))) + .map(|p| p.decl(ctx)) .collect::>() .join(", "); - let is_async = matches!(f.shape, CallShape::Async(_)); - let effects = match (is_async, err.throws) { + let effects = match (f.is_async(), err.throws()) { (true, true) => " async throws", (true, false) => " async", (false, true) => " throws", @@ -157,14 +214,11 @@ pub(crate) fn render_callable( if receiver == Receiver::Init { w.line(format!("public init({sig}){effects} {{")); } else { - let ret = match (&f.shape, &f.ret) { - (CallShape::Iterator(it), _) => format!( - " -> {}", - ctx.ty_name(&iterator_class_name(it, ctx.c_prefix)) - ), - (_, Some(t)) => format!(" -> {}", ctx.swift_type(t)), - (_, None) => String::new(), - }; + let ret = f + .ret + .as_ref() + .map(|t| format!(" -> {}", ctx.ret_type(t))) + .unwrap_or_default(); let static_kw = if receiver == Receiver::Instance { "" } else { @@ -178,54 +232,147 @@ pub(crate) fn render_callable( if receiver != Receiver::Instance { w.line("wvLoad()"); } - if is_async { - render_async_body(w, f, receiver, err, ctx); - } else { - render_sync_body(w, f, receiver, err, ctx); + match f.async_binding() { + Some(binding) => render_async_body(w, f, binding, ¶ms, receiver, &err, ctx), + None => render_sync_body(w, f, ¶ms, receiver, &err, ctx), } w.dedent(); w.line("}"); } -/// The C argument list for `params`, prefixed with the receiver's object for -/// an instance method. -fn c_call_args(params: &[ParamBinding], self_arg: Option<&str>, ctx: &SwiftCtx) -> Vec { +/// The Swift spelling of the C type of a direct scalar or C-style enum +/// (`Int32` for an enum, whose C type is an `int32_t` typedef), and its +/// zero value. +fn c_scalar(t: &Ty) -> (&'static str, &'static str) { + match t { + Ty::Prim(Prim::Bool) => ("Bool", "false"), + Ty::Prim(p) => (prim_swift_type(*p), "0"), + _ => ("Int32", "0"), + } +} + +/// The value type inside an optional (`T` of `T?`). +fn unwrap_optional(t: &Ty) -> &Ty { + match t { + Ty::Optional(inner) => inner, + other => other, + } +} + +/// How a value the library hands over is received. +pub(crate) enum Recv<'a> { + /// A scalar or C-style enum discriminant, by value. + Direct, + /// An optional scalar: the value and the expression saying it's present. + Opt(&'a str), + /// A typed array of `len` elements. + Slice, + /// UTF-8 bytes. + String, + /// Raw bytes. + Bytes, + /// A value buffer. + Buffer, + /// An object reference the wrapper adopts. + Object { + /// `true` for `I?`. + nullable: bool, + /// The interface's name. + interface: &'a str, + }, +} + +/// The Swift expression receiving a value of type `ty` that the library +/// handed over in `raw` (and `len`): `owned` values (returns, async +/// results, iterator elements) are released after copying, borrowed ones +/// (callback arguments) are only copied. An object argument is always +/// adopted. +pub(crate) fn receive( + ty: &Ty, + recv: &Recv, + raw: &str, + len: &str, + owned: bool, + ctx: &SwiftCtx, +) -> String { + let take = if owned { "wvTake" } else { "wvBorrow" }; + match recv { + Recv::Direct => match ty { + Ty::Enum(name) => format!("wvEnumCase({}.self, {raw})", ctx.ty_name(name)), + _ => raw.to_string(), + }, + Recv::Opt(has) => { + let value = receive(unwrap_optional(ty), &Recv::Direct, raw, len, owned, ctx); + format!("{has} ? {value} : nil") + } + Recv::Slice => format!("{take}Slice({raw}, {len})"), + Recv::String => format!("{take}String({raw}, {len})"), + Recv::Bytes => format!("{take}Bytes({raw}, {len})"), + Recv::Buffer => format!( + "{take}Buffer({raw}, {len}, as: {}.self)", + ctx.swift_type(ty) + ), + Recv::Object { + nullable, + interface, + } => { + let class = ctx.ty_name(interface); + if *nullable { + format!("{raw}.map {{ {class}(ptr: $0) }}") + } else { + format!("{class}(ptr: wvNonNull({raw}))") + } + } + } +} + +/// The C arguments for `params`, after the receiver's object for an +/// instance method. +fn c_call_args(params: &[Param], self_arg: Option<&str>) -> Vec { let mut args: Vec = self_arg.into_iter().map(str::to_string).collect(); for p in params { - let n = &p.name; - match p.arg_pass() { - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + let n = &p.local; + match &p.binding.pass { + ArgPass::Direct { .. } => match p.binding.ty.value() { + Some(Ty::Enum(_)) => args.push(format!("{n}.rawValue")), + _ => args.push(n.clone()), + }, + ArgPass::OptDirect { inner, .. } => { + args.push(format!("{n} != nil")); + args.push(match inner { + Ty::Enum(_) => format!("{n}?.rawValue ?? 0"), + other => format!("{n} ?? {}", c_scalar(other).1), + }); + } + ArgPass::Slice { .. } + | ArgPass::String { .. } + | ArgPass::Bytes { .. } + | ArgPass::Buffer { .. } => { args.push(format!("{n}_ptr")); args.push(format!("{n}_len")); } // The wrapper borrows its own reference for the call; Swift - // keeps every parameter alive until the wrapper returns. + // keeps every argument alive until the wrapper returns. ArgPass::Object { nullable, .. } => { - args.push(if nullable { + args.push(if *nullable { format!("{n}?.ptr") } else { format!("{n}.ptr") }); } - ArgPass::Callback { nullable, .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback family names a callback interface"); - let vtable = format!("{}.shared.pointer", callback_vtable_name(cb)); + ArgPass::Callback { + nullable, + interface, + .. + } => { + let vtable = format!("{}.shared.pointer", callback_vtable_name(interface)); args.push(format!("{n}_ctx")); - args.push(if nullable { + args.push(if *nullable { format!("{n} == nil ? nil : {vtable}") } else { vtable }); } - ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(name) => args.push(format!( - "{}(rawValue: numericCast({n}.rawValue))", - ctx.c_enum_type(name) - )), - _ => args.push(n.clone()), - }, } } args @@ -234,11 +381,11 @@ fn c_call_args(params: &[ParamBinding], self_arg: Option<&str>, ctx: &SwiftCtx) /// Emit the pre-call staging of callback parameters: each implementation is /// retained for the library, whose vtable `free` entry releases it (also /// when the call fails). -fn render_arg_staging(w: &mut CodeWriter, params: &[ParamBinding]) { +fn render_arg_staging(w: &mut CodeWriter, params: &[Param]) { for p in params { - if let ArgPass::Callback { nullable, .. } = p.arg_pass() { - let n = &p.name; - if nullable { + if let ArgPass::Callback { nullable, .. } = &p.binding.pass { + let n = &p.local; + if *nullable { w.line(format!("let {n}_ctx = {n}.map {{ wvRetain($0) }}")); } else { w.line(format!("let {n}_ctx = wvRetain({n})")); @@ -247,17 +394,19 @@ fn render_arg_staging(w: &mut CodeWriter, params: &[ParamBinding]) { } } -/// Open the pointer-lending closures every string, bytes, and buffered -/// parameter needs, binding `{name}_ptr`/`{name}_len`. `bind` prefixes the -/// first opening line (`let rv: T = ` or nothing); nested ones return their -/// inner value implicitly. Returns how many closures were opened. -fn open_lending_closures(w: &mut CodeWriter, params: &[ParamBinding], bind: &str) -> usize { +/// Open the closures lending the storage of every string, bytes, typed +/// array, and buffered parameter, binding `{local}_ptr`/`{local}_len`. +/// `bind` prefixes the first opening line (`let rv: T = ` or nothing); +/// nested ones return their inner value implicitly. Returns how many +/// closures were opened. +fn open_lending_closures(w: &mut CodeWriter, params: &[Param], bind: &str) -> usize { let mut opened = 0; for p in params { - let n = &p.name; - let open = match p.arg_pass() { + let n = &p.local; + let open = match &p.binding.pass { ArgPass::String { .. } => format!("wvWithUTF8({n})"), ArgPass::Bytes { .. } => format!("wvWithBytes({n})"), + ArgPass::Slice { .. } => format!("wvWithSlice({n})"), ArgPass::Buffer { .. } => format!("wvWithEncoded({n})"), _ => continue, }; @@ -277,21 +426,22 @@ fn close_closures(w: &mut CodeWriter, n: usize) { } } -/// The Swift type of the raw C result of a synchronous call, used to annotate -/// its binding when the call sits inside lending closures. -fn raw_return_swift(f: &FnBinding, rp: Option<&RetPass>, ctx: &SwiftCtx) -> String { - match rp { - None | Some(RetPass::Object { .. }) => "OpaquePointer?".to_string(), - Some(RetPass::Void) => "Void".to_string(), - Some(RetPass::String | RetPass::Bytes | RetPass::Buffer) => { +/// The Swift type of the raw C result of a synchronous call, which +/// annotates its binding when the call sits inside lending closures. +fn raw_return_swift(f: &FnBinding) -> Option { + let ret = f.ret.as_ref().map(RetTy::elem); + Some(match &f.ret_pass { + RetPass::Void => return None, + RetPass::Direct => c_scalar(ret?).0.to_string(), + RetPass::OptDirect { .. } => "Bool".to_string(), + RetPass::Slice { elem, .. } => { + format!("UnsafeMutablePointer<{}>?", prim_swift_type(*elem)) + } + RetPass::String { .. } | RetPass::Bytes { .. } | RetPass::Buffer { .. } => { "UnsafePointer?".to_string() } - Some(RetPass::Direct) => match f.ret.as_ref() { - Some(Ty::Enum(name)) => ctx.c_enum_type(name).to_string(), - Some(other) => scalar_swift_type(other).to_string(), - None => unreachable!("a direct return carries a type"), - }, - } + RetPass::Object { .. } | RetPass::Iterator(_) => "OpaquePointer?".to_string(), + }) } /// Render the body of a synchronous (or iterator-returning) callable: the @@ -300,42 +450,44 @@ fn raw_return_swift(f: &FnBinding, rp: Option<&RetPass>, ctx: &SwiftCtx) -> Stri fn render_sync_body( w: &mut CodeWriter, f: &FnBinding, + params: &[Param], receiver: Receiver, - err: ErrCtx, + err: &ErrCtx, ctx: &SwiftCtx, ) { w.line("var err = WvError()"); - render_arg_staging(w, &f.params); - - // An iterator launch returns the handle; its plan is the iterator - // protocol rather than a `RetPass`. - let (symbol, rp) = match &f.shape { - CallShape::Iterator(it) => (&it.launch.symbol, None), - CallShape::Sync(abi) => (&abi.symbol, Some(RetPass::of(f.ret.as_ref()))), - CallShape::Async(_) => unreachable!("async callables render through render_async_body"), - }; - let needs_out_len = matches!(rp, Some(RetPass::String | RetPass::Bytes | RetPass::Buffer)); - if needs_out_len { - w.line("var outLen = 0"); - } - - let self_arg = (receiver == Receiver::Instance).then_some("ptr"); - let mut args = c_call_args(&f.params, self_arg, ctx); - if needs_out_len { - args.push("&outLen".to_string()); + render_arg_staging(w, params); + let ret = f.ret.as_ref().map(RetTy::elem); + let mut args = c_call_args( + params, + (receiver == Receiver::Instance).then_some("self.ptr"), + ); + match &f.ret_pass { + RetPass::Slice { .. } + | RetPass::String { .. } + | RetPass::Bytes { .. } + | RetPass::Buffer { .. } => { + w.line("var outLen = 0"); + args.push("&outLen".to_string()); + } + RetPass::OptDirect { .. } => { + let (ty, zero) = c_scalar(unwrap_optional(ret.expect("an optional return"))); + w.line(format!("var outValue: {ty} = {zero}")); + args.push("&outValue".to_string()); + } + _ => {} } args.push("&err".to_string()); - let call = format!("{symbol}({})", args.join(", ")); + let call = format!("{}({})", f.abi.symbol, args.join(", ")); - let has_ret = !matches!(rp, Some(RetPass::Void)); - let bind = if has_ret { - format!("let rv: {} = ", raw_return_swift(f, rp.as_ref(), ctx)) - } else { - String::new() - }; - let opened = open_lending_closures(w, &f.params, &bind); + let raw = raw_return_swift(f); + let bind = raw + .as_ref() + .map(|t| format!("let rv: {t} = ")) + .unwrap_or_default(); + let opened = open_lending_closures(w, params, &bind); if opened == 0 { - if has_ret { + if raw.is_some() { w.line(format!("let rv = {call}")); } else { w.line(call); @@ -346,60 +498,196 @@ fn render_sync_body( } w.line(err.check_stmt()); - match rp { - None => { - let CallShape::Iterator(it) = &f.shape else { - unreachable!("only an iterator launch has no return plan") - }; - let class_name = ctx.ty_name(&iterator_class_name(it, ctx.c_prefix)); - w.line(format!("return {class_name}(handle: wvNonNull(rv))")); - } - Some(RetPass::Void) => {} + let value = match &f.ret_pass { + RetPass::Void => return, // The constructor adopts the returned reference as its own. - Some(_) if receiver == Receiver::Init => { + _ if receiver == Receiver::Init => { w.line("self.ptr = wvNonNull(rv)"); + return; } - Some(rp) => { - let value = receive_value(f.ret.as_ref(), &rp, "rv", "outLen", ctx); - w.line(format!("return {value}")); + RetPass::Iterator(it) => { + render_sequence(w, it, err, ctx); + return; } - } + RetPass::Direct => receive( + ret.expect("a direct return"), + &Recv::Direct, + "rv", + "", + true, + ctx, + ), + RetPass::OptDirect { .. } => receive( + ret.expect("an optional return"), + &Recv::Opt("rv"), + "outValue", + "", + true, + ctx, + ), + RetPass::Slice { .. } => "wvTakeSlice(rv, outLen)".to_string(), + RetPass::String { .. } => "wvTakeString(rv, outLen)".to_string(), + RetPass::Bytes { .. } => "wvTakeBytes(rv, outLen)".to_string(), + RetPass::Buffer { .. } => receive( + ret.expect("a buffered return"), + &Recv::Buffer, + "rv", + "outLen", + true, + ctx, + ), + RetPass::Object { + nullable, + interface, + .. + } => receive( + ret.expect("an object return"), + &Recv::Object { + nullable: *nullable, + interface, + }, + "rv", + "", + true, + ctx, + ), + }; + w.line(format!("return {value}")); } -/// The Swift expression converting a value the library returned (an owned -/// result) into its Swift form, releasing what the conversion copied. -/// `raw` names the result slot and `len` its length slot, if any. -fn receive_value(ty: Option<&Ty>, rp: &RetPass, raw: &str, len: &str, ctx: &SwiftCtx) -> String { - match rp { - RetPass::Void => "()".to_string(), - RetPass::Direct => match ty { - Some(Ty::Enum(name)) => format!( - "wvEnumCase({}.self, numericCast({raw}.rawValue))", - ctx.ty_name(name) - ), - _ => raw.to_string(), - }, - RetPass::String => format!("wvTakeString({raw}, {len})"), - RetPass::Bytes => format!("wvTakeBytes({raw}, {len})"), - RetPass::Buffer => { - let ty = ty.expect("a buffered value carries a type"); - format!( - "wvTakeBuffer({raw}, {len}, as: {}.self)", - ctx.swift_type(ty) - ) +/// Return the `NativeSequence` over the iterator handle in `rv`: its pull +/// closure calls `_next` once per element and receives the element like a +/// return of the same type. +fn render_sequence(w: &mut CodeWriter, it: &IteratorBinding, err: &ErrCtx, ctx: &SwiftCtx) { + let elem = &it.elem; + let mut decls = Vec::new(); + let (args, recv, len) = match &it.item { + ItemPass::Direct { .. } => { + let (ty, zero) = c_scalar(elem); + decls.push(format!("var item: {ty} = {zero}")); + ("&item", Recv::Direct, "") } - RetPass::Object { nullable } => { - let class = ctx.ty_name( - ty.and_then(Ty::interface_name) - .expect("an object return names an interface"), - ); - if *nullable { - format!("{raw}.map {{ {class}(ptr: $0) }}") - } else { - format!("{class}(ptr: wvNonNull({raw}))") - } + ItemPass::OptDirect { .. } => { + let (ty, zero) = c_scalar(unwrap_optional(elem)); + decls.push("var hasItem = false".to_string()); + decls.push(format!("var item: {ty} = {zero}")); + ("&hasItem, &item", Recv::Opt("hasItem"), "") + } + ItemPass::Slice { elem: prim, .. } => { + decls.push(format!( + "var item: UnsafeMutablePointer<{}>? = nil", + prim_swift_type(*prim) + )); + decls.push("var itemLen = 0".to_string()); + ("&item, &itemLen", Recv::Slice, "itemLen") + } + ItemPass::String { .. } | ItemPass::Bytes { .. } | ItemPass::Buffer { .. } => { + decls.push("var item: UnsafePointer? = nil".to_string()); + decls.push("var itemLen = 0".to_string()); + let recv = match &it.item { + ItemPass::String { .. } => Recv::String, + ItemPass::Bytes { .. } => Recv::Bytes, + _ => Recv::Buffer, + }; + ("&item, &itemLen", recv, "itemLen") + } + ItemPass::Object { + nullable, + interface, + .. + } => { + decls.push("var item: OpaquePointer? = nil".to_string()); + ( + "&item", + Recv::Object { + nullable: *nullable, + interface, + }, + "", + ) } + }; + let mut value = receive(elem, &recv, "item", len, true, ctx); + // An optional element must stay distinguishable from the end of the + // stream (`nil`). + if matches!(recv, Recv::Opt(_) | Recv::Object { nullable: true, .. }) { + value = format!(".some({value})"); } + w.line(format!( + "return NativeSequence(wvNonNull(rv), release: {{ {}($0) }}, failure: {}) {{ handle, err in", + it.destroy_symbol, + err.sequence_failure() + )); + w.scope(|w| { + for d in &decls { + w.line(d); + } + w.line(format!( + "guard {}(handle, {args}, err) != 0 else {{ return nil }}", + it.next.symbol + )); + w.line(format!("return {value}")); + }); + w.line("}"); +} + +/// The completion closure's formals and the expression receiving its +/// result, per the async result's [`ResultPass`]. +fn completion_shape(f: &FnBinding, binding: &AsyncBinding, ctx: &SwiftCtx) -> (String, String) { + let formals = binding + .callback_params + .iter() + .map(|p| lang::escape_ident(&p.name, lang::SWIFT_KEYWORDS)) + .collect::>() + .join(", "); + let ret = f.ret.as_ref().map(RetTy::elem); + let value = match &binding.result { + ResultPass::Void => "()".to_string(), + ResultPass::Direct { result } => receive( + ret.expect("a direct result"), + &Recv::Direct, + &result.name, + "", + true, + ctx, + ), + ResultPass::OptDirect { has, value } => receive( + ret.expect("an optional result"), + &Recv::Opt(&has.name), + &value.name, + "", + true, + ctx, + ), + ResultPass::Slice { ptr, len, .. } => format!("wvTakeSlice({}, {})", ptr.name, len.name), + ResultPass::String { ptr, len } => format!("wvTakeString({}, {})", ptr.name, len.name), + ResultPass::Bytes { ptr, len } => format!("wvTakeBytes({}, {})", ptr.name, len.name), + ResultPass::Buffer { ptr, len } => receive( + ret.expect("a buffered result"), + &Recv::Buffer, + &ptr.name, + &len.name, + true, + ctx, + ), + ResultPass::Object { + result, + nullable, + interface, + .. + } => receive( + ret.expect("an object result"), + &Recv::Object { + nullable: *nullable, + interface, + }, + &result.name, + "", + true, + ctx, + ), + }; + (formals, value) } /// Render the body of an async callable: a checked continuation resumed @@ -408,85 +696,73 @@ fn receive_value(ty: Option<&Ty>, rp: &RetPass, raw: &str, len: &str, ctx: &Swif fn render_async_body( w: &mut CodeWriter, f: &FnBinding, + binding: &AsyncBinding, + params: &[Param], receiver: Receiver, - err: ErrCtx, + err: &ErrCtx, ctx: &SwiftCtx, ) { - let CallShape::Async(binding) = &f.shape else { - unreachable!("render_async_body renders async callables") - }; - let protocol = binding.protocol(f); let ret = f .ret .as_ref() - .map_or_else(|| "Void".to_string(), |t| ctx.swift_type(t)); - let err_ty = if err.throws { "Error" } else { "Never" }; + .map_or_else(|| "Void".to_string(), |t| ctx.ret_type(t)); + let err_ty = if err.throws() { "Error" } else { "Never" }; + let cancellable = binding.cancellable(); - if protocol.cancellable { + if cancellable { w.line("let token = WvCancelToken()"); w.line("return try await withTaskCancellationHandler {"); w.indent(); } - if err.throws { + let ret_kw = if cancellable { "" } else { "return " }; + if err.throws() { w.line(format!( - "{}try await withCheckedThrowingContinuation {{ (continuation: CheckedContinuation<{ret}, Error>) in", - if protocol.cancellable { "" } else { "return " } + "{ret_kw}try await withCheckedThrowingContinuation {{ (continuation: CheckedContinuation<{ret}, Error>) in" )); } else { w.line(format!( - "return await withCheckedContinuation {{ (continuation: CheckedContinuation<{ret}, Never>) in" + "{ret_kw}await withCheckedContinuation {{ (continuation: CheckedContinuation<{ret}, Never>) in" )); } w.indent(); - // The library copies every input during the launch, so lending them for - // the launch call's duration is enough. - render_arg_staging(w, &f.params); + // The library copies or retains every input during the launch, so + // lending them for the launch call's duration is enough. + render_arg_staging(w, params); w.line("let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque()"); - let opened = open_lending_closures(w, &f.params, ""); + let opened = open_lending_closures(w, params, ""); - let self_arg = (receiver == Receiver::Instance).then_some("self.ptr"); - let mut args = c_call_args(&f.params, self_arg, ctx); - if protocol.cancellable { + let mut args = c_call_args( + params, + (receiver == Receiver::Instance).then_some("self.ptr"), + ); + if cancellable { args.push("token.raw".to_string()); } - let (slots, raw) = match &protocol.result { - RetPass::Void => ("context, err", "result"), - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - ("context, err, resultPtr, resultLen", "resultPtr") - } - _ => ("context, err, result", "result"), - }; + let (formals, value) = completion_shape(f, binding, ctx); let mut launch = args.join(", "); if !launch.is_empty() { launch.push_str(", "); } - w.line(format!("{}({launch}{{ {slots} in", binding.launch.symbol)); + w.line(format!("{}({launch}{{ {formals} in", f.abi.symbol)); w.indent(); w.line(format!( "let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value" )); w.line("if let err = err {"); - w.indent(); - if err.throws { - w.line(format!( - "cont.resume(throwing: wvTakeError(err, {}))", - err.mapper() - )); - w.line("return"); - } else { - w.line("wvTrapBoxed(err)"); - } - w.dedent(); + w.scope(|w| { + for line in err.completion_failure() { + w.line(line); + } + }); w.line("}"); - let value = receive_value(f.ret.as_ref(), &protocol.result, raw, "resultLen", ctx); w.line(format!("cont.resume(returning: {value})")); w.dedent(); w.line("}, context)"); close_closures(w, opened); w.dedent(); w.line("}"); - if protocol.cancellable { + if cancellable { w.dedent(); w.line("} onCancel: {"); w.scope(|w| { @@ -495,127 +771,3 @@ fn render_async_body( w.line("}"); } } - -/// Emit the lazy sequence class backing one `iter` function. -/// -/// The class conforms to `Sequence & IteratorProtocol` and owns the C -/// iterator handle. Each `next()` issues exactly one library `next` call; -/// the handle is destroyed eagerly on exhaustion (or on a mid-stream error) -/// and from `deinit` when iteration is abandoned early. Elements are received -/// exactly like returns of the same type. -/// -/// `next()` can't throw under `IteratorProtocol`, so for a throwing function -/// a mid-stream error ends iteration and is stored in the sequence's public -/// `error` property; for a non-throwing one a reported error stops the -/// process. -pub(crate) fn render_swift_iterator_class( - w: &mut CodeWriter, - f: &FnBinding, - it: &IteratorBinding, - err: ErrCtx, - ctx: &SwiftCtx, -) { - let protocol = it.protocol(f); - let class_name = iterator_class_name(it, ctx.c_prefix); - let elem = &it.elem; - let elem_swift = ctx.swift_type(elem); - let has_len_slot = matches!( - protocol.elem, - RetPass::String | RetPass::Bytes | RetPass::Buffer - ); - - // The `out_item` slot declaration. - let item_decl = match &protocol.elem { - RetPass::String | RetPass::Bytes | RetPass::Buffer => { - "var item: UnsafePointer? = nil".to_string() - } - RetPass::Object { .. } => "var item: OpaquePointer? = nil".to_string(), - RetPass::Direct => match elem { - Ty::Enum(name) => format!("var item = {}(rawValue: 0)", ctx.c_enum_type(name)), - Ty::Prim(Prim::Bool) => "var item = false".to_string(), - other => format!("var item: {} = 0", scalar_swift_type(other)), - }, - RetPass::Void => unreachable!("an iterator element is never void"), - }; - - w.line(format!( - "/// A lazy sequence over the `{elem_swift}` elements streamed by `{}`.", - it.launch.symbol - )); - w.line("///"); - w.line("/// Each `next()` call pulls exactly one element from the library. The"); - w.line("/// underlying iterator is destroyed eagerly on exhaustion and from `deinit`"); - w.line("/// when iteration is abandoned early."); - if err.throws { - w.line("///"); - w.line("/// If the library reports an error mid-stream, iteration ends and the"); - w.line("/// error is stored in ``error`` for the caller to inspect after the loop."); - } - w.line(format!( - "public final class {class_name}: Sequence, IteratorProtocol {{" - )); - w.indent(); - w.line("private var handle: OpaquePointer?"); - if err.throws { - w.line("/// The error that ended iteration early, if any."); - w.line("public private(set) var error: Error?"); - } - w.blank(); - w.line("init(handle: OpaquePointer) {"); - w.scope(|w| { - w.line("self.handle = handle"); - }); - w.line("}"); - w.blank(); - w.line("deinit {"); - w.scope(|w| { - w.line("destroyHandle()"); - }); - w.line("}"); - w.blank(); - w.line("private func destroyHandle() {"); - w.scope(|w| { - w.line("guard let handle = handle else { return }"); - w.line(format!("{}(handle)", it.destroy_symbol)); - w.line("self.handle = nil"); - }); - w.line("}"); - w.blank(); - w.line("/// Pulls the next element from the library, or returns `nil` once the"); - w.line("/// stream is exhausted (destroying the underlying iterator)."); - w.line(format!("public func next() -> {elem_swift}? {{")); - w.indent(); - w.line("guard let handle = handle else { return nil }"); - w.line(item_decl); - if has_len_slot { - w.line("var itemLen = 0"); - } - w.line("var err = WvError()"); - let len_arg = if has_len_slot { ", &itemLen" } else { "" }; - w.line(format!( - "if {}(handle, &item{len_arg}, &err) == 0 {{", - it.next.symbol - )); - w.indent(); - if err.throws { - w.line("if err.code != 0 {"); - w.scope(|w| { - w.line(format!("error = {}(err)", err.mapper())); - w.line(format!("{}_error_clear(&err)", ctx.c_prefix)); - }); - w.line("}"); - } else { - w.line("wvTrap(&err)"); - } - w.line("destroyHandle()"); - w.line("return nil"); - w.dedent(); - w.line("}"); - let value = receive_value(Some(elem), &protocol.elem, "item", "itemLen", ctx); - w.line(format!("return {value}")); - w.dedent(); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); -} diff --git a/crates/weaveffi-cli/src/targets/swift/codec.rs b/crates/weaveffi-cli/src/targets/swift/codec.rs index 256bc22a..141f0ad1 100644 --- a/crates/weaveffi-cli/src/targets/swift/codec.rs +++ b/crates/weaveffi-cli/src/targets/swift/codec.rs @@ -6,19 +6,26 @@ //! encoder and one decoder (the generic ones) instead of a loop inlined at //! each use. This module renders the conformances of the declared types: a //! record reads and writes its fields in order, a rich enum its `i32` tag and -//! then the variant's fields, a C-style enum its discriminant, and an -//! interface an object token carrying one strong reference. +//! then the variant's fields, and a C-style enum its discriminant (through +//! the runtime's `WvCEnum`). Interfaces get theirs from the runtime's +//! `WvObject`: an object token carrying one strong reference. use crate::codegen::CodeWriter; -use weaveffi_model::model::{EnumBinding, InterfaceBinding, StructBinding}; +use weaveffi_model::model::{EnumBinding, StructBinding}; -use crate::targets::swift::types::{deprecated_attr, swift_ident, SwiftCtx}; +use crate::targets::swift::types::{swift_ident, SwiftCtx}; /// Open a conformance extension of `ty`, marked deprecated with the type /// itself so it can name it without a warning. -fn open_extension(w: &mut CodeWriter, ty: &str, protocol: &str, deprecated: Option<&str>) { - if let Some(msg) = deprecated { - w.line(deprecated_attr(msg)); +fn open_extension( + w: &mut CodeWriter, + ty: &str, + protocol: &str, + deprecated: &Option, + ctx: &SwiftCtx, +) { + if let Some(attr) = ctx.deprecated_attr(deprecated) { + w.line(attr); } w.line(format!("extension {ty}: {protocol} {{")); } @@ -26,8 +33,8 @@ fn open_extension(w: &mut CodeWriter, ty: &str, protocol: &str, deprecated: Opti /// Render a C-style enum's conformance: its discriminant, via the runtime's /// `WvCEnum` defaults. pub(crate) fn render_c_enum_codec(w: &mut CodeWriter, e: &EnumBinding, ctx: &SwiftCtx) { - if let Some(msg) = &e.deprecated { - w.line(deprecated_attr(msg)); + if let Some(attr) = ctx.deprecated_attr(&e.deprecated) { + w.line(attr); } w.line(format!("extension {}: WvCEnum {{}}", ctx.ty_name(&e.name))); w.blank(); @@ -36,7 +43,7 @@ pub(crate) fn render_c_enum_codec(w: &mut CodeWriter, e: &EnumBinding, ctx: &Swi /// Render a record's conformance: its fields in declaration order. pub(crate) fn render_record_codec(w: &mut CodeWriter, s: &StructBinding, ctx: &SwiftCtx) { let ty = ctx.ty_name(&s.name); - open_extension(w, &ty, "WvCodable", s.deprecated.as_deref()); + open_extension(w, &ty, "WvCodable", &s.deprecated, ctx); w.scope(|w| { let reader = if s.fields.is_empty() { "_" } else { "r" }; let args = s @@ -71,7 +78,7 @@ pub(crate) fn render_record_codec(w: &mut CodeWriter, s: &StructBinding, ctx: &S /// buffer. pub(crate) fn render_rich_enum_codec(w: &mut CodeWriter, e: &EnumBinding, ctx: &SwiftCtx) { let ty = ctx.ty_name(&e.name); - open_extension(w, &ty, "WvCodable", e.deprecated.as_deref()); + open_extension(w, &ty, "WvCodable", &e.deprecated, ctx); w.scope(|w| { w.line(format!( "static func wvRead(_ r: inout WvReader) -> {ty} {{" @@ -131,19 +138,3 @@ pub(crate) fn render_rich_enum_codec(w: &mut CodeWriter, e: &EnumBinding, ctx: & w.line("}"); w.blank(); } - -/// Render an interface's conformance: an object token. Reading adopts the -/// token's reference into a new wrapper; writing mints a fresh one, so the -/// wrapper keeps its own. -pub(crate) fn render_interface_codec(w: &mut CodeWriter, iface: &InterfaceBinding, ctx: &SwiftCtx) { - let ty = ctx.ty_name(&iface.name); - open_extension(w, &ty, "WvCodable", iface.deprecated.as_deref()); - w.scope(|w| { - w.line(format!( - "static func wvRead(_ r: inout WvReader) -> {ty} {{ {ty}(ptr: r.readObject()) }}" - )); - w.line("func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) }"); - }); - w.line("}"); - w.blank(); -} diff --git a/crates/weaveffi-cli/src/targets/swift/docs.rs b/crates/weaveffi-cli/src/targets/swift/docs.rs index 4f6c4832..7cbc4385 100644 --- a/crates/weaveffi-cli/src/targets/swift/docs.rs +++ b/crates/weaveffi-cli/src/targets/swift/docs.rs @@ -1,32 +1,34 @@ //! Swift doc-comment emission in Swift's markup: an item's summary and //! discussion, then its callouts (`- Parameter`, `- Returns:`, `- Throws:`). -use crate::codegen::common::DocCommentStyle; +use crate::codegen::common::{wrap, DocCommentStyle}; use crate::codegen::CodeWriter; -use weaveffi_model::model::ParamBinding; -/// Emit the doc comment of a callable: its own doc, then a -/// `/// - Parameter name: ...` callout for each documented parameter, then -/// `callouts` (each a `- Returns:` or `- Throws:` line without the `///`), -/// separated from the discussion by an empty doc line. Callers pass params -/// whose names are already camel-cased and keyword-escaped (see -/// `calls::camel_params`), so the labels match the emitted signature. +use crate::targets::swift::types::SwiftCtx; + +/// Emit the doc comment of a declaration: its own doc, then a +/// `/// - Parameter name: ...` callout for each documented parameter (each +/// `(Swift name, doc)`), then `callouts` (each a `- Returns:` or +/// `- Throws:` item without the `///`, wrapped here), separated from the +/// discussion by an empty doc line. Backticked API identifiers are +/// rewritten to their Swift spelling. pub(crate) fn emit_fn_doc( w: &mut CodeWriter, + ctx: &SwiftCtx, doc: &Option, - params: &[ParamBinding], + params: &[(String, &Option)], callouts: &[String], ) { let mut lines = Vec::new(); - for p in params { - let Some(pdoc) = p.doc.as_deref().map(str::trim) else { + for (name, pdoc) in params { + let Some(pdoc) = ctx.doc(pdoc) else { continue; }; let mut pdoc = pdoc.lines(); let Some(first) = pdoc.next() else { continue; }; - lines.push(format!("/// - Parameter {}: {first}", p.name)); + lines.push(format!("/// - Parameter {name}: {first}")); for line in pdoc { lines.push(if line.is_empty() { "///".to_string() @@ -35,9 +37,20 @@ pub(crate) fn emit_fn_doc( }); } } - lines.extend(callouts.iter().map(|c| format!("/// {c}"))); - let has_doc = doc.as_deref().is_some_and(|d| !d.trim().is_empty()); - w.doc(doc, DocCommentStyle::TripleSlash); + // Callouts are generated prose: wrap them, continuing each list item + // with an indent. + for callout in callouts { + for (i, line) in wrap(callout, 74).lines().enumerate() { + lines.push(if i == 0 { + format!("/// {line}") + } else { + format!("/// {line}") + }); + } + } + let doc = ctx.doc(doc); + let has_doc = doc.is_some(); + w.doc(&doc, DocCommentStyle::TripleSlash); if has_doc && !lines.is_empty() { w.line("///"); } @@ -45,3 +58,17 @@ pub(crate) fn emit_fn_doc( w.line(line); } } + +/// Emit a declaration's own doc comment, rewritten to Swift spelling, and +/// its deprecation attribute. +pub(crate) fn emit_decl_doc( + w: &mut CodeWriter, + ctx: &SwiftCtx, + doc: &Option, + deprecated: &Option, +) { + w.doc(&ctx.doc(doc), DocCommentStyle::TripleSlash); + if let Some(attr) = ctx.deprecated_attr(deprecated) { + w.line(attr); + } +} diff --git a/crates/weaveffi-cli/src/targets/swift/entities.rs b/crates/weaveffi-cli/src/targets/swift/entities.rs index 9f3d0693..c27e90de 100644 --- a/crates/weaveffi-cli/src/targets/swift/entities.rs +++ b/crates/weaveffi-cli/src/targets/swift/entities.rs @@ -1,72 +1,204 @@ -//! Entity rendering: enums, rich enums, records, interfaces, typed error -//! domains, and the recursive module walk emitting them (callback interfaces -//! render through [`crate::targets::swift::callbacks`], value-buffer codecs -//! through [`crate::targets::swift::codec`]). +//! Entity rendering: error domains, enums, rich enums, records, interfaces, +//! and the recursive module walk emitting them (callback interfaces render +//! through [`crate::targets::swift::callbacks`], value-buffer codecs through +//! [`crate::targets::swift::codec`]). use crate::codegen::common::DocCommentStyle; +use crate::codegen::errors::ErrorTable; use crate::codegen::CodeWriter; +use crate::lang; use heck::ToUpperCamelCase; -use weaveffi_model::model::{ - CallShape, EnumBinding, ErrorBinding, FieldBinding, FnBinding, InterfaceBinding, Model, - ModuleBinding, StructBinding, -}; +use weaveffi_model::model::{EnumBinding, InterfaceBinding, Model, ModuleBinding, StructBinding}; use crate::targets::swift::callbacks::render_swift_callback_interface; -use crate::targets::swift::calls::{ - camel_params, render_callable, render_swift_iterator_class, ErrCtx, Receiver, -}; +use crate::targets::swift::calls::{render_callable, Receiver}; use crate::targets::swift::codec::{ - render_c_enum_codec, render_interface_codec, render_record_codec, render_rich_enum_codec, -}; -use crate::targets::swift::types::{ - deprecated_attr, swift_ident, swift_member, swift_str, SwiftCtx, + render_c_enum_codec, render_record_codec, render_rich_enum_codec, }; +use crate::targets::swift::docs::emit_decl_doc; +use crate::targets::swift::types::{swift_ident, swift_member, swift_str, SwiftCtx}; -/// The PascalCase helper stem of an error domain, naming its -/// `wvCheck{Stem}`/`wvMap{Stem}`/`wvReport{Stem}` helpers (derived from the -/// *declaring* module's path, so inheriting submodules reference the -/// ancestor's helpers). -fn stem(eb: &ErrorBinding) -> String { - eb.owner_path.to_upper_camel_case() -} +/// Members every domain error enum declares, which a code's case must not +/// redeclare. +const DOMAIN_MEMBERS: &[&str] = &["errorCode", "errorDescription", "message"]; -/// The `(type name, stem)` of the error domain in scope for `module`. -fn domain_of(model: &Model, module: &ModuleBinding) -> Option<(String, String)> { - model - .error_domain(module) - .map(|e| (e.type_name.clone(), stem(e))) -} +/// The leading associated value of every declared code's case. +const MESSAGE_LABEL: &str = "message"; -/// Borrow a `(type name, stem)` pair. -fn as_refs(d: &Option<(String, String)>) -> Option<(&str, &str)> { - d.as_ref().map(|(t, s)| (t.as_str(), s.as_str())) -} +/// Render one error domain as a `public enum {TypeName}: Error`: one +/// lowerCamel case per code carrying the message plus, for codes that +/// declare fields, one labeled associated value per field; and an `unknown` +/// case for codes a newer library added (domains are open). Its internal +/// `WvDomain` conformance decodes a filled error slot (the payload for +/// codes with fields) and reports the error back from a callback method. +/// +/// Domain codes are validated positive; a failed call maps every negative +/// (runtime) code to the runtime error before it gets here. +fn render_swift_error(w: &mut CodeWriter, table: &ErrorTable, ctx: &SwiftCtx) { + let ty = ctx.ty_name(&table.type_name); + let cases: Vec = table + .codes + .iter() + .map(|row| lang::escape_member(&swift_ident(&row.code.name), DOMAIN_MEMBERS)) + .collect(); + let unknown = lang::escape_member( + "unknown", + &cases.iter().map(String::as_str).collect::>(), + ); + // A field named like the leading message gets a `_`. + let field_label = |name: &str| lang::escape_member(&swift_ident(name), &[MESSAGE_LABEL]); -/// The conformance list of a value type: `Hashable` (which implies -/// `Equatable`) when every field is, and always `Sendable`. -fn value_conformances<'t>( - ctx: &SwiftCtx, - fields: impl IntoIterator, -) -> &'static str { - if fields.into_iter().all(|f| ctx.is_hashable(&f.ty)) { - "Hashable, Sendable" - } else { - "Sendable" + w.line(format!( + "/// The `{}` error domain of the `{}` module.", + table.domain.name, table.module.dot_path + )); + w.line("///"); + w.line("/// Every case carries the message the library reported (the code's"); + w.line("/// documented message when it sent none) and the code's fields."); + w.line(format!( + "public enum {}: Error, LocalizedError, Hashable, Sendable {{", + table.type_name + )); + w.indent(); + for (row, case) in table.codes.iter().zip(&cases) { + emit_decl_doc(w, ctx, &row.code.doc, &None); + let mut parts = vec![format!("{MESSAGE_LABEL}: String")]; + parts.extend( + row.code + .fields + .iter() + .map(|f| format!("{}: {}", field_label(&f.name), ctx.swift_type(&f.ty))), + ); + w.line(format!("case {case}({})", parts.join(", "))); } + w.line("/// A code these bindings don't declare, from a newer library."); + w.line(format!("case {unknown}(code: Int32, message: String)")); + w.blank(); + w.line("/// The numeric code the library reported."); + w.line("public var errorCode: Int32 {"); + w.scope(|w| { + w.line("switch self {"); + for (row, case) in table.codes.iter().zip(&cases) { + w.line(format!("case .{case}: return {}", row.code.value)); + } + w.line(format!("case let .{unknown}(code, _): return code")); + w.line("}"); + }); + w.line("}"); + w.blank(); + w.line("/// The message the library reported, or the code's documented message."); + w.line("public var message: String {"); + w.scope(|w| { + w.line("switch self {"); + for (row, case) in table.codes.iter().zip(&cases) { + let mut binds = vec![MESSAGE_LABEL.to_string()]; + binds.extend(row.code.fields.iter().map(|_| "_".to_string())); + w.line(format!( + "case let .{case}({}): return {MESSAGE_LABEL}", + binds.join(", ") + )); + } + w.line(format!( + "case let .{unknown}(_, {MESSAGE_LABEL}): return {MESSAGE_LABEL}" + )); + w.line("}"); + }); + w.line("}"); + w.blank(); + w.line(format!( + "public var errorDescription: String? {{ {MESSAGE_LABEL} }}" + )); + w.dedent(); + w.line("}"); + w.blank(); + + w.line(format!("extension {ty}: WvDomain {{")); + w.indent(); + w.line("init(wvError err: WvError) {"); + w.scope(|w| { + w.line("let message = wvErrorMessage(err)"); + w.line("switch err.code {"); + for (row, case) in table.codes.iter().zip(&cases) { + let c = row.code; + let message = format!( + "message: message.isEmpty ? \"{}\" : message", + swift_str(&c.message) + ); + w.line(format!("case {}:", c.value)); + w.scope(|w| { + if c.fields.is_empty() { + w.line(format!("self = .{case}({message})")); + } else { + let mut args = vec![message]; + args.extend( + c.fields + .iter() + .map(|f| format!("{}: r.read()", field_label(&f.name))), + ); + w.line("var r = wvPayload(err)"); + w.line(format!("self = .{case}({})", args.join(", "))); + w.line("r.finish()"); + } + }); + } + w.line("default:"); + w.scope(|w| { + w.line(format!( + "self = .{unknown}(code: err.code, message: message)" + )); + }); + w.line("}"); + }); + w.line("}"); + w.blank(); + w.line("func wvReport(_ outErr: UnsafeMutablePointer?) {"); + w.scope(|w| { + w.line("switch self {"); + for (row, case) in table.codes.iter().zip(&cases) { + let c = row.code; + if c.fields.is_empty() { + w.line(format!("case let .{case}(message):")); + w.scope(|w| { + w.line(format!("wvSetError(outErr, {}, message)", c.value)); + }); + } else { + let binds = (0..c.fields.len()) + .map(|i| format!("v{i}")) + .collect::>(); + w.line(format!("case let .{case}(message, {}):", binds.join(", "))); + w.scope(|w| { + w.line("var payload = WvWriter()"); + for b in &binds { + w.line(format!("payload.write({b})")); + } + w.line(format!("wvSetError(outErr, {}, message, payload)", c.value)); + }); + } + } + w.line(format!("case let .{unknown}(code, message):")); + w.scope(|w| { + w.line("wvSetError(outErr, code, message)"); + }); + w.line("}"); + }); + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); } /// Render a C-style enum as an `Int32` raw-value Swift enum, one case per -/// variant, plus its codec. Wrappers convert to and from the imported C -/// type at the call. +/// variant, plus its codec. Wrappers pass the raw value, which is the C +/// type's `int32_t`. fn render_swift_enum(w: &mut CodeWriter, e: &EnumBinding, ctx: &SwiftCtx) { - w.doc(&e.doc, DocCommentStyle::TripleSlash); - if let Some(msg) = &e.deprecated { - w.line(deprecated_attr(msg)); - } - w.line(format!("public enum {}: Int32, Sendable {{", e.name)); + emit_decl_doc(w, ctx, &e.doc, &e.deprecated); + w.line(format!( + "public enum {}: Int32, CaseIterable, Sendable {{", + e.name + )); w.scope(|w| { for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::TripleSlash); + w.doc(&ctx.doc(&v.doc), DocCommentStyle::TripleSlash); w.line(format!("case {} = {}", swift_ident(&v.name), v.value)); } }); @@ -79,19 +211,11 @@ fn render_swift_enum(w: &mut CodeWriter, e: &EnumBinding, ctx: &SwiftCtx) { /// values: one case per variant, with labeled associated values matching the /// variant's field names, plus its codec. fn render_swift_rich_enum(w: &mut CodeWriter, e: &EnumBinding, ctx: &SwiftCtx) { - let fields = e.variants.iter().flat_map(|v| v.fields.iter()); - w.doc(&e.doc, DocCommentStyle::TripleSlash); - if let Some(msg) = &e.deprecated { - w.line(deprecated_attr(msg)); - } - w.line(format!( - "public enum {}: {} {{", - e.name, - value_conformances(ctx, fields) - )); + emit_decl_doc(w, ctx, &e.doc, &e.deprecated); + w.line(format!("public enum {}: Hashable, Sendable {{", e.name)); w.scope(|w| { for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::TripleSlash); + w.doc(&ctx.doc(&v.doc), DocCommentStyle::TripleSlash); let case_name = swift_ident(&v.name); if v.fields.is_empty() { w.line(format!("case {case_name}")); @@ -111,22 +235,16 @@ fn render_swift_rich_enum(w: &mut CodeWriter, e: &EnumBinding, ctx: &SwiftCtx) { render_rich_enum_codec(w, e, ctx); } -/// Render a record as a plain Swift struct with one `public var` per field -/// and an explicit public memberwise initializer (the synthesized one is -/// internal), plus its codec. +/// Render a record as a plain `Hashable` Swift struct with one `public var` +/// per field and an explicit public memberwise initializer (the synthesized +/// one is internal), plus its codec. Every field type is `Hashable` +/// (interface wrappers by identity), so every record is. fn render_swift_struct(w: &mut CodeWriter, s: &StructBinding, ctx: &SwiftCtx) { - w.doc(&s.doc, DocCommentStyle::TripleSlash); - if let Some(msg) = &s.deprecated { - w.line(deprecated_attr(msg)); - } - w.line(format!( - "public struct {}: {} {{", - s.name, - value_conformances(ctx, &s.fields) - )); + emit_decl_doc(w, ctx, &s.doc, &s.deprecated); + w.line(format!("public struct {}: Hashable, Sendable {{", s.name)); w.indent(); for f in &s.fields { - w.doc(&f.doc, DocCommentStyle::TripleSlash); + w.doc(&ctx.doc(&f.doc), DocCommentStyle::TripleSlash); w.line(format!( "public var {}: {}", swift_ident(&f.name), @@ -157,212 +275,25 @@ fn render_swift_struct(w: &mut CodeWriter, s: &StructBinding, ctx: &SwiftCtx) { render_record_codec(w, s, ctx); } -/// Render one declaring module's typed error surface: a `public enum -/// {TypeName}: Error` whose lowerCamel cases carry the message plus, for -/// codes that declare payload fields, one labeled associated value per -/// field. Also emits the file-scope helpers: `wvMap{Stem}` and -/// `wvCheck{Stem}` convert a filled error slot into it (decoding the payload -/// for codes with fields), and, when a callback method can throw it, -/// `wvReport{Stem}` reports it back through a callback's error slot. -/// -/// Only declared codes get typed cases. Domain codes are validated positive, -/// so the mapper's `default` arm is what every reserved negative runtime -/// code falls through to: the runtime error, or `CancellationError` for `-5`. -fn render_swift_error( - w: &mut CodeWriter, - module: &ModuleBinding, - eb: &ErrorBinding, - ctx: &SwiftCtx, -) { - let stem = stem(eb); - let ty = &eb.type_name; - - w.doc( - &Some(format!( - "Typed errors reported by the `{}` module.", - module.segments.join(".") - )), - DocCommentStyle::TripleSlash, - ); - w.line(format!( - "public enum {ty}: Error, LocalizedError, Sendable {{" - )); - w.indent(); - for c in &eb.codes { - w.doc(&c.doc, DocCommentStyle::TripleSlash); - let mut parts = vec!["message: String".to_string()]; - parts.extend( - c.fields - .iter() - .map(|f| format!("{}: {}", swift_ident(&f.name), ctx.swift_type(&f.ty))), - ); - w.line(format!( - "case {}({})", - swift_ident(&c.name), - parts.join(", ") - )); - } - w.blank(); - w.line("/// The message the library reported, or the code's documented default."); - w.line("public var errorDescription: String? {"); - w.scope(|w| { - w.line("switch self {"); - for c in &eb.codes { - // Bind only the message; wildcard the payload fields. - let mut binds = vec!["message".to_string()]; - binds.extend(c.fields.iter().map(|_| "_".to_string())); - w.line(format!( - "case let .{}({}): return message", - swift_ident(&c.name), - binds.join(", ") - )); - } - w.line("}"); - }); - w.line("}"); - w.blank(); - w.line("/// The numeric ABI code carried by this error."); - w.line("public var errorCode: Int32 {"); - w.scope(|w| { - w.line("switch self {"); - for c in &eb.codes { - w.line(format!( - "case .{}: return {}", - swift_ident(&c.name), - c.value - )); - } - w.line("}"); - }); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - - // `wvMap{Stem}`: code -> typed case (default message when the slot - // carried none), decoding the payload buffer for codes with fields. - w.line(format!("func wvMap{stem}(_ err: WvError) -> Error {{")); - w.indent(); - w.line("let message = wvErrorMessage(err)"); - w.line("switch err.code {"); - for c in &eb.codes { - let case_name = swift_ident(&c.name); - let message_arg = format!( - "message: message.isEmpty ? \"{}\" : message", - swift_str(&c.message) - ); - if c.fields.is_empty() { - w.line(format!( - "case {}: return {ty}.{case_name}({message_arg})", - c.value - )); - } else { - let mut args = vec![message_arg]; - args.extend( - c.fields - .iter() - .map(|f| format!("{}: r.read()", swift_ident(&f.name))), - ); - w.line(format!("case {}:", c.value)); - w.scope(|w| { - w.line("var r = WvReader(err.payload_ptr, err.payload_len)"); - w.line(format!("let error = {ty}.{case_name}({})", args.join(", "))); - w.line("r.finish()"); - w.line("return error"); - }); - } - } - w.line("default: return wvRuntimeError(err)"); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - - w.line(format!( - "func wvCheck{stem}(_ err: inout WvError) throws {{" - )); - w.scope(|w| { - w.line("guard err.code != 0 else { return }"); - w.line(format!("let error = wvMap{stem}(err)")); - w.line(format!("{}_error_clear(&err)", ctx.c_prefix)); - w.line("throw error"); - }); - w.line("}"); - w.blank(); - - if !ctx.reports(eb) { - return; - } - w.line(format!( - "/// Reports a ``{ty}`` thrown by a callback method declared to throw it: its" - )); - w.line("/// code, its message, and its fields as the payload. Any other error is left"); - w.line("/// to the caller, which reports a callback failure."); - w.line(format!( - "func wvReport{stem}(_ error: Error, _ outErr: UnsafeMutablePointer?) -> Bool {{" - )); - w.scope(|w| { - w.line(format!( - "guard let error = error as? {ty} else {{ return false }}" - )); - w.line("switch error {"); - for c in &eb.codes { - let case_name = swift_ident(&c.name); - if c.fields.is_empty() { - w.line(format!("case let .{case_name}(message):")); - w.scope(|w| { - w.line(format!("wvSetError(outErr, {}, message)", c.value)); - }); - } else { - let binds = (0..c.fields.len()) - .map(|i| format!("v{i}")) - .collect::>(); - w.line(format!( - "case let .{case_name}(message, {}):", - binds.join(", ") - )); - w.scope(|w| { - w.line("var payload = WvWriter()"); - for b in &binds { - w.line(format!("payload.write({b})")); - } - w.line(format!("wvSetError(outErr, {}, message, payload)", c.value)); - }); - } - } - w.line("}"); - w.line("return true"); - }); - w.line("}"); - w.blank(); -} - /// Render one interface as a `public final class` owning one strong /// reference to its object: a stored `ptr`, an internal reference-adopting /// `init(ptr:)`, a `deinit` that releases the reference exactly once through /// the destroy symbol, and an internal `clonePtr()` that mints a second /// reference for positions that take ownership (an object token inside a -/// value buffer, a callback method's return). The native object is -/// thread-safe, so the class is `@unchecked Sendable`. +/// value buffer, a callback method's return). It's `Hashable` by identity: +/// two wrappers are equal when they hold the same native object (`_clone` +/// keeps the pointer value). The native object is thread-safe, so the class +/// is `@unchecked Sendable`. /// /// The constructor named `new` surfaces as `public init`; every other /// constructor becomes a `public static func` factory. Methods are instance /// funcs passing `ptr` as the leading C argument; statics are plain `public /// static func`s. -fn render_swift_interface( - w: &mut CodeWriter, - module: &ModuleBinding, - iface: &InterfaceBinding, - ctx: &SwiftCtx, -) { - let domain = domain_of(ctx.model, module); - w.doc(&iface.doc, DocCommentStyle::TripleSlash); - if let Some(msg) = &iface.deprecated { - w.line(deprecated_attr(msg)); - } +fn render_swift_interface(w: &mut CodeWriter, iface: &InterfaceBinding, ctx: &SwiftCtx) { + let name = &iface.name; + emit_decl_doc(w, ctx, &iface.doc, &iface.deprecated); w.line(format!( - "public final class {}: @unchecked Sendable {{", - iface.name + "public final class {name}: WvObject, Hashable, @unchecked Sendable {{" )); w.indent(); w.line("let ptr: OpaquePointer"); @@ -379,13 +310,26 @@ fn render_swift_interface( }); w.line("}"); w.blank(); - w.line("/// Returns a new strong reference to the same object, for a position that"); - w.line("/// takes ownership of it."); w.line("func clonePtr() -> OpaquePointer {"); w.scope(|w| { w.line(format!("wvNonNull({}(ptr))", iface.clone_symbol)); }); w.line("}"); + w.blank(); + w.line("/// Whether two wrappers hold the same native object."); + w.line(format!( + "public static func == (lhs: {name}, rhs: {name}) -> Bool {{" + )); + w.scope(|w| { + w.line("lhs.ptr == rhs.ptr"); + }); + w.line("}"); + w.blank(); + w.line("public func hash(into hasher: inout Hasher) {"); + w.scope(|w| { + w.line("hasher.combine(ptr)"); + }); + w.line("}"); let constructors = iface.constructors.iter().map(|f| { let receiver = if f.name == "new" { @@ -399,25 +343,25 @@ fn render_swift_interface( .chain(iface.methods.iter().map(|f| (f, Receiver::Instance))) .chain(iface.statics.iter().map(|f| (f, Receiver::Static))); for (f, receiver) in members { - let f = camel_params(f); - let err = ErrCtx::for_fn(&f, as_refs(&domain)); w.blank(); - render_callable(w, &f, &swift_member(&f.name), receiver, err, ctx); + render_callable(w, f, &swift_member(&f.name), receiver, ctx); } w.dedent(); w.line("}"); w.blank(); - render_interface_codec(w, iface, ctx); } -/// Emit every file-scope type one module contributes: the typed error -/// surface, enums and records with their codecs, callback-interface -/// protocols with their vtables, interface classes, and the sequence classes -/// backing `iter` callables. -pub(crate) fn render_swift_module_types(w: &mut CodeWriter, mb: &ModuleBinding, ctx: &SwiftCtx) { - if let Some(eb) = mb.errors.as_ref() { - render_swift_error(w, mb, eb, ctx); +/// Emit every error domain of the API. +pub(crate) fn render_swift_errors(w: &mut CodeWriter, tables: &[ErrorTable], ctx: &SwiftCtx) { + for table in tables { + render_swift_error(w, table, ctx); } +} + +/// Emit every file-scope type one module contributes: enums and records +/// with their codecs, callback-interface protocols with their vtables, and +/// interface classes. +pub(crate) fn render_swift_module_types(w: &mut CodeWriter, mb: &ModuleBinding, ctx: &SwiftCtx) { for e in &mb.enums { if e.is_rich() { render_swift_rich_enum(w, e, ctx); @@ -428,17 +372,11 @@ pub(crate) fn render_swift_module_types(w: &mut CodeWriter, mb: &ModuleBinding, for s in &mb.structs { render_swift_struct(w, s, ctx); } - let domain = domain_of(ctx.model, mb); for cb in &mb.callback_interfaces { - render_swift_callback_interface(w, cb, as_refs(&domain), ctx); + render_swift_callback_interface(w, cb, ctx); } for i in &mb.interfaces { - render_swift_interface(w, mb, i, ctx); - } - for f in mb.callables() { - if let CallShape::Iterator(it) = &f.shape { - render_swift_iterator_class(w, f, it, ErrCtx::for_fn(f, as_refs(&domain)), ctx); - } + render_swift_interface(w, i, ctx); } } @@ -450,8 +388,7 @@ pub(crate) fn render_swift_namespace( mb: &ModuleBinding, ctx: &SwiftCtx, ) { - let domain = domain_of(model, mb); - w.doc(&mb.doc, DocCommentStyle::TripleSlash); + w.doc(&ctx.doc(&mb.doc), DocCommentStyle::TripleSlash); w.line(format!("public enum {} {{", mb.name.to_upper_camel_case())); w.indent(); let mut first = true; @@ -460,9 +397,7 @@ pub(crate) fn render_swift_namespace( w.blank(); } first = false; - let f: FnBinding = camel_params(f); - let err = ErrCtx::for_fn(&f, as_refs(&domain)); - render_callable(w, &f, &swift_ident(&f.name), Receiver::Static, err, ctx); + render_callable(w, f, &swift_ident(&f.name), Receiver::Static, ctx); } for sub in model.children(mb) { if !first { diff --git a/crates/weaveffi-cli/src/targets/swift/mod.rs b/crates/weaveffi-cli/src/targets/swift/mod.rs index c79201a6..3bfd7f2b 100644 --- a/crates/weaveffi-cli/src/targets/swift/mod.rs +++ b/crates/weaveffi-cli/src/targets/swift/mod.rs @@ -1,25 +1,29 @@ //! Swift binding generator. //! //! Emits a standalone SwiftPM package: the Swift wrapper module over the C -//! ABI, plus a `C{Module}` system-library target holding a copy of the C -//! header and a module map that links the native library. Implements -//! [`LanguageBackend`]; the shared driver bridges it into the generator -//! pipeline. +//! ABI (the API file plus a fixed runtime file), and a `C{Module}` +//! system-library target holding a copy of the C header and a module map +//! that links the native library. //! -//! Records, rich enums, optionals, lists, and maps cross the C ABI as value -//! buffers. The wrapper ships a small private writer/reader pair -//! (`WvWriter`/`WvReader`, decoding in place from the library's memory) and a -//! `WvCodable` protocol: every primitive conforms, optionals, arrays, and -//! dictionaries conform generically, and each record, enum, and interface -//! conforms where it's declared, so records surface as plain Swift structs -//! and rich enums as native Swift enums with associated values, and every -//! composite type has one codec. Objects surface as `final class` wrappers -//! owning one strong reference each, callback interfaces as class-bound -//! `Sendable` protocols whose implementations cross the boundary through a -//! process-wide vtable of `@convention(c)` trampolines, and async functions -//! as `async` functions over checked continuations, with task cancellation -//! wired to the native cancel token. Before the first call the wrapper checks -//! the ABI revision and every top-level module's contract table. +//! Records, rich enums, and every optional, list, and map that isn't an +//! optional scalar or a numeric list cross the C ABI as value buffers. The +//! runtime ships a small private writer/reader pair (`WvWriter`/`WvReader`, +//! decoding in place from the library's memory) and a `WvCodable` protocol: +//! every primitive conforms, optionals, arrays, and dictionaries conform +//! generically, and each record, enum, and interface conforms where it's +//! declared, so records surface as plain `Hashable` Swift structs and rich +//! enums as native Swift enums with associated values. Optional scalars +//! (`T?`) cross directly as a presence flag and a value, and numeric lists +//! (`[T]`) as the array's own storage. Objects surface as `final class` +//! wrappers owning one strong reference each, `Hashable` by identity; +//! callback interfaces as class-bound `Sendable` protocols whose +//! implementations cross through a process-wide vtable of `@convention(c)` +//! trampolines; iterators as the runtime's generic `NativeSequence`; and +//! async functions as `async` functions over checked continuations, with task +//! cancellation wired to the native cancel token. Error domains are open +//! `Error` enums. `{Module}Library.check()` reports a library that doesn't +//! match the bindings as a thrown error; a call made while it doesn't +//! match stops the process. //! //! The fixed Swift sources (runtime, manifest, module map, README) live //! under `runtime/` and are spliced with the package's names. @@ -34,22 +38,28 @@ mod runtime; #[cfg(test)] mod tests; mod types; +mod xcframework; -use crate::backend::{LanguageBackend, OutputFile}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{Artifact, PackageContext, PackagedFile}; use crate::platform::Os; use crate::targets::c::{header_name, render_c_header_from_model}; +use crate::targets::{Linkage, Target}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; use camino::{Utf8Path, Utf8PathBuf}; +use miette::Result; use serde::{Deserialize, Serialize}; use weaveffi_model::model::Model; -use crate::targets::swift::entities::{render_swift_module_types, render_swift_namespace}; +use crate::codegen::errors; +use crate::targets::swift::entities::{ + render_swift_errors, render_swift_module_types, render_swift_namespace, +}; use crate::targets::swift::package::{ render_modulemap, render_package_swift, render_packaged_readme, CSource, }; -use crate::targets::swift::runtime::{render_load_checks, render_runtime, Names}; +use crate::targets::swift::runtime::{render_contract_checks, render_runtime, Names, RUNTIME_FILE}; use crate::targets::swift::types::SwiftCtx; /// The URL a packaged manifest points its binary target at when @@ -158,6 +168,7 @@ impl Layout { config: &SwiftConfig, ) -> Vec<(Utf8PathBuf, String)> { let c_dir = dir.join("Sources").join(&self.c_module); + let swift_dir = dir.join("Sources").join(&self.module); let swift_file = format!("{}.swift", self.module); vec![ ( @@ -173,44 +184,67 @@ impl Layout { render_c_header_from_model(model, &self.header), ), ( - dir.join("Sources").join(&self.module).join(&swift_file), + swift_dir.join(&swift_file), render_swift_wrapper(self, model, &swift_file), ), + (swift_dir.join(RUNTIME_FILE), self.runtime(model)), ] } + + /// The runtime source file, framed with the prelude and trailer. + fn runtime(&self, model: &Model) -> String { + format!( + "{}{}\n{}", + render_prelude(CommentStyle::DoubleSlash), + render_runtime(model, &self.names()), + render_trailer(CommentStyle::DoubleSlash, RUNTIME_FILE), + ) + } } /// Swift backend: emits a standalone SwiftPM package wrapping the C ABI. -pub struct SwiftGenerator; +pub struct SwiftGenerator { + config: SwiftConfig, +} -impl LanguageBackend for SwiftGenerator { - type Config = SwiftConfig; +impl From for SwiftGenerator { + fn from(config: SwiftConfig) -> Self { + Self { config } + } +} +impl Target for SwiftGenerator { fn name(&self) -> &'static str { "swift" } - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { + fn render(&self, model: &Model) -> Vec { + let config = &self.config; Layout::new(model, config) - .sources(model, &out_dir.join("swift"), config) + .sources(model, &Utf8PathBuf::new(), config) .into_iter() .map(|(path, contents)| OutputFile::new(path, contents)) .collect() } + fn linkage(&self) -> Linkage { + Linkage::Link + } + + fn fixed_files(&self) -> &'static [&'static str] { + &["Package.swift", "module.modulemap", RUNTIME_FILE] + } + /// The SwiftPM package at `swift/{Module}/` whose binary target is the - /// `C{Module}.xcframework.zip` archive `weaveffi package` assembles from - /// the Apple static libraries (at the configured `xcframework_url`, with - /// its checksum). Without an archive (no Apple platform built, or not on - /// macOS) there is nothing to package. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { - let Some(archive) = ctx.xcframework else { - return Some(Vec::new()); + /// `C{Module}.xcframework.zip` archive assembled here from the Apple + /// static libraries (at the configured `xcframework_url`, with its + /// checksum), written next to it with a `.sha256` file. Without an + /// archive (no Apple platform built, or no Xcode) there is nothing to + /// package. + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { + let config = &self.config; + let Some(archive) = xcframework::assemble(model, &config.c_module_name(model), ctx)? else { + return Ok(Vec::new()); }; let layout = Layout::new(model, config); let names = layout.names(); @@ -240,21 +274,30 @@ impl LanguageBackend for SwiftGenerator { format!("Sources/{}/{swift_file}", layout.module), render_swift_wrapper(&layout, model, &swift_file), ), + PackagedFile::text( + format!("Sources/{}/{RUNTIME_FILE}", layout.module), + layout.runtime(model), + ), PackagedFile::text( "README.md", render_packaged_readme(&names, &slices, &url, &archive.checksum), ), ]; - Some(vec![Artifact::directory( - format!("swift/{}", layout.module), - files, - )]) + let checksum_file = format!("{} {}\n", archive.checksum, archive.file_name); + Ok(vec![ + Artifact::single_file( + format!("swift/{}.sha256", archive.file_name), + checksum_file.into_bytes(), + ), + Artifact::single_file(format!("swift/{}", archive.file_name), archive.bytes), + Artifact::directory(format!("swift/{}", layout.module), files), + ]) } } -/// Render the complete Swift wrapper file: prelude and imports, the private -/// runtime, the load-time checks, every module's file-scope types, and one -/// namespace `enum` per top-level module. +/// Render the Swift API file: prelude and imports, the contract rows the +/// load check compares, every error domain, every module's file-scope +/// types, and one namespace `enum` per top-level module. fn render_swift_wrapper(layout: &Layout, model: &Model, filename: &str) -> String { let ctx = SwiftCtx::new(model, &layout.module); let mut w = CodeWriter::four_space(); @@ -262,11 +305,10 @@ fn render_swift_wrapper(layout: &Layout, model: &Model, filename: &str) -> Strin w.line(format!("import {}", layout.c_module)); w.line("import Foundation"); w.blank(); - w.raw(render_runtime(model, &layout.names(), &ctx.runtime_error)); - w.blank(); - render_load_checks(&mut w, model); + render_contract_checks(&mut w, model, &ctx.library_type); w.line("// MARK: - API"); w.blank(); + render_swift_errors(&mut w, &errors::tables(model, "Error"), &ctx); for mb in &model.modules { render_swift_module_types(&mut w, mb, &ctx); } diff --git a/crates/weaveffi-cli/src/targets/swift/runtime.rs b/crates/weaveffi-cli/src/targets/swift/runtime.rs index 98d01f2c..f3466114 100644 --- a/crates/weaveffi-cli/src/targets/swift/runtime.rs +++ b/crates/weaveffi-cli/src/targets/swift/runtime.rs @@ -1,18 +1,24 @@ -//! The fixed Swift sources the generator splices names into (the private -//! runtime with its codec, error helpers, and async and callback support; -//! the `Package.swift` manifest; the C module map; and the packaged README), -//! plus the load-time checks rendered from the model. Each fixed source -//! lives as a real file under `runtime/` with `{{PLACEHOLDER}}` markers. +//! The fixed Swift sources the generator splices names into (the runtime +//! with the library's load check, error helpers, codec, generic sequence, +//! and async and callback support; the `Package.swift` manifest; the C +//! module map; and the packaged README), plus the contract rows rendered +//! from the model. Each fixed source lives as a real file under `runtime/` +//! with `{{PLACEHOLDER}}` markers. +use crate::codegen::contract::{self, hex}; use crate::codegen::CodeWriter; -use weaveffi_model::contract::entries; -use weaveffi_model::model::{contract_symbol, Model, ABI_VERSION}; +use weaveffi_model::model::{Model, ABI_VERSION}; + +use crate::targets::swift::types::{library_type_name, runtime_error_name, swift_str}; const RUNTIME: &str = include_str!("runtime/Runtime.swift"); const PACKAGE: &str = include_str!("runtime/Package.swift"); const MODULE_MAP: &str = include_str!("runtime/module.modulemap"); const README: &str = include_str!("runtime/README.md"); +/// The runtime's file name, next to the wrapper in `Sources/{Module}/`. +pub(crate) const RUNTIME_FILE: &str = "WeaveFFIRuntime.swift"; + /// Replace every `{{KEY}}` in `template` with its value. /// /// # Panics @@ -40,52 +46,72 @@ pub(crate) struct Names<'a> { pub(crate) header: &'a str, } -/// The private runtime the wrapper file starts with: error helpers, the -/// value-buffer codec, and async and callback support. -pub(crate) fn render_runtime(model: &Model, names: &Names, runtime_error: &str) -> String { +/// The runtime source (without the generated-file prelude): the library's +/// load check, error helpers, the value-buffer codec, the generic sequence, +/// and async and callback support. +pub(crate) fn render_runtime(model: &Model, names: &Names) -> String { fill( RUNTIME, &[ - ("RUNTIME_ERROR", runtime_error), + ("RUNTIME_ERROR", &runtime_error_name(names.module)), + ("LIBRARY_TYPE", &library_type_name(names.module)), ("PREFIX", model.prefix()), ("MODULE", names.module), + ("C_MODULE", names.c_module), ("LIBRARY", names.library), ("ABI_VERSION", &ABI_VERSION.to_string()), ], ) } -/// Render the load-time checks the runtime's `wvLoad()` runs once: the ABI -/// revision, then every top-level module's contract table against the -/// entries these bindings were generated with. -pub(crate) fn render_load_checks(w: &mut CodeWriter, model: &Model) { - w.line("// MARK: - Load-time checks"); +/// Render `wvCheckContracts()`, which the runtime's load check calls after +/// the ABI revision: every top-level module's contract table against the +/// rows these bindings were generated with. +pub(crate) fn render_contract_checks(w: &mut CodeWriter, model: &Model, library_type: &str) { + let tables = contract::tables(model); + w.line("// MARK: - Contract"); w.blank(); - w.line("/// The load-time checks, run once before the first native call: the library"); + w.line("/// The first declaration these bindings were generated with that the library"); + w.line(format!( + "/// lacks or declares differently, checked once by `{library_type}.check()`." + )); w.line(format!( - "/// must implement C ABI revision {ABI_VERSION} and carry every declaration these" + "func wvCheckContracts() -> {library_type}.LoadError? {{" )); - w.line("/// bindings were generated with, unchanged."); - w.line("let wvContract: Void = {"); - w.scope(|w| { - w.line("wvCheckAbiVersion()"); - for root in model.roots() { - w.line(format!( - "wvCheckContract({}, [", - contract_symbol(model.prefix(), &root.name) - )); + w.indent(); + for (i, table) in tables.iter().enumerate() { + let last = i + 1 == tables.len(); + let open = format!("wvCheckContract({}, [", table.symbol); + if last { + w.line(format!("return {open}")); + } else { + w.line(format!("if let failure = {open}")); + } + w.scope(|w| { + for row in &table.rows { + w.line(format!( + "({}, {}, \"{}\"),", + hex(row.id), + hex(row.hash), + swift_str(&row.path) + )); + } + }); + if last { + w.line("])"); + } else { + w.line("]) {"); w.scope(|w| { - for e in entries(model, root) { - w.line(format!( - "(0x{:016x}, 0x{:016x}, \"{}\"),", - e.id, e.hash, e.path - )); - } + w.line("return failure"); }); - w.line("])"); + w.line("}"); } - }); - w.line("}()"); + } + if tables.is_empty() { + w.line("return nil"); + } + w.dedent(); + w.line("}"); w.blank(); } diff --git a/crates/weaveffi-cli/src/targets/swift/runtime/Runtime.swift b/crates/weaveffi-cli/src/targets/swift/runtime/Runtime.swift index 560b7527..fa06c1c1 100644 --- a/crates/weaveffi-cli/src/targets/swift/runtime/Runtime.swift +++ b/crates/weaveffi-cli/src/targets/swift/runtime/Runtime.swift @@ -1,61 +1,79 @@ -// MARK: - Runtime support +import {{C_MODULE}} +import Foundation + +// MARK: - The library + +/// The native library `{{LIBRARY}}` these bindings call. +/// +/// Before the first call, the bindings check once that the library +/// implements C ABI revision {{ABI_VERSION}} and carries every declaration they were +/// generated with, unchanged. Call ``check()`` at startup to handle a +/// mismatch as an error; a call made while the library doesn't match stops +/// the process instead, since a stale binding would otherwise corrupt +/// memory. +public enum {{LIBRARY_TYPE}} { + /// Why the native library can't be used with these bindings. + public enum LoadError: Error, LocalizedError, Hashable, Sendable { + /// The library implements another C ABI revision than `expected`. + case abiMismatch(found: UInt32, expected: UInt32) + /// The library lacks a declaration, named by its dotted IDL path + /// (`kv.Store.put`). + case missing(declaration: String) + /// A declaration's signature changed since these bindings were + /// generated. + case changed(declaration: String) + + public var errorDescription: String? { + switch self { + case let .abiMismatch(found, expected): + return "{{MODULE}}: the native library '{{LIBRARY}}' implements C ABI revision \(found), but these bindings need revision \(expected)" + case let .missing(declaration): + return "{{MODULE}}: \(declaration) is missing from the library '{{LIBRARY}}'; regenerate the bindings or rebuild the library" + case let .changed(declaration): + return "{{MODULE}}: \(declaration) changed since these bindings were generated; regenerate the bindings or rebuild the library '{{LIBRARY}}'" + } + } + } -/// A failure the native library reports outside any declared error domain: -/// an unknown error code or a runtime failure. Throwing wrappers raise it; -/// non-throwing wrappers stop the process with `fatalError` instead, since -/// Swift has no unchecked errors. -public struct {{RUNTIME_ERROR}}: Error, LocalizedError, Hashable, Sendable { - /// The code of a generic runtime failure. - public static let genericCode: Int32 = -1 - /// The code of a panic caught at the library boundary. - public static let panicCode: Int32 = -2 - /// The code of an argument or result that failed to marshal. - public static let marshalCode: Int32 = -3 - /// The code of a callback implementation that threw. - public static let foreignCode: Int32 = -4 + /// The C ABI revision these bindings were generated against. + public static let abiVersion: UInt32 = {{ABI_VERSION}} - /// The numeric code the native library reported. - public let errorCode: Int32 - /// The message the native library reported. - public let message: String - - /// Creates an error carrying a native code and message. - public init(errorCode: Int32, message: String) { - self.errorCode = errorCode - self.message = message + /// Checks that the loaded library matches these bindings: its C ABI + /// revision, then every declaration's signature. The checks run once per + /// process; later calls return or throw the same result. + /// + /// - Throws: ``LoadError`` naming the first mismatch. + public static func check() throws { + if let failure = wvLoadFailure { + throw failure + } } - - public var errorDescription: String? { message } } -/// The error slot every native call reports through. -typealias WvError = {{PREFIX}}_error - -/// The C ABI revision these bindings were generated against. -let wvAbiVersion: UInt32 = {{ABI_VERSION}} +/// The outcome of the load-time checks, computed once. +let wvLoadFailure: {{LIBRARY_TYPE}}.LoadError? = { + let abi = {{PREFIX}}_abi_version() + guard abi == {{LIBRARY_TYPE}}.abiVersion else { + return .abiMismatch(found: abi, expected: {{LIBRARY_TYPE}}.abiVersion) + } + return wvCheckContracts() +}() -/// Runs the load-time checks if they haven't run yet. +/// Stops the process unless the load-time checks passed. Every entry point +/// that doesn't already hold an object runs it first. @inline(__always) func wvLoad() { - _ = wvContract -} - -/// Stops the process unless the library implements the ABI revision these -/// bindings were generated against. -func wvCheckAbiVersion() { - let abi = {{PREFIX}}_abi_version() - guard abi == wvAbiVersion else { - fatalError("{{MODULE}}: the native library '{{LIBRARY}}' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + if let failure = wvLoadFailure { + fatalError(failure.errorDescription ?? "{{MODULE}}: the native library doesn't match these bindings") } } -/// Stops the process unless every declaration these bindings were generated -/// with is in a top-level module's contract table with an equal signature -/// hash. Declarations the library adds are fine. +/// The first expected row that a top-level module's contract table lacks or +/// declares with another signature hash. Rows the library adds are fine. func wvCheckContract( _ table: (UnsafeMutablePointer?) -> UnsafePointer<{{PREFIX}}_contract_entry>?, - _ expected: [(id: UInt64, hash: UInt64, path: StaticString)] -) { + _ expected: [(id: UInt64, hash: UInt64, path: String)] +) -> {{LIBRARY_TYPE}}.LoadError? { var len = 0 let entries = UnsafeBufferPointer(start: table(&len), count: len) var hashes: [UInt64: UInt64] = [:] @@ -64,17 +82,69 @@ func wvCheckContract( } for (id, hash, path) in expected { guard let actual = hashes[id] else { - fatalError("{{MODULE}}: \(path) is missing from the library '{{LIBRARY}}'; regenerate the bindings or rebuild the library") + return .missing(declaration: path) } guard actual == hash else { - fatalError("{{MODULE}}: \(path) changed since these bindings were generated; regenerate the bindings or rebuild the library '{{LIBRARY}}'") + return .changed(declaration: path) } } + return nil +} + +// MARK: - Errors + +/// A failure the library reports outside any declared error domain: the +/// failure of a call declared `throws: any` (code ``untypedCode``), or a +/// runtime failure (a panic, a value that failed to marshal, a callback +/// implementation that failed). Throwing calls raise it; calls that can't +/// throw stop the process instead. +public struct {{RUNTIME_ERROR}}: Error, LocalizedError, Hashable, Sendable { + /// The code of a failure of a call declared `throws: any`. + public static let untypedCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that failed. + public static let callbackCode: Int32 = -4 + + /// The numeric code the library reported. + public let errorCode: Int32 + /// The message the library reported. + public let message: String + + /// Creates an error carrying a code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = {{PREFIX}}_error + +/// A declared error domain: an `Error` enum with one case per code, plus a +/// case for codes a newer library added. +protocol WvDomain: Error { + /// The error for a filled slot with a positive code, decoding the + /// code's fields from the payload. + init(wvError err: WvError) + /// Reports this error through a callback method's error slot: its code, + /// message, and fields. + func wvReport(_ outErr: UnsafeMutablePointer?) } -/// The message carried by an error slot. +/// The message carried by an error slot (UTF-8, not NUL-terminated). func wvErrorMessage(_ err: WvError) -> String { - err.message.map { String(cString: $0) } ?? "" + guard let ptr = err.message_ptr, err.message_len > 0 else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: err.message_len), as: UTF8.self) +} + +/// The payload reader of an error slot, for a code with fields. +func wvPayload(_ err: WvError) -> WvReader { + WvReader(err.payload_ptr, err.payload_len) } /// The Swift error for a code outside any declared domain: cancellation @@ -86,10 +156,16 @@ func wvRuntimeError(_ err: WvError) -> Error { return {{RUNTIME_ERROR}}(errorCode: err.code, message: wvErrorMessage(err)) } +/// The Swift error for a failed call declared `throws: Domain`: a positive +/// code is the domain's, anything else a runtime error. +func wvDomainError(_ err: WvError, _ domain: D.Type) -> Error { + err.code > 0 ? D(wvError: err) : wvRuntimeError(err) +} + /// Stops the process for a failed call that can't throw, naming the code and /// message the library reported. -func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { - fatalError("{{MODULE}}.\(function) failed with code \(code): \(message)") +func wvFatal(_ err: WvError, _ function: StaticString = #function) -> Never { + fatalError("{{MODULE}}.\(function) failed with code \(err.code): \(wvErrorMessage(err))") } /// Throws the runtime error for a filled error slot, clearing it. @@ -101,42 +177,49 @@ func wvCheck(_ err: inout WvError) throws { throw error } +/// Throws the domain or runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError, _ domain: D.Type) throws { + guard err.code != 0 else { return } + let error = wvDomainError(err, domain) + {{PREFIX}}_error_clear(&err) + throw error +} + /// Stops the process for a filled error slot of a call that can't throw. @inline(__always) func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { guard err.code != 0 else { return } - let code = err.code - let message = wvErrorMessage(err) - {{PREFIX}}_error_clear(&err) - wvFatal(code, message, function) + wvFatal(err, function) } -/// Maps and releases the heap-boxed error an async completion received. -func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { - let error = map(err.pointee) - {{PREFIX}}_error_free(err) - return error +/// Maps and releases the heap-boxed error of an async completion. +func wvTakeError(_ err: UnsafeMutablePointer) -> Error { + defer { {{PREFIX}}_error_free(err) } + return wvRuntimeError(err.pointee) } -/// The error mapping of a cancellable call that declares no error domain: -/// cancellation throws `CancellationError`, anything else stops the process. -func wvCancelledOrTrap(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - wvFatal(err.code, wvErrorMessage(err), "async call") +/// Maps and releases the heap-boxed error of an async completion of a call +/// declared `throws: Domain`. +func wvTakeError(_ err: UnsafeMutablePointer, _ domain: D.Type) -> Error { + defer { {{PREFIX}}_error_free(err) } + return wvDomainError(err.pointee, domain) } -/// Stops the process for the boxed error of an async call that can't throw. -func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { - wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +/// Maps and releases the boxed error of a cancellable async call that +/// declares no errors: cancellation throws `CancellationError`, anything +/// else stops the process. +func wvTakeCancellation(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Error { + guard err.pointee.code == -5 else { wvFatal(err.pointee, function) } + {{PREFIX}}_error_free(err) + return CancellationError() } /// Unwraps a pointer the library promised is non-null. @inline(__always) func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { guard let ptr = ptr else { - wvFatal({{RUNTIME_ERROR}}.marshalCode, "the native library returned a null pointer", function) + fatalError("{{MODULE}}.\(function): the native library returned a null pointer") } return ptr } @@ -149,7 +232,7 @@ func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E. return value } -// MARK: Strings, bytes, and buffers +// MARK: - Strings, bytes, typed arrays, and buffers /// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. @inline(__always) @@ -164,6 +247,12 @@ func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } } +/// Lends the storage of `a` (aligned for its element) for one call. +@inline(__always) +func wvWithSlice(_ a: [T], _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try a.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + /// Encodes `value` as a value buffer and lends it for one call. @inline(__always) func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { @@ -172,43 +261,63 @@ func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, return try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } } -/// Copies a returned string, then releases the library's allocation. +/// Releases a run the library handed over. +@inline(__always) +func wvFree(_ ptr: UnsafeRawPointer, _ byteCount: Int) { + {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr.assumingMemoryBound(to: UInt8.self)), byteCount) +} + +/// Copies a returned string, then releases the library's run. func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { guard let ptr = ptr else { return "" } - defer { {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + defer { wvFree(ptr, len) } return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) } -/// Copies returned bytes, then releases the library's allocation. +/// Copies returned bytes, then releases the library's run. func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { guard let ptr = ptr else { return Data() } - defer { {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + defer { wvFree(ptr, len) } return Data(bytes: ptr, count: len) } +/// Copies a returned typed array of `count` elements, then releases the +/// library's run. +func wvTakeSlice(_ ptr: UnsafePointer?, _ count: Int) -> [T] { + guard let ptr = ptr else { return [] } + defer { wvFree(ptr, count * MemoryLayout.stride) } + return Array(UnsafeBufferPointer(start: ptr, count: count)) +} + /// Decodes a returned value buffer in place, then releases it. func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { defer { - if let ptr = ptr { {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + if let ptr = ptr { wvFree(ptr, len) } } return wvBorrowBuffer(ptr, len, as: type) } /// Copies a borrowed string. func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } + guard let ptr = ptr, len > 0 else { return "" } return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) } /// Copies borrowed bytes. func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } + guard let ptr = ptr, len > 0 else { return Data() } return Data(bytes: ptr, count: len) } +/// Copies a borrowed typed array of `count` elements. +func wvBorrowSlice(_ ptr: UnsafePointer?, _ count: Int) -> [T] { + guard let ptr = ptr, count > 0 else { return [] } + return Array(UnsafeBufferPointer(start: ptr, count: count)) +} + /// Decodes a borrowed value buffer in place. func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - var r = WvReader(UnsafeRawPointer(ptr), len) + var r = WvReader(ptr, len) let value: T = r.read() r.finish() return value @@ -378,7 +487,7 @@ struct WvReader { } } -// MARK: Value-buffer codecs +// MARK: - Value-buffer codecs /// A type that crosses the C ABI inside a value buffer. Every primitive, /// optional, list, and map conforms here; each record, enum, and interface @@ -398,6 +507,23 @@ extension WvCEnum { func wvWrite(_ w: inout WvWriter) { w.writeI32(rawValue) } } +/// An interface wrapper: one strong reference to a native object, which +/// crosses inside a value buffer as an object token. +protocol WvObject: AnyObject, WvCodable { + /// The reference this wrapper owns. + var ptr: OpaquePointer { get } + /// Adopts one strong reference. + init(ptr: OpaquePointer) + /// Returns a new strong reference to the same object, for a position + /// that takes ownership of it. + func clonePtr() -> OpaquePointer +} + +extension WvObject { + static func wvRead(_ r: inout WvReader) -> Self { Self(ptr: r.readObject()) } + func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) } +} + extension Bool: WvCodable { static func wvRead(_ r: inout WvReader) -> Bool { r.readBool() } func wvWrite(_ w: inout WvWriter) { w.writeBool(self) } @@ -515,7 +641,80 @@ extension Dictionary: WvCodable where Key: WvCodable, Value: WvCodable { } } -// MARK: Async and callbacks +// MARK: - Iterators + +/// A lazy sequence over the elements a native iterator streams, returned by +/// every function declared to return `iter`. +/// +/// Each `next()` pulls exactly one element from the library. The native +/// iterator is released as soon as the stream ends (or fails), and from +/// `deinit` when iteration stops early. The sequence is single-pass: once +/// exhausted, `next()` keeps returning `nil`. +/// +/// `next()` can't throw, so when the library reports an error mid-stream, +/// iteration ends and the error is kept in ``error``. ``collect()`` pulls +/// the rest and throws that error instead. A function that declares no +/// errors stops the process on a mid-stream failure. +public final class NativeSequence: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + private let pull: (OpaquePointer, UnsafeMutablePointer) -> Element? + private let release: (OpaquePointer) -> Void + private let failure: (WvError) -> Error + + /// The error that ended iteration early, if any. + public private(set) var error: Error? + + init( + _ handle: OpaquePointer, + release: @escaping (OpaquePointer) -> Void, + failure: @escaping (WvError) -> Error, + pull: @escaping (OpaquePointer, UnsafeMutablePointer) -> Element? + ) { + self.handle = handle + self.release = release + self.failure = failure + self.pull = pull + } + + deinit { + if let handle = handle { + release(handle) + } + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream has ended. + public func next() -> Element? { + guard let handle = handle else { return nil } + var err = WvError() + if let item = pull(handle, &err) { + return item + } + self.handle = nil + release(handle) + if err.code != 0 { + error = failure(err) + {{PREFIX}}_error_clear(&err) + } + return nil + } + + /// Pulls every remaining element. + /// + /// - Throws: The error that ended iteration early, if any. + public func collect() throws -> [Element] { + var items: [Element] = [] + while let item = next() { + items.append(item) + } + if let error = error { + throw error + } + return items + } +} + +// MARK: - Async and callbacks /// A native cancel token owned by one cancellable call. The library takes its /// own reference at launch, so this one is released whenever the wrapper is @@ -577,64 +776,114 @@ func wvRelease(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) { Unmanaged>.fromOpaque(ctx!).release() } +/// The implementation behind `ctx`. +@inline(__always) +func wvImpl(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) -> T { + Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value +} + +/// Reports an error a callback method threw as an untyped failure (`-1`) +/// with its description. +func wvReport(_ error: Error, _ outErr: UnsafeMutablePointer?) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + wvSetError(outErr, {{RUNTIME_ERROR}}.untypedCode, message) +} + /// Runs one callback method on the implementation behind `ctx`. A thrown -/// error never unwinds through the library: `domain` reports it when it's -/// the method's declared error, anything else is reported as a callback -/// failure (`-4`), and the method returns `fallback`. +/// error never unwinds through the library: it's reported through `outErr` +/// as an untyped failure, and the method returns `fallback`. func wvInvoke( _ ctx: UnsafeMutableRawPointer?, _ outErr: UnsafeMutablePointer?, as type: T.Type, fallback: R, - domain: (Error, UnsafeMutablePointer?) -> Bool = { _, _ in false }, _ body: (T) throws -> R ) -> R { - let impl = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value do { - return try body(impl) + return try body(wvImpl(ctx, as: type)) } catch { - if !domain(error, outErr) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { {{PREFIX}}_error_set(outErr, {{RUNTIME_ERROR}}.foreignCode, $0) } - } + wvReport(error, outErr) return fallback } } -/// Reports a declared domain error from a callback method: its code, its -/// message, and its fields as the payload. +/// Runs one callback method declared `throws: Domain`: a thrown `D` is +/// reported with its code, message, and fields; any other error as an +/// untyped failure. +func wvInvoke( + _ ctx: UnsafeMutableRawPointer?, + _ outErr: UnsafeMutablePointer?, + as type: T.Type, + throwing domain: D.Type, + fallback: R, + _ body: (T) throws -> R +) -> R { + do { + return try body(wvImpl(ctx, as: type)) + } catch let error as D { + error.wvReport(outErr) + return fallback + } catch { + wvReport(error, outErr) + return fallback + } +} + +/// Fills a callback method's error slot: a code, a message, and the fields +/// of a domain code as the payload. func wvSetError(_ outErr: UnsafeMutablePointer?, _ code: Int32, _ message: String, _ payload: WvWriter? = nil) { - message.withCString { {{PREFIX}}_error_set(outErr, code, $0) } + wvWithUTF8(message) { {{PREFIX}}_error_set(outErr, code, $0, $1) } if let payload = payload { payload.bytes.withUnsafeBufferPointer { {{PREFIX}}_error_set_payload(outErr, $0.baseAddress, $0.count) } } } +/// Returns an optional scalar from a callback method: the value through +/// `outValue` and whether it's present. +func wvReturnOptional(_ value: T?, _ outValue: UnsafeMutablePointer?) -> Bool { + guard let value = value else { return false } + outValue?.pointee = value + return true +} + /// Hands `bytes` to the library as a callback method's return: a run /// allocated with `{{PREFIX}}_alloc` that the library adopts. -func wvHandOver(_ bytes: UnsafeRawBufferPointer, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - let run = {{PREFIX}}_alloc(bytes.count) - if let run = run, let base = bytes.baseAddress { - run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) +func wvHandOver(_ bytes: UnsafeRawBufferPointer) -> (UnsafeMutableRawPointer?, Int) { + guard let base = bytes.baseAddress, bytes.count > 0, let run = {{PREFIX}}_alloc(bytes.count) else { + return (nil, 0) } - outPtr?.pointee = run - outLen?.pointee = run == nil ? 0 : bytes.count + run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) + return (UnsafeMutableRawPointer(run), bytes.count) } /// Returns a string from a callback method through its out slots. func wvReturnString(_ value: String, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { var value = value - value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0), outPtr, outLen) } + let (run, len) = value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0)) } + outPtr?.pointee = run?.assumingMemoryBound(to: UInt8.self) + outLen?.pointee = len } /// Returns bytes from a callback method through its out slots. func wvReturnBytes(_ value: Data, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - value.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } + let (run, len) = value.withUnsafeBytes { wvHandOver($0) } + outPtr?.pointee = run?.assumingMemoryBound(to: UInt8.self) + outLen?.pointee = len +} + +/// Returns a typed array from a callback method through its out slots: the +/// element count, in a run the library adopts. +func wvReturnSlice(_ value: [T], _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { + let (run, len) = value.withUnsafeBytes { wvHandOver($0) } + outPtr?.pointee = run?.bindMemory(to: T.self, capacity: value.count) + outLen?.pointee = len / MemoryLayout.stride } /// Returns a value buffer from a callback method through its out slots. func wvReturnBuffer(_ value: T, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { var w = WvWriter() w.write(value) - w.bytes.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } + let (run, len) = w.bytes.withUnsafeBytes { wvHandOver($0) } + outPtr?.pointee = run?.assumingMemoryBound(to: UInt8.self) + outLen?.pointee = len } diff --git a/crates/weaveffi-cli/src/targets/swift/tests.rs b/crates/weaveffi-cli/src/targets/swift/tests.rs index 8c202195..efec6d89 100644 --- a/crates/weaveffi-cli/src/targets/swift/tests.rs +++ b/crates/weaveffi-cli/src/targets/swift/tests.rs @@ -1,9 +1,8 @@ -//! Unit tests: render a small API that exercises every C ABI revision 4 -//! shape the Swift wrapper distinguishes and assert the key pieces of each -//! contract. +//! Unit tests of the Swift target's own policies: package and file naming, +//! configuration, identifier escaping, and doc spelling. The `kitchen_sink` +//! snapshot pins the rendering of every ABI shape. use camino::Utf8Path; -use weaveffi_model::contract::entries; use weaveffi_model::ir::Api; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; @@ -11,67 +10,54 @@ use weaveffi_model::validate::validate; use super::{render_swift_wrapper, Layout, SwiftConfig}; -/// One module with an error domain carrying a field; an interface with a -/// `new` constructor, a method, and a throwing static; nullable objects; an -/// iterator of objects; a cancellable async function; a record carrying -/// objects; and a callback interface whose methods take every argument -/// family and return a direct value, an enum, a string, a record, an object, -/// and an optional object, one of them `throws`, passed both required and -/// optional. +/// Names that collide with what the generator declares: a parameter named +/// like a wrapper body's locals, an error code named like the catch-all +/// case and the error enum's members, a field named like the leading +/// message, domains whose names already end in `Error`/`Errors`, a record +/// shadowed by a module namespace, and doc text naming API identifiers. const FIXTURE: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: shop errors: - name: ShopError - codes: - - { name: OutOfStock, code: 1, message: "out of stock", fields: [{ name: sku, type: string }] } - - { name: Closed, code: 2, message: "closed" } - enums: - - name: Mood - variants: [{ name: Happy, value: 0 }, { name: Grumpy, value: 1 }] + - name: ShopErrors + codes: + - { name: Unknown, code: 1, message: "unknown" } + - { name: Message, code: 2, message: "message" } + - { name: Failed, code: 3, message: "failed", fields: [{ name: message, type: string }] } + - name: Failure + codes: + - { name: Broken, code: 1, message: "broken" } structs: - - name: Order + - name: Stats fields: - - { name: cart, type: Cart } - - { name: history, type: "[Cart]" } - - { name: note, type: "string?" } + - { name: count, type: u64 } interfaces: - name: Cart - constructors: - - { name: new, params: [{ name: owner, type: string }] } methods: - - { name: add, params: [{ name: sku, type: string }], return: bool } - statics: - - { name: restore, params: [{ name: id, type: i64 }], return: Cart, throws: true } - callback_interfaces: - - name: Watcher - methods: - - name: on_event - params: - - { name: name, type: string } - - { name: count, type: i32 } - - { name: order, type: Order } - - { name: cart, type: Cart } - return: bool - - { name: mood, return: Mood } - - { name: label, return: string, throws: true } - - { name: latest, return: "Order?" } - - { name: favorite, return: Cart } - - { name: maybe, return: "Cart?" } - - { name: on_done } + - { name: ptr, params: [], return: i64 } functions: - - { name: find_cart, params: [{ name: current, type: "Cart?" }], return: "Cart?" } - - { name: all_carts, return: "iter" } - - { name: watch, params: [{ name: watcher, type: Watcher }] } - - { name: maybe_watch, params: [{ name: watcher, type: "Watcher?" }] } - - { name: echo, params: [{ name: text, type: string }], return: string } - - { name: tally, params: [{ name: counts, type: "{string:[i32?]}" }], return: "[Order]" } - - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } + - name: locals + doc: "Calls `old_count`; fails with `ShopErrors`." + params: + - { name: err, type: i32 } + - { name: rv, type: string } + - { name: in, type: "i32?" } + return: i32 + throws: ShopErrors + - name: old_count + deprecated: "Use `locals` instead" + params: [] + return: u32 + throws: Failure + modules: + - name: stats + functions: + - { name: summarize, params: [], return: Stats } "#; fn model() -> Model { - let api: Api = serde_yaml::from_str(FIXTURE).unwrap(); + let api: Api = serde_saphyr::from_str(FIXTURE).unwrap(); validate(&api, &Identity::named("shop_kit"), None).unwrap() } @@ -102,19 +88,22 @@ fn names_come_from_the_identity() { "out/Sources/CShopKit/module.modulemap", "out/Sources/CShopKit/shop_kit.h", "out/Sources/ShopKit/ShopKit.swift", + "out/Sources/ShopKit/WeaveFFIRuntime.swift", ] ); assert_has(&files[0].1, "name: \"ShopKit\""); assert_has(&files[0].1, ".systemLibrary(name: \"CShopKit\")"); - assert_has( - &files[0].1, - ".binaryTarget(name: \"CShopKit\", path: xcframework)", - ); assert_has(&files[1].1, "header \"shop_kit.h\""); assert_has(&files[1].1, "link \"shop_kit\""); + let runtime = &files[4].1; + assert_has(runtime, "import CShopKit\n"); + assert_has(runtime, "public enum ShopKitLibrary {"); + assert_has(runtime, "public struct ShopKitRuntimeError: Error"); + assert_has(runtime, "let abi = shop_kit_abi_version()"); + assert_has(runtime, "public static let abiVersion: UInt32 = 5\n"); + assert!(!runtime.contains("{{"), "{runtime}"); let all = files.iter().map(|(_, c)| c.as_str()).collect::(); assert!(!all.contains("weaveffi_"), "{all}"); - assert!(!all.contains("WeaveFFIError"), "{all}"); } #[test] @@ -128,291 +117,73 @@ fn module_name_override_wins() { assert_eq!(layout.module, "Shop"); assert_eq!(layout.c_module, "CShop"); assert_eq!(layout.library, "shop_kit"); + let out = render_swift_wrapper(&layout, &model, "Shop.swift"); + assert_has(&out, "func wvCheckContracts() -> ShopLibrary.LoadError? {"); + assert_has(&out, "``ShopRuntimeError``"); } #[test] -fn load_checks_embed_every_contract_entry() { - let model = model(); - let out = render(); - assert_has(&out, "let wvAbiVersion: UInt32 = 4"); - assert_has(&out, "let abi = shop_kit_abi_version()"); - assert_has( - &out, - " wvCheckAbiVersion()\n wvCheckContract(shop_kit_shop_contract, [\n", - ); - let root = model.roots().next().unwrap(); - let table = entries(&model, root); - assert!(table.len() > 10); - for e in &table { - assert_has( - &out, - &format!("(0x{:016x}, 0x{:016x}, \"{}\"),", e.id, e.hash, e.path), - ); - } - assert_has(&out, "\\(path) is missing from the library 'shop_kit'"); - assert_has(&out, "\\(path) changed since these bindings were generated"); - assert!(!out.contains("checksum"), "{out}"); - // Static entry points run the checks before the first native call. - assert_has( - &out, - "public static func echo(text: String) -> String {\n wvLoad()", - ); -} - -#[test] -fn strings_cross_as_pointer_and_length() { - let out = render(); - assert_has(&out, "wvWithUTF8(text) { text_ptr, text_len in"); - assert_has( - &out, - "shop_kit_shop_echo(text_ptr, text_len, &outLen, &err)", - ); - assert_has(&out, "return wvTakeString(rv, outLen)"); -} - -#[test] -fn composites_share_one_generic_codec() { - let out = render(); - // Records conform once; every composite reuses the runtime's generic - // Optional, Array, and Dictionary conformances instead of inlined loops. - assert_has(&out, "extension Order: WvCodable {"); - assert_has( - &out, - "Order(cart: r.read(), history: r.read(), note: r.read())", - ); - assert_has(&out, " w.write(self.history)\n"); - assert_has( - &out, - "extension Array: WvCodable where Element: WvCodable {", - ); - assert_has( - &out, - "extension Dictionary: WvCodable where Key: WvCodable, Value: WvCodable {", - ); - assert_has(&out, "extension Mood: WvCEnum {}"); - assert_has(&out, "wvWithEncoded(counts) { counts_ptr, counts_len in"); - assert_has(&out, "return wvTakeBuffer(rv, outLen, as: [Order].self)"); - assert!(!out.contains("for v"), "{out}"); -} - -#[test] -fn interface_class_owns_one_reference() { - let out = render(); - assert_has(&out, "public final class Cart: @unchecked Sendable {"); - assert_has(&out, "let ptr: OpaquePointer"); - assert_has(&out, "shop_kit_shop_Cart_destroy(ptr)"); - assert_has(&out, "wvNonNull(shop_kit_shop_Cart_clone(ptr))"); - assert_eq!(out.matches("shop_kit_shop_Cart_destroy(").count(), 1); - assert_has(&out, "public init(owner: String) {"); - assert_has(&out, "self.ptr = wvNonNull(rv)"); - assert_has(&out, "public func add(sku: String) -> Bool {"); - assert_has(&out, "shop_kit_shop_Cart_add(ptr, sku_ptr, sku_len, &err)"); - assert_has( - &out, - "static func wvRead(_ r: inout WvReader) -> Cart { Cart(ptr: r.readObject()) }", - ); - assert_has( - &out, - "func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) }", - ); -} - -#[test] -fn throwing_calls_map_the_domain_and_others_trap() { - let out = render(); - assert_has( - &out, - "public enum ShopError: Error, LocalizedError, Sendable {", - ); - assert_has(&out, "case outOfStock(message: String, sku: String)"); - assert_has( - &out, - "/// - Throws: ``ShopError`` for a declared failure, ``ShopKitRuntimeError`` otherwise.", - ); - assert_has( - &out, - "public static func restore(id: Int64) throws -> Cart {", - ); - assert_has(&out, "try wvCheckShop(&err)"); - assert_has( - &out, - "let error = ShopError.outOfStock(message: message.isEmpty ? \"out of stock\" : message, sku: r.read())", - ); - // A call that can't throw stops the process, naming code and message. - assert_has(&out, "wvTrap(&err)"); - assert_has( - &out, - "fatalError(\"ShopKit.\\(function) failed with code \\(code): \\(message)\")", - ); -} - -#[test] -fn nullable_objects_map_to_optional_wrappers() { - let out = render(); - assert_has( - &out, - "public static func findCart(current: Cart?) -> Cart? {", - ); - assert_has(&out, "shop_kit_shop_find_cart(current?.ptr, &err)"); - assert_has(&out, "return rv.map { Cart(ptr: $0) }"); -} - -#[test] -fn object_iterator_adopts_each_element() { +fn parameters_named_like_body_locals_keep_their_labels() { let out = render(); assert_has( &out, - "public final class ShopAllCartsIterator: Sequence, IteratorProtocol {", + "public static func locals(err err_: Int32, rv rv_: String, in_: Int32?) throws -> Int32 {", ); - assert_has(&out, "var item: OpaquePointer? = nil"); + assert_has(&out, "wvWithUTF8(rv_) { rv__ptr, rv__len in"); assert_has( &out, - "shop_kit_shop_AllCartsIterator_next(handle, &item, &err)", + "shop_kit_shop_locals(err_, rv__ptr, rv__len, in_ != nil, in_ ?? 0, &err)", ); - assert_has(&out, "shop_kit_shop_AllCartsIterator_destroy(handle)"); - assert_has(&out, "return Cart(ptr: wvNonNull(item))"); } #[test] -fn callback_vtable_starts_with_the_header() { +fn domains_use_the_shared_type_name_and_avoid_their_members() { let out = render(); - assert_has(&out, "public protocol Watcher: AnyObject, Sendable {"); + // `ShopErrors` and `Failure` through `errors::type_name`. assert_has( &out, - "static let shared = WvVtable(shop_kit_shop_Watcher_vtable(\n size: UInt32(MemoryLayout.stride),\n flags: 0,\n free: { ctx in\n wvRelease(ctx, as: (any Watcher).self)\n },\n on_event: {", + "public enum ShopError: Error, LocalizedError, Hashable, Sendable {", ); - // No trailing comma after the last entry (Swift before 6.1 rejects it). - assert_has(&out, " }\n ))\n}"); -} - -#[test] -fn callback_arguments_are_copied_or_adopted() { - let out = render(); assert_has( &out, - "func onEvent(name: String, count: Int32, order: Order, cart: Cart) throws -> Bool", + "public enum FailureError: Error, LocalizedError, Hashable, Sendable {", ); + // A code named `Unknown` keeps its case; the catch-all moves aside. A + // code named like a member and a field named like the message escape. + assert_has(&out, " case unknown(message: String)\n"); + assert_has(&out, " case unknown_(code: Int32, message: String)\n"); + assert_has(&out, " case message_(message: String)\n"); + assert_has(&out, " case failed(message: String, message_: String)\n"); assert_has( &out, - "on_event: { ctx, name_ptr, name_len, count, order_ptr, order_len, cart, out_err in", - ); - assert_has( - &out, - "wvInvoke(ctx, out_err, as: (any Watcher).self, fallback: false) { wvImpl in", - ); - assert_has( - &out, - "try wvImpl.onEvent(name: wvBorrowString(name_ptr, name_len), count: count, order: wvBorrowBuffer(order_ptr, order_len, as: Order.self), cart: Cart(ptr: wvNonNull(cart)))", + "self = .failed(message: message.isEmpty ? \"failed\" : message, message_: r.read())", ); + assert_has(&out, "try wvCheck(&err, ShopError.self)"); } #[test] -fn callback_returns_cover_every_family() { +fn types_shadowed_by_a_namespace_are_qualified() { let out = render(); - // An enum by value. - assert_has(&out, "func mood() throws -> Mood"); - assert_has( - &out, - "fallback: shop_kit_shop_Mood(rawValue: 0)) { wvImpl in\n try shop_kit_shop_Mood(rawValue: numericCast(wvImpl.mood().rawValue))", - ); - // A string and a buffer through the out slots, as `{p}_alloc` runs. - assert_has(&out, "label: { ctx, out_ptr, out_len, out_err in"); - assert_has(&out, "try wvReturnString(wvImpl.label(), out_ptr, out_len)"); - assert_has( - &out, - "try wvReturnBuffer(wvImpl.latest(), out_ptr, out_len)", - ); - assert_has(&out, "let run = shop_kit_alloc(bytes.count)"); - assert!(!out.contains("_dealloc"), "{out}"); - // Objects as a fresh reference the library adopts; `I?` may be nil. - assert_has(&out, "func favorite() throws -> Cart"); - assert_has( - &out, - "fallback: nil) { wvImpl in\n try wvImpl.favorite().clonePtr()", - ); - assert_has(&out, "func maybe() throws -> Cart?"); - assert_has(&out, "try wvImpl.maybe()?.clonePtr()"); - assert_has( - &out, - "fallback: ()) { wvImpl in\n try wvImpl.onDone()", - ); + assert_has(&out, "public struct Stats: Hashable, Sendable {"); + assert_has(&out, "public enum Stats {"); + assert_has(&out, "public static func summarize() -> ShopKit.Stats {"); } #[test] -fn throwing_callback_methods_report_the_domain_error() { +fn interface_members_avoid_the_wrapper_members() { let out = render(); - assert_has(&out, "func label() throws -> String"); - assert_has( - &out, - "/// - Throws: ``ShopError`` to report a declared failure with its fields", - ); - assert_has( - &out, - "wvInvoke(ctx, out_err, as: (any Watcher).self, fallback: (), domain: wvReportShop) { wvImpl in", - ); - assert_eq!(out.matches("domain: wvReportShop").count(), 1, "{out}"); - assert_has( - &out, - "func wvReportShop(_ error: Error, _ outErr: UnsafeMutablePointer?) -> Bool {", - ); - assert_has( - &out, - " case let .outOfStock(message, v0):\n var payload = WvWriter()\n payload.write(v0)\n wvSetError(outErr, 1, message, payload)", - ); - assert_has( - &out, - " case let .closed(message):\n wvSetError(outErr, 2, message)", - ); - assert_has( - &out, - "shop_kit_error_set_payload(outErr, $0.baseAddress, $0.count)", - ); - // Any other error is a callback failure. - assert_has( - &out, - "shop_kit_error_set(outErr, ShopKitRuntimeError.foreignCode, $0)", - ); + assert_has(&out, "public func ptr_() -> Int64 {"); + assert_has(&out, "shop_kit_shop_Cart_ptr(self.ptr, &err)"); } #[test] -fn callback_parameters_retain_the_implementation() { +fn doc_text_uses_swift_spellings() { let out = render(); - assert_has(&out, "public static func watch(watcher: any Watcher) {"); - assert_has(&out, "let watcher_ctx = wvRetain(watcher)"); - assert_has( - &out, - "shop_kit_shop_watch(watcher_ctx, WvWatcherVtable.shared.pointer, &err)", - ); - // An optional callback passes a null vtable for nil. - assert_has( - &out, - "public static func maybeWatch(watcher: (any Watcher)?) {", - ); - assert_has(&out, "let watcher_ctx = watcher.map { wvRetain($0) }"); - assert_has( - &out, - "shop_kit_shop_maybe_watch(watcher_ctx, watcher == nil ? nil : WvWatcherVtable.shared.pointer, &err)", - ); -} - -#[test] -fn cancellable_async_cancels_the_native_token() { - let out = render(); - assert_has( - &out, - "public static func wait(ms: Int64) async throws -> Int64 {", - ); - assert_has(&out, "let token = WvCancelToken()"); - assert_has(&out, "return try await withTaskCancellationHandler {"); - assert_has( - &out, - "shop_kit_shop_wait(ms, token.raw, { context, err, result in", - ); + assert_has(&out, "/// Calls `oldCount`; fails with `ShopError`."); assert_has( &out, - "cont.resume(throwing: wvTakeError(err, wvCancelledOrTrap))", + "@available(*, deprecated, message: \"Use `locals` instead\")", ); - assert_has(&out, "} onCancel: {\n token.cancel()"); } #[test] diff --git a/crates/weaveffi-cli/src/targets/swift/types.rs b/crates/weaveffi-cli/src/targets/swift/types.rs index 22812933..a1fa9433 100644 --- a/crates/weaveffi-cli/src/targets/swift/types.rs +++ b/crates/weaveffi-cli/src/targets/swift/types.rs @@ -1,12 +1,14 @@ -//! Swift type spellings and identifier policy: how model types and -//! user-chosen names render in Swift source. +//! Swift type spellings and identifier policy: how model types, user-chosen +//! names, and doc text render in Swift source. use std::collections::HashSet; +use crate::codegen::docs::{ApiNames, Doc, IdentKind}; use crate::lang; use heck::{ToLowerCamelCase, ToUpperCamelCase}; -use weaveffi_model::model::{ErrorBinding, IteratorBinding, Model}; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::errors::type_name; +use weaveffi_model::model::Model; +use weaveffi_model::ty::{ParamTy, Prim, RetTy, Ty}; /// The Swift spelling of a user-chosen identifier in a lowerCamel position /// (parameters, fields, enum cases, wrapper names): camel-cased, then @@ -32,73 +34,78 @@ pub(crate) fn swift_member(name: &str) -> String { /// Escape a string for embedding inside a Swift double-quoted literal. pub(crate) fn swift_str(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '\\' => out.push_str("\\\\"), + '"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if c.is_control() => out.push_str(&format!("\\u{{{:x}}}", c as u32)), + c => out.push(c), + } + } + out } -/// The `@available(*, deprecated, ...)` attribute line for a deprecation -/// message. -pub(crate) fn deprecated_attr(msg: &str) -> String { - format!("@available(*, deprecated, message: \"{}\")", swift_str(msg)) +/// The Swift spelling of a scalar, string, or bytes type. +pub(crate) fn prim_swift_type(p: Prim) -> &'static str { + match p { + Prim::I8 => "Int8", + Prim::I16 => "Int16", + Prim::I32 => "Int32", + Prim::I64 => "Int64", + Prim::U8 => "UInt8", + Prim::U16 => "UInt16", + Prim::U32 => "UInt32", + Prim::U64 => "UInt64", + Prim::F32 => "Float", + Prim::F64 => "Double", + Prim::Bool => "Bool", + Prim::String => "String", + Prim::Bytes => "Data", + } } -/// Facts about the whole API that type spellings and codecs consult: -/// namespace collisions, C enum type names, and which value types can be -/// `Hashable`. +/// Facts about the whole API that spellings consult: the generated +/// package's names, namespace collisions, and every declared identifier +/// (for doc text). pub(crate) struct SwiftCtx<'a> { /// The model being rendered. pub(crate) model: &'a Model, - /// C ABI symbol prefix (e.g. `kvstore`). - pub(crate) c_prefix: &'a str, /// SwiftPM module name (e.g. `Kvstore`). pub(crate) swift_module: &'a str, - /// The error type for codes outside any declared domain + /// The error type for failures outside any declared domain /// (`KvstoreRuntimeError`). pub(crate) runtime_error: String, + /// The namespace type of the library itself (`KvstoreLibrary`), which + /// carries `check()`. + pub(crate) library_type: String, /// Every module name in the API, PascalCased: the namespace `enum` /// names a wrapper-type reference can be shadowed by. module_names: HashSet, - /// The records and rich enums that are `Hashable` (every field is). - hashable: HashSet, - /// The declaring module paths of the error domains a callback method - /// may throw, which need a `wvReport{Stem}` helper. - reported: HashSet, + /// Every declared identifier, for rewriting doc text. + names: ApiNames, } impl<'a> SwiftCtx<'a> { /// Collect the API-wide facts from the model. pub(crate) fn new(model: &'a Model, swift_module: &'a str) -> SwiftCtx<'a> { - let module_names = model - .modules - .iter() - .map(|m| m.name.to_upper_camel_case()) - .collect(); - let reported = model - .modules - .iter() - .filter(|m| { - m.callback_interfaces - .iter() - .any(|cb| cb.methods.iter().any(|f| f.throws)) - }) - .filter_map(|m| model.error_domain(m)) - .map(|e| e.owner_path.clone()) - .collect(); SwiftCtx { model, - c_prefix: model.prefix(), swift_module, - runtime_error: format!("{swift_module}RuntimeError"), - module_names, - hashable: hashable_types(model), - reported, + runtime_error: runtime_error_name(swift_module), + library_type: library_type_name(swift_module), + module_names: model + .modules + .iter() + .map(|m| m.name.to_upper_camel_case()) + .collect(), + names: ApiNames::new(model), } } - /// `true` when a callback method may throw the domain `eb`. - pub(crate) fn reports(&self, eb: &ErrorBinding) -> bool { - self.reported.contains(&eb.owner_path) - } - /// Qualify a top-level wrapper type name with the Swift module when its /// name collides with a namespace `enum`. Inside `enum Kv { enum Stats { /// ... } }` the bare name `Stats` resolves to the namespace, not the @@ -111,118 +118,92 @@ impl<'a> SwiftCtx<'a> { } } - /// The Swift surface type of a model type. A callback interface is an - /// existential (`any Listener`). - /// - /// # Panics - /// - /// Panics on an iterator, which only appears as a function return and - /// renders as its per-function sequence class instead. + /// The Swift type of a value type. pub(crate) fn swift_type(&self, t: &Ty) -> String { match t { + Ty::Prim(p) => prim_swift_type(*p).to_string(), Ty::Enum(name) | Ty::Record(name) | Ty::RichEnum(name) | Ty::Interface(name) => { self.ty_name(name) } - Ty::CallbackInterface(name) => format!("any {}", self.ty_name(name)), - Ty::Optional(inner) if matches!(**inner, Ty::CallbackInterface(_)) => { - format!("({})?", self.swift_type(inner)) - } Ty::Optional(inner) => format!("{}?", self.swift_type(inner)), Ty::List(inner) => format!("[{}]", self.swift_type(inner)), Ty::Map(k, v) => format!("[{}: {}]", self.swift_type(k), self.swift_type(v)), - other => scalar_swift_type(other).to_string(), } } - /// The imported C type of the C-style enum named `name`. - /// - /// # Panics - /// - /// Panics when no enum has that name, which validation rules out. - pub(crate) fn c_enum_type(&self, name: &str) -> &str { - &self.model.enumeration(name).c_tag + /// The Swift type of a parameter. A callback interface is an existential + /// (`any Listener`, or `(any Listener)?`). + pub(crate) fn param_type(&self, t: &ParamTy) -> String { + match t { + ParamTy::Value(t) => self.swift_type(t), + ParamTy::Callback { name, nullable } => { + let ty = format!("any {}", self.ty_name(name)); + if *nullable { + format!("({ty})?") + } else { + ty + } + } + } } - /// `true` when values of `t` can be `Hashable` (and so `Equatable`). - pub(crate) fn is_hashable(&self, t: &Ty) -> bool { - type_is_hashable(t, &self.hashable) + /// The Swift type of a return: an iterator is the runtime's generic + /// `NativeSequence`. + pub(crate) fn ret_type(&self, t: &RetTy) -> String { + match t { + RetTy::Value(t) => self.swift_type(t), + RetTy::Iterator(elem) => format!("NativeSequence<{}>", self.swift_type(elem)), + } } -} -/// The Swift spelling of a scalar, string, or bytes type. -/// -/// # Panics -/// -/// Panics on a user, container, or iterator type. -pub(crate) fn scalar_swift_type(t: &Ty) -> &'static str { - match t { - Ty::Prim(Prim::I8) => "Int8", - Ty::Prim(Prim::I16) => "Int16", - Ty::Prim(Prim::I32) => "Int32", - Ty::Prim(Prim::I64) => "Int64", - Ty::Prim(Prim::U8) => "UInt8", - Ty::Prim(Prim::U16) => "UInt16", - Ty::Prim(Prim::U32) => "UInt32", - Ty::Prim(Prim::U64) => "UInt64", - Ty::Prim(Prim::F32) => "Float", - Ty::Prim(Prim::F64) => "Double", - Ty::Prim(Prim::Bool) => "Bool", - Ty::Prim(Prim::String) => "String", - Ty::Prim(Prim::Bytes) => "Data", - other => unreachable!("`{other}` isn't a scalar type"), + /// The Swift spelling of an API identifier named in doc text, or `None` + /// to keep it as written. + fn spell(&self, ident: &str) -> Option { + if ident.contains('.') { + return None; + } + match self.names.kind(ident)? { + IdentKind::Function + | IdentKind::Member + | IdentKind::CallbackMethod + | IdentKind::Param + | IdentKind::Field + | IdentKind::Variant + | IdentKind::ErrorCode => Some(swift_ident(ident)), + IdentKind::ErrorDomain => Some(type_name(ident, "Error")), + IdentKind::Module => Some(ident.to_upper_camel_case()), + IdentKind::Type => None, + } } -} -fn type_is_hashable(t: &Ty, hashable: &HashSet) -> bool { - match t { - Ty::Interface(_) | Ty::CallbackInterface(_) | Ty::Iterator(_) => false, - Ty::Record(name) | Ty::RichEnum(name) => hashable.contains(name), - Ty::Optional(inner) | Ty::List(inner) => type_is_hashable(inner, hashable), - Ty::Map(k, v) => type_is_hashable(k, hashable) && type_is_hashable(v, hashable), - _ => true, + /// A doc text with every backticked API identifier in Swift spelling. + pub(crate) fn doc(&self, doc: &Option) -> Option { + Doc::new(doc, &None).text(|i| self.spell(i)) } -} -/// The records and rich enums whose every field is `Hashable`, as a greatest -/// fixed point so mutually referencing types resolve. -fn hashable_types(model: &Model) -> HashSet { - let mut types: Vec<(String, Vec<&Ty>)> = Vec::new(); - for m in &model.modules { - for s in &m.structs { - let fields = s.fields.iter().map(|f| &f.ty).collect(); - types.push((s.name.clone(), fields)); - } - for e in m.enums.iter().filter(|e| e.is_rich()) { - let fields = e - .variants - .iter() - .flat_map(|v| v.fields.iter().map(|f| &f.ty)) - .collect(); - types.push((e.name.clone(), fields)); - } - } - let mut hashable: HashSet = types.iter().map(|(n, _)| n.clone()).collect(); - loop { - let before = hashable.len(); - for (name, fields) in &types { - if hashable.contains(name) && !fields.iter().all(|t| type_is_hashable(t, &hashable)) { - hashable.remove(name); - } - } - if hashable.len() == before { - return hashable; - } + /// The `@available(*, deprecated, ...)` attribute of a deprecated + /// declaration, its message in Swift spelling. + pub(crate) fn deprecated_attr(&self, deprecated: &Option) -> Option { + Doc::new(&None, deprecated) + .deprecation(|i| self.spell(i)) + .map(|msg| { + format!( + "@available(*, deprecated, message: \"{}\")", + swift_str(&msg) + ) + }) } } -/// The Swift name of the lazy sequence class emitted for one `iter` -/// function: the iterator tag minus the C prefix, PascalCased -/// (`kvstore_kv_Store_ListKeysIterator` becomes `KvStoreListKeysIterator`). -pub(crate) fn iterator_class_name(it: &IteratorBinding, c_prefix: &str) -> String { - it.iter_tag - .strip_prefix(&format!("{c_prefix}_")) - .unwrap_or(&it.iter_tag) - .to_upper_camel_case() +/// The error type for failures outside any declared domain: +/// `{Module}RuntimeError`. +pub(crate) fn runtime_error_name(swift_module: &str) -> String { + format!("{swift_module}RuntimeError") +} + +/// The namespace type of the library: `{Module}Library`. +pub(crate) fn library_type_name(swift_module: &str) -> String { + format!("{swift_module}Library") } /// The internal namespace holding the process-wide vtable for the callback @@ -243,4 +224,10 @@ mod tests { assert_eq!(swift_member("init"), "init_"); assert_eq!(swift_member("close"), "close"); } + + #[test] + fn string_literals_escape_quotes_and_controls() { + assert_eq!(swift_str(r#"a "b" \c"#), r#"a \"b\" \\c"#); + assert_eq!(swift_str("x\ny\u{1}"), "x\\ny\\u{1}"); + } } diff --git a/crates/weaveffi-cli/src/targets/swift/xcframework.rs b/crates/weaveffi-cli/src/targets/swift/xcframework.rs new file mode 100644 index 00000000..8f7cd2d4 --- /dev/null +++ b/crates/weaveffi-cli/src/targets/swift/xcframework.rs @@ -0,0 +1,149 @@ +//! Assembling the `XCFramework` a packaged Swift package's binary target +//! points at, from the Apple static libraries `weaveffi build` laid out. + +use std::process::Command; + +use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, miette, IntoDiagnostic, Result, WrapErr}; + +use crate::package::{archive, PackageContext, PackagedFile}; +use crate::platform::{Os, Platform}; +use crate::targets::c::render_c_header_from_model; +use weaveffi_model::model::Model; + +/// The zipped `XCFramework`: its file name, bytes, and SHA-256 checksum (as +/// `swift package compute-checksum` prints it). +pub(super) struct XcframeworkZip { + pub(super) file_name: String, + pub(super) bytes: Vec, + pub(super) checksum: String, +} + +/// Fuse the Apple static libraries into `{c_module}.xcframework` and zip it. +/// +/// Slices are grouped the way `xcodebuild -create-xcframework` needs them +/// (iOS device, iOS simulator, macOS), with the architectures of a group +/// fused by `lipo`. Returns `None` when there are no Apple static libraries, +/// and records a skip when the host can't run `xcodebuild` or `lipo`. +pub(super) fn assemble( + model: &Model, + c_module: &str, + ctx: &PackageContext<'_>, +) -> Result> { + let binaries = ctx.binaries; + let static_lib = |p: Platform| binaries.get(p).and_then(|nb| nb.staticlib.clone()); + let any_apple = binaries + .platforms() + .any(|p| matches!(p.os(), Os::MacOs | Os::Ios) && static_lib(p).is_some()); + if !any_apple { + return Ok(None); + } + let what = "the Swift package's XCFramework"; + if !cfg!(target_os = "macos") { + ctx.skip(what, "xcodebuild needs macOS"); + return Ok(None); + } + for tool in ["xcodebuild", "lipo"] { + if Command::new(tool).arg("-version").output().is_err() { + ctx.skip(what, format!("`{tool}` isn't installed (install Xcode)")); + return Ok(None); + } + } + let library = &model.identity.library; + + let scratch = tempfile::tempdir().into_diagnostic()?; + let scratch = Utf8Path::from_path(scratch.path()) + .ok_or_else(|| miette!("temp directory path is not valid UTF-8"))? + .to_path_buf(); + let headers = scratch.join("Headers"); + std::fs::create_dir_all(headers.as_std_path()).into_diagnostic()?; + let header = format!("{library}.h"); + std::fs::write( + headers.join(&header).as_std_path(), + render_c_header_from_model(model, &header), + ) + .into_diagnostic()?; + std::fs::write( + headers.join("module.modulemap").as_std_path(), + format!("module {c_module} {{\n header \"{header}\"\n export *\n}}\n"), + ) + .into_diagnostic()?; + + let mut args: Vec = vec!["-create-xcframework".into()]; + for (group, platforms) in [ + ("ios", &[Platform::IosArm64][..]), + ( + "ios-simulator", + &[Platform::IosSimArm64, Platform::IosSimX64][..], + ), + ("macos", &[Platform::MacosArm64, Platform::MacosX64][..]), + ] { + let libs: Vec = platforms.iter().filter_map(|p| static_lib(*p)).collect(); + let lib = match libs.as_slice() { + [] => continue, + [one] => one.clone(), + many => { + let fat = scratch.join(group).join(format!("lib{library}.a")); + std::fs::create_dir_all(scratch.join(group).as_std_path()).into_diagnostic()?; + let status = Command::new("lipo") + .arg("-create") + .args(many.iter().map(|p| p.as_str())) + .args(["-output", fat.as_str()]) + .status() + .into_diagnostic() + .wrap_err("failed to run lipo")?; + if !status.success() { + bail!("lipo failed to fuse the {group} slices"); + } + fat + } + }; + args.extend(["-library".into(), lib.to_string()]); + args.extend(["-headers".into(), headers.to_string()]); + } + let framework_name = format!("{c_module}.xcframework"); + let framework = scratch.join(&framework_name); + args.extend(["-output".into(), framework.to_string()]); + let output = Command::new("xcodebuild") + .args(&args) + .output() + .into_diagnostic() + .wrap_err("failed to run xcodebuild")?; + if !output.status.success() { + bail!( + "xcodebuild -create-xcframework failed:\n{}", + String::from_utf8_lossy(&output.stderr).trim_end() + ); + } + + let mut files = Vec::new(); + collect_files(&framework, &framework_name, &mut files)?; + let entries = archive::entries(&files, None)?; + let bytes = archive::zip(&entries)?; + Ok(Some(XcframeworkZip { + checksum: archive::sha256_hex(&bytes), + file_name: format!("{framework_name}.zip"), + bytes, + })) +} + +/// Every file under `dir`, in sorted order, as an archive entry under +/// `prefix/`. +fn collect_files(dir: &Utf8Path, prefix: &str, out: &mut Vec) -> Result<()> { + let mut entries: Vec = dir + .read_dir_utf8() + .into_diagnostic()? + .map(|e| e.map(|e| e.path().to_path_buf())) + .collect::>() + .into_diagnostic()?; + entries.sort(); + for path in entries { + let name = format!("{prefix}/{}", path.file_name().unwrap_or_default()); + if path.is_dir() { + collect_files(&path, &name, out)?; + } else { + out.push(PackagedFile::copy(name, path)); + } + } + Ok(()) +} diff --git a/crates/weaveffi-cli/src/targets/wasm/glue.rs b/crates/weaveffi-cli/src/targets/wasm/glue.rs index c44f2174..acec5d55 100644 --- a/crates/weaveffi-cli/src/targets/wasm/glue.rs +++ b/crates/weaveffi-cli/src/targets/wasm/glue.rs @@ -3,25 +3,34 @@ //! //! Each entry point follows the raw calling convention of the shared //! JavaScript layer ([`crate::targets::js`]): it checks and converts direct -//! arguments, stages strings and byte runs in linear memory for the call, -//! passes the reused error and `out_len` slots, throws the reported error, -//! and turns results back into JavaScript values (copying and freeing -//! returned strings and buffers). Callback-interface vtables and async -//! completions are JavaScript functions installed in the module's function -//! table; a callback's string, bytes, or buffer return is copied into a run -//! from `{prefix}_alloc` that the producer adopts. +//! arguments (range-checking integers), splits optional scalars into a flag +//! and a value, stages strings, byte runs, and typed arrays in linear memory +//! for the call, passes the reused error and out slots, throws the reported +//! error, and turns results back into JavaScript values (copying and freeing +//! returned strings, buffers, and typed arrays). Callback-interface vtables +//! and async completions are JavaScript functions installed in the module's +//! function table; a callback's string, bytes, buffer, or typed-array +//! return is copied into a run from `{prefix}_alloc` that the producer +//! adopts. +//! +//! Every wasm call is built from the model's lowered signature ([`AbiFn`]): +//! each slot's argument is looked up by the slot's name, so the argument +//! order is the model's by construction. + +use std::collections::HashMap; use weaveffi_model::abi::{AbiParam, CType}; use weaveffi_model::model::{ - contract_symbol, CallShape, CallbackInterfaceBinding, FnBinding, Model, + contract_symbol, AbiFn, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, + IteratorBinding, Model, }; -use weaveffi_model::plan::{ArgPass, RetPass}; -use weaveffi_model::ty::{Prim, Ty}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; use crate::codegen::CodeWriter; -use crate::targets::js::names::direct_prim; +use crate::targets::js::names::typed_array; -/// The wasm value type of one C slot. +/// The wasm value type of one C slot (on wasm32, pointers, `size_t`, +/// `bool`, and every integer of 32 bits or fewer are `i32`). fn valtype(ty: &CType) -> &'static str { match ty { CType::Int64 | CType::Uint64 => "i64", @@ -31,96 +40,108 @@ fn valtype(ty: &CType) -> &'static str { } } -/// The Emscripten signature character of a wasm value type. -fn sig_char(valtype: &str) -> char { - match valtype { - "i64" => 'j', - "f32" => 'f', - "f64" => 'd', - _ => 'i', - } -} - -/// The `[params], [results], 'sig'` triple of a table function. +/// The `[params], [results]` pair of a table function. fn signature(params: &[AbiParam], ret: &CType) -> String { - let ps: Vec<&str> = params.iter().map(|p| valtype(&p.ty)).collect(); - let (results, ret_char) = match ret { - CType::Void => (String::new(), 'v'), - t => (format!("'{}'", valtype(t)), sig_char(valtype(t))), - }; - let sig: String = std::iter::once(ret_char) - .chain(ps.iter().map(|t| sig_char(t))) + let ps: Vec = params + .iter() + .map(|p| format!("'{}'", valtype(&p.ty))) .collect(); - let quoted: Vec = ps.iter().map(|t| format!("'{t}'")).collect(); - format!("[{}], [{results}], '{sig}'", quoted.join(", ")) + let results = match ret { + CType::Void => String::new(), + t => format!("'{}'", valtype(t)), + }; + format!("[{}], [{results}]", ps.join(", ")) } -/// The conversion of a direct JavaScript argument `v` for a wasm call. -fn direct_arg(p: Prim, v: &str) -> String { - match p { - Prim::Bool => format!("m.bool({v})"), - Prim::I64 => format!("m.i64({v})"), - Prim::U64 => format!("m.u64({v})"), - _ => format!("m.num({v})"), +/// The checked wasm argument of the scalar JavaScript value `v` for a slot +/// of type `ty`. +fn scalar_arg(ty: &CType, v: &str) -> String { + match ty { + CType::Bool => format!("m.bool({v})"), + CType::Int8 => format!("m.i8({v})"), + CType::Int16 => format!("m.i16({v})"), + CType::Int32 | CType::Enum { .. } => format!("m.i32({v})"), + CType::Uint8 => format!("m.u8({v})"), + CType::Uint16 => format!("m.u16({v})"), + CType::Uint32 => format!("m.u32({v})"), + CType::Int64 => format!("m.i64({v})"), + CType::Uint64 => format!("m.u64({v})"), + CType::Float | CType::Double => format!("m.num({v})"), + other => unreachable!("{other:?} is not a scalar slot"), } } -/// The JavaScript value of a direct wasm value `r`: unsigned integers and -/// bools are reinterpreted, everything else is already right. -fn direct_value(p: Prim, r: &str) -> String { - match p { - Prim::Bool => format!("{r} !== 0"), - Prim::U32 => format!("{r} >>> 0"), - Prim::U64 => format!("BigInt.asUintN(64, {r})"), - _ => r.to_string(), +/// The zero of a scalar slot's wasm value (`0n` for 64-bit integers). +fn scalar_zero(ty: &CType) -> &'static str { + match ty { + CType::Int64 | CType::Uint64 => "0n", + _ => "0", } } -/// The JavaScript value of a received value whose slots are `slots` (one, -/// or a pointer and a length). Owned values are freed after copying; -/// borrowed ones (callback arguments) are only copied. -fn receive(ty: &Ty, pass: &RetPass, slots: &[&str], owned: bool) -> String { - match pass { - RetPass::Void => "undefined".into(), - RetPass::Direct => direct_value(direct_prim(ty), slots[0]), - RetPass::String => format!( - "m.{}({}, {})", - if owned { "takeStr" } else { "readStr" }, - slots[0], - slots[1] - ), - RetPass::Bytes | RetPass::Buffer => format!( - "m.{}({}, {})", - if owned { "takeData" } else { "readData" }, - slots[0], - slots[1] - ), - RetPass::Object { nullable: true, .. } => format!("{0} === 0 ? null : {0}", slots[0]), - RetPass::Object { .. } => slots[0].to_string(), +/// The JavaScript value of the wasm value `r` of a scalar slot of type +/// `ty`: narrow integers are normalized from their `i32` carrier, unsigned +/// ones reinterpreted, and bools compared. +fn scalar_value(ty: &CType, r: &str) -> String { + match ty { + CType::Bool => format!("{r} !== 0"), + CType::Int8 => format!("({r} << 24) >> 24"), + CType::Int16 => format!("({r} << 16) >> 16"), + CType::Uint8 => format!("{r} & 0xff"), + CType::Uint16 => format!("{r} & 0xffff"), + CType::Uint32 => format!("{r} >>> 0"), + CType::Uint64 => format!("BigInt.asUintN(64, {r})"), + CType::Int32 | CType::Enum { .. } | CType::Int64 | CType::Float | CType::Double => { + r.to_string() + } + other => unreachable!("{other:?} is not a scalar slot"), } } -/// The `DataView` read of one iterator element of direct type `p` from the -/// `out_item` slot. -fn item_read(p: Prim) -> String { - let (get, le) = match p { - Prim::Bool => return "m.view().getUint8(m.item) !== 0".into(), - Prim::I8 => ("getInt8", false), - Prim::U8 => ("getUint8", false), - Prim::I16 => ("getInt16", true), - Prim::U16 => ("getUint16", true), - Prim::I32 => ("getInt32", true), - Prim::U32 => ("getUint32", true), - Prim::I64 => ("getBigInt64", true), - Prim::U64 => ("getBigUint64", true), - Prim::F32 => ("getFloat32", true), - Prim::F64 => ("getFloat64", true), - Prim::String | Prim::Bytes => unreachable!("not direct"), +/// The `DataView` read of a scalar of type `ty` stored at `addr`. +fn load(ty: &CType, addr: &str) -> String { + let get = match ty { + CType::Bool => return format!("m.view().getUint8({addr}) !== 0"), + CType::Int8 => return format!("m.view().getInt8({addr})"), + CType::Uint8 => return format!("m.view().getUint8({addr})"), + CType::Int16 => "getInt16", + CType::Uint16 => "getUint16", + CType::Int32 | CType::Enum { .. } => "getInt32", + CType::Uint32 => "getUint32", + CType::Int64 => "getBigInt64", + CType::Uint64 => "getBigUint64", + CType::Float => "getFloat32", + CType::Double => "getFloat64", + other => unreachable!("{other:?} is not a scalar slot"), }; - if le { - format!("m.view().{get}(m.item, true)") - } else { - format!("m.view().{get}(m.item)") + format!("m.view().{get}({addr}, true)") +} + +/// The `DataView` write of the scalar JavaScript value `v` (already +/// checked) of type `ty` at `addr`. +fn store(ty: &CType, addr: &str, v: &str) -> String { + let set = match ty { + CType::Bool => return format!("m.view().setUint8({addr}, {v} ? 1 : 0)"), + CType::Int8 => return format!("m.view().setInt8({addr}, {v})"), + CType::Uint8 => return format!("m.view().setUint8({addr}, {v})"), + CType::Int16 => "setInt16", + CType::Uint16 => "setUint16", + CType::Int32 | CType::Enum { .. } => "setInt32", + CType::Uint32 => "setUint32", + CType::Int64 => "setBigInt64", + CType::Uint64 => "setBigUint64", + CType::Float => "setFloat32", + CType::Double => "setFloat64", + other => unreachable!("{other:?} is not a scalar slot"), + }; + format!("m.view().{set}({addr}, {v}, true)") +} + +/// The type an out slot (`T* out_x`) points to. +fn pointee(ty: &CType) -> &CType { + match ty { + CType::Ptr { pointee, .. } => pointee, + other => unreachable!("{other:?} is not an out slot"), } } @@ -129,67 +150,124 @@ fn item_read(p: Prim) -> String { struct Args { /// The JavaScript parameter names. params: Vec, + /// Checks and conversions that run first, in parameter order, so a bad + /// argument throws before anything is staged or registered. + prep: Vec, /// Staging statements (inside the `try`). stage: Vec, /// Staged locals released in the `finally`. staged: Vec, - /// The wasm argument expressions, in slot order. - call: Vec, + /// The wasm argument expression of each slot, by slot name. + slots: HashMap, +} + +impl Args { + fn slot(&mut self, slot: &AbiParam, expr: impl Into) { + self.slots.insert(slot.name.clone(), expr.into()); + } + + /// The call of `abi` through `x`, every slot in the model's order. + fn call(&self, x: &str, abi: &AbiFn) -> String { + let args: Vec<&str> = abi + .params + .iter() + .map(|p| { + self.slots + .get(&p.name) + .unwrap_or_else(|| { + panic!("no argument for slot `{}` of {}", p.name, abi.symbol) + }) + .as_str() + }) + .collect(); + format!("{x}.{}({})", abi.symbol, args.join(", ")) + } } fn marshal(model: &Model, f: &FnBinding) -> Args { let mut a = Args::default(); - if f.has_self { + if let Some(recv) = &f.receiver { a.params.push("self".into()); - a.call.push("m.handle(self)".into()); + a.slot(recv, "m.handle(self)"); } for (n, p) in f.params.iter().enumerate() { let v = format!("a{n}"); a.params.push(v.clone()); - match p.arg_pass() { - ArgPass::Direct { .. } => a.call.push(direct_arg(direct_prim(&p.ty), &v)), - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + match &p.pass { + ArgPass::Direct { slot } => { + a.prep + .push(format!("const c{n} = {};", scalar_arg(&slot.ty, &v))); + a.slot(slot, format!("c{n}")); + } + ArgPass::OptDirect { has, value, .. } => { + a.prep.push(format!("const h{n} = m.some({v});")); + a.prep.push(format!( + "const c{n} = h{n} ? {} : {};", + scalar_arg(&value.ty, &v), + scalar_zero(&value.ty) + )); + a.slot(has, format!("h{n} ? 1 : 0")); + a.slot(value, format!("c{n}")); + } + ArgPass::Slice { ptr, len, elem } => { + let s = format!("s{n}"); + a.stage + .push(format!("{s} = m.slice({v}, {});", typed_array(*elem))); + a.staged.push(s.clone()); + a.slot(ptr, format!("{s}[0]")); + a.slot(len, format!("{s}[1]")); + } + ArgPass::String { ptr, len } + | ArgPass::Bytes { ptr, len } + | ArgPass::Buffer { ptr, len } => { let s = format!("s{n}"); - let stage = if matches!(p.ty, Ty::Prim(Prim::String)) { + let stage = if matches!(p.pass, ArgPass::String { .. }) { "str" } else { "data" }; a.stage.push(format!("{s} = m.{stage}({v});")); a.staged.push(s.clone()); - a.call.push(format!("{s}[0]")); - a.call.push(format!("{s}[1]")); + a.slot(ptr, format!("{s}[0]")); + a.slot(len, format!("{s}[1]")); } - ArgPass::Object { nullable, .. } => a.call.push(format!( - "m.{}({v})", - if nullable { "handleOpt" } else { "handle" } - )), - ArgPass::Callback { nullable, .. } => { - let cb = model.callback_interface( - p.ty.callback_interface_name().expect("callback parameter"), + ArgPass::Object { slot, nullable, .. } => { + let check = if *nullable { "handleOpt" } else { "handle" }; + a.prep.push(format!("const c{n} = m.{check}({v});")); + a.slot(slot, format!("c{n}")); + } + ArgPass::Callback { + ctx, + vtable, + nullable, + interface, + } => { + let tag = &model.callback_interface(interface).c_tag; + a.slot(ctx, format!("m.register({v})")); + a.slot( + vtable, + if *nullable { + format!("m.vtableOpt({v}, '{tag}', () => $vt_{tag}(m))") + } else { + format!("m.vtable('{tag}', () => $vt_{tag}(m))") + }, ); - a.call.push(format!("m.register({v})")); - a.call.push(if nullable { - format!( - "m.vtableOpt({v}, '{}', () => $vt_{}(m))", - cb.c_tag, cb.c_tag - ) - } else { - format!("m.vtable('{}', () => $vt_{}(m))", cb.c_tag, cb.c_tag) - }); } } } - if f.cancellable { + if let Some(token) = f.async_binding().and_then(|ab| ab.cancel_token.as_ref()) { a.params.push("token".into()); - a.call.push("m.handleOpt(token)".into()); + a.slot(token, "m.handleOpt(token)"); } a } -/// Emit `body` (the lines of an entry point) wrapped in the staging -/// `try`/`finally` when anything is staged. +/// Emit `body` (the lines of an entry point) after the argument checks, +/// wrapped in the staging `try`/`finally` when anything is staged. fn emit_staged(w: &mut CodeWriter, a: &Args, body: &[String]) { + for l in &a.prep { + w.line(l); + } if a.staged.is_empty() { for l in body { w.line(l); @@ -216,55 +294,10 @@ fn emit_staged(w: &mut CodeWriter, a: &Args, body: &[String]) { w.line("}"); } -/// The runtime symbols the glue calls. -const RUNTIME_SYMBOLS: &[&str] = &[ - "abi_version", - "alloc", - "free_bytes", - "error_set", - "error_set_payload", - "error_clear", - "error_free", - "debug_live", - "cancel_token_create", - "cancel_token_cancel", - "cancel_token_destroy", -]; - -/// Every C symbol the glue calls, which Emscripten mode binds by name. -pub(crate) fn bound_symbols(model: &Model) -> Vec { - let prefix = model.prefix(); - let mut out: Vec = RUNTIME_SYMBOLS - .iter() - .map(|s| format!("{prefix}_{s}")) - .collect(); - out.extend( - model - .roots() - .map(|root| contract_symbol(prefix, &root.name)), - ); - for m in &model.modules { - for i in &m.interfaces { - out.push(i.clone_symbol.clone()); - out.push(i.destroy_symbol.clone()); - } - for f in m.callables() { - match &f.shape { - CallShape::Sync(abi) => out.push(abi.symbol.clone()), - CallShape::Async(ab) => out.push(ab.launch.symbol.clone()), - CallShape::Iterator(it) => out.extend([ - it.launch.symbol.clone(), - it.next.symbol.clone(), - it.destroy_symbol.clone(), - ]), - } - } - } - out -} - /// Render `$bind(m)`, which builds the raw entry points over a loaded -/// `$Linear`, preceded by the callback-interface vtable builders. +/// `$Linear`, preceded by the callback-interface vtable builders. Releases +/// (`_destroy`, cancel tokens) go through `m.q`, which does nothing once the +/// instance is poisoned. pub(crate) fn render_bind(w: &mut CodeWriter, model: &Model) { let prefix = model.prefix(); for (_, cb) in model.callback_interfaces() { @@ -274,19 +307,20 @@ pub(crate) fn render_bind(w: &mut CodeWriter, model: &Model) { w.line("// The raw entry points, one per C symbol."); w.block("function $bind(m) {", "}", |w| { w.line("const x = m.x;"); + w.line("const q = m.q;"); w.block("return {", "};", |w| { w.line(format!( "{prefix}_abi_version: () => x.{prefix}_abi_version() >>> 0," )); w.line(format!( - "{prefix}_debug_live: (kind) => m.live(x.{prefix}_debug_live(m.num(kind)), kind)," + "{prefix}_debug_live: (kind) => m.live(x.{prefix}_debug_live(m.i32(kind)), kind)," )); w.line(format!( "{prefix}_cancel_token_create: () => x.{prefix}_cancel_token_create()," )); for op in ["cancel", "destroy"] { w.line(format!( - "{prefix}_cancel_token_{op}: (t) => x.{prefix}_cancel_token_{op}(m.handle(t))," + "{prefix}_cancel_token_{op}: (t) => q.{prefix}_cancel_token_{op}(m.handle(t))," )); } for root in model.roots() { @@ -295,9 +329,8 @@ pub(crate) fn render_bind(w: &mut CodeWriter, model: &Model) { } for m in &model.modules { for i in &m.interfaces { - for sym in [&i.clone_symbol, &i.destroy_symbol] { - w.line(format!("{sym}: (h) => x.{sym}(m.handle(h)),")); - } + w.line(format!("{0}: (h) => x.{0}(m.handle(h)),", i.clone_symbol)); + w.line(format!("{0}: (h) => q.{0}(m.handle(h)),", i.destroy_symbol)); } for f in m.callables() { emit_callable(w, model, f); @@ -308,183 +341,312 @@ pub(crate) fn render_bind(w: &mut CodeWriter, model: &Model) { } fn emit_callable(w: &mut CodeWriter, model: &Model, f: &FnBinding) { - let a = marshal(model, f); - let head = |sym: &str| format!("{sym}: ({}) => {{", a.params.join(", ")); - match &f.shape { - CallShape::Sync(abi) => { - let pass = RetPass::of(f.ret.as_ref()); - let pair = matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer); - let mut call = a.call.clone(); - if pair { - call.push("m.len".into()); + let mut a = marshal(model, f); + let head = |sym: &str, params: &[String]| format!("{sym}: ({}) => {{", params.join(", ")); + let symbol = &f.abi.symbol; + if let Some(ab) = f.async_binding() { + let names: Vec = ab.result.slots().iter().map(|p| p.name.clone()).collect(); + let value = match &ab.result { + ResultPass::Void => "undefined".to_string(), + ResultPass::Direct { result } => scalar_value(&result.ty, &result.name), + ResultPass::OptDirect { has, value } => format!( + "{} !== 0 ? {} : null", + has.name, + scalar_value(&value.ty, &value.name) + ), + ResultPass::Slice { ptr, len, elem } => format!( + "m.takeSlice({}, {}, {})", + ptr.name, + len.name, + typed_array(*elem) + ), + ResultPass::String { ptr, len } => format!("m.takeStr({}, {})", ptr.name, len.name), + ResultPass::Bytes { ptr, len } | ResultPass::Buffer { ptr, len } => { + format!("m.takeData({}, {})", ptr.name, len.name) } - call.push("m.err".into()); - let call = format!("x.{}({})", abi.symbol, call.join(", ")); - let mut body = Vec::new(); - match &pass { - RetPass::Void => { - body.push(format!("{call};")); - body.push("m.check();".into()); - } - pass => { - body.push(format!("const r = {call};")); - body.push("m.check();".into()); - let ty = f.ret.as_ref().expect("non-void"); - body.push(format!( - "return {};", - receive(ty, pass, &["r", "m.outLen()"], true) - )); + ResultPass::Object { + result, nullable, .. + } => { + if *nullable { + format!("{0} === 0 ? null : {0}", result.name) + } else { + result.name.clone() } } - w.block(head(&abi.symbol), "},", |w| emit_staged(w, &a, &body)); + }; + let convert = format!("({}) => {value}", names.join(", ")); + a.slots.insert("callback".into(), "cb".into()); + a.slots.insert("context".into(), "ctx".into()); + let body = vec![format!( + "return m.launch({}, (cb, ctx) => {}, {convert});", + signature(&ab.callback_params, &CType::Void), + a.call("x", &f.abi) + )]; + w.block(head(symbol, &a.params), "},", |w| emit_staged(w, &a, &body)); + return; + } + + let out_ptr = |slot: &AbiParam| -> &'static str { + if slot.name == "out_value" { + "m.out" + } else { + "m.len" } - CallShape::Iterator(it) => { - let mut call = a.call.clone(); - call.push("m.err".into()); - let body = vec![ - format!("const r = x.{}({});", it.launch.symbol, call.join(", ")), - "m.check();".into(), - "return r;".into(), - ]; - w.block(head(&it.launch.symbol), "},", |w| emit_staged(w, &a, &body)); + }; + for slot in f.ret_pass.out_slots() { + a.slot(slot, out_ptr(slot)); + } + a.slots.insert("out_err".into(), "m.err".into()); + let call = a.call("x", &f.abi); + let value = match &f.ret_pass { + RetPass::Void => None, + RetPass::Direct => Some(scalar_value(&f.abi.ret, "r")), + RetPass::OptDirect { out_value } => Some(format!( + "r !== 0 ? {} : null", + load(pointee(&out_value.ty), "m.out") + )), + RetPass::Slice { elem, .. } => Some(format!( + "m.takeSlice(r, m.outLen(), {})", + typed_array(*elem) + )), + RetPass::String { .. } => Some("m.takeStr(r, m.outLen())".into()), + RetPass::Bytes { .. } | RetPass::Buffer { .. } => Some("m.takeData(r, m.outLen())".into()), + RetPass::Object { nullable: true, .. } => Some("r === 0 ? null : r".into()), + RetPass::Object { .. } | RetPass::Iterator(_) => Some("r".into()), + }; + let body = match &value { + None => vec![format!("{call};"), "m.check();".into()], + Some(v) => vec![ + format!("const r = {call};"), + "m.check();".into(), + format!("return {v};"), + ], + }; + w.block(head(symbol, &a.params), "},", |w| emit_staged(w, &a, &body)); - let protocol = it.protocol(f); - let pair = matches!( - protocol.elem, - RetPass::String | RetPass::Bytes | RetPass::Buffer - ); - let len = if pair { "m.len, " } else { "" }; - w.block(format!("{}: (h) => {{", it.next.symbol), "},", |w| { - w.line(format!( - "const has = x.{}(m.handle(h), m.item, {len}m.err);", - it.next.symbol - )); - w.line("m.check();"); - w.line("if (has === 0) return undefined;"); - match &protocol.elem { - RetPass::Direct => { - w.line(format!("return {};", item_read(direct_prim(&it.elem)))); - } - pass => { - w.line("const p = m.view().getUint32(m.item, true);"); - w.line(format!( - "return {};", - receive(&it.elem, pass, &["p", "m.outLen()"], true) - )); - } - } - }); - w.line(format!( - "{0}: (h) => x.{0}(m.handle(h)),", - it.destroy_symbol - )); + if let Some(it) = f.iterator() { + emit_iterator(w, it); + } +} + +/// Emit an iterator's `next` (`undefined` once exhausted, else the element, +/// `null` for an absent optional one) and `destroy` entry points. +fn emit_iterator(w: &mut CodeWriter, it: &IteratorBinding) { + let mut a = Args::default(); + a.slot(&it.next.params[0], "m.handle(h)"); + a.slots.insert("out_err".into(), "m.err".into()); + let value = match &it.item { + ItemPass::Direct { out_item } => { + a.slot(out_item, "m.item"); + load(pointee(&out_item.ty), "m.item") } - CallShape::Async(ab) => { - let pass = RetPass::of(f.ret.as_ref()); - let names: Vec = ab.callback_params[2..] - .iter() - .map(|p| p.name.clone()) - .collect(); - let slots: Vec<&str> = names.iter().map(String::as_str).collect(); - let convert = match &f.ret { - None => "() => undefined".to_string(), - Some(ty) => format!( - "({}) => {}", - names.join(", "), - receive(ty, &pass, &slots, true) - ), - }; - let mut call = a.call.clone(); - call.push("cb".into()); - call.push("ctx".into()); - let body = vec![format!( - "return m.launch({}, (cb, ctx) => x.{}({}), {convert});", - signature(&ab.callback_params, &CType::Void), - ab.launch.symbol, - call.join(", ") - )]; - w.block(head(&ab.launch.symbol), "},", |w| emit_staged(w, &a, &body)); + ItemPass::OptDirect { out_has, out_item } => { + a.slot(out_has, "m.has"); + a.slot(out_item, "m.item"); + format!( + "m.view().getUint8(m.has) !== 0 ? {} : null", + load(pointee(&out_item.ty), "m.item") + ) } - } + ItemPass::Slice { + out_item, + out_len, + elem, + } => { + a.slot(out_item, "m.item"); + a.slot(out_len, "m.len"); + format!( + "m.takeSlice(m.ptrAt(m.item), m.outLen(), {})", + typed_array(*elem) + ) + } + ItemPass::String { out_item, out_len } => { + a.slot(out_item, "m.item"); + a.slot(out_len, "m.len"); + "m.takeStr(m.ptrAt(m.item), m.outLen())".into() + } + ItemPass::Bytes { out_item, out_len } | ItemPass::Buffer { out_item, out_len } => { + a.slot(out_item, "m.item"); + a.slot(out_len, "m.len"); + "m.takeData(m.ptrAt(m.item), m.outLen())".into() + } + ItemPass::Object { + out_item, nullable, .. + } => { + a.slot(out_item, "m.item"); + if *nullable { + "m.ptrAt(m.item) || null".into() + } else { + "m.ptrAt(m.item)".into() + } + } + }; + w.block(format!("{}: (h) => {{", it.next.symbol), "},", |w| { + w.line(format!("const has = {};", a.call("x", &it.next))); + w.line("m.check();"); + w.line("if (has === 0) return undefined;"); + w.line(format!("return {value};")); + }); + w.line(format!( + "{0}: (h) => q.{0}(m.handle(h)),", + it.destroy_symbol + )); +} + +/// The JavaScript parameter of a callback method slot. +fn field(slot: &AbiParam) -> String { + format!("p_{}", slot.name) } /// Emit `$vt_{tag}(m)`, the table functions of one callback interface's /// vtable methods: each converts the raw slots into the values the adapter /// takes, calls it, and hands the result back (a direct value or object -/// handle as the return value, a string, bytes, or buffer as a run in the -/// `out_ptr`/`out_len` slots), reporting an exception through `out_err` -/// instead of letting it unwind into the module. +/// handle as the return value, an optional scalar as the `bool` return and +/// a value behind `out_value`, a string, bytes, buffer, or typed array as a +/// run in the `out_ptr`/`out_len` slots), reporting an exception through +/// `out_err` instead of letting it unwind into the module. fn emit_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding) { - let protocol = cb.protocol(); w.block(format!("function $vt_{}(m) {{", cb.c_tag), "}", |w| { w.block("return [", "];", |w| { - for ((method, passes), ret) in cb - .methods - .iter() - .zip(&protocol.method_args) - .zip(&protocol.method_returns) - { - let inputs: usize = method.params.iter().map(|p| p.abi.len()).sum(); - let names: Vec = (0..inputs).map(|i| format!("p{i}")).collect(); - let mut k = 0usize; - let args: Vec = method - .params - .iter() - .zip(passes) - .map(|(p, pass)| { - let s: Vec<&str> = names[k..k + p.abi.len()] - .iter() - .map(String::as_str) - .collect(); - k += p.abi.len(); - receive(&p.ty, pass, &s, false) - }) - .collect(); - let call = format!("m.adapter(ctx).{}({})", method.name, args.join(", ")); - let mut params = vec!["ctx".to_string()]; - params.extend(names.iter().cloned()); - let (body, fallback) = match (ret, &method.ret) { - (RetPass::Void, _) | (_, None) => (format!("{call};"), None), - (RetPass::Direct, Some(ty)) => match direct_prim(ty) { - Prim::Bool => (format!("return {call} ? 1 : 0;"), Some("return 0;")), - Prim::I64 | Prim::U64 => (format!("return {call};"), Some("return 0n;")), - _ => (format!("return {call};"), Some("return 0;")), - }, - (RetPass::Object { .. }, _) => { - (format!("return m.handleOpt({call});"), Some("return 0;")) - } - (RetPass::String, _) => { - params.extend(["out".to_string(), "outLen".to_string()]); - (format!("m.giveStr(out, outLen, {call});"), None) - } - (RetPass::Bytes | RetPass::Buffer, _) => { - params.extend(["out".to_string(), "outLen".to_string()]); - (format!("m.give(out, outLen, {call});"), None) - } - }; - params.push("err".into()); - w.line(format!( - "[{}, ({}) => {{", - signature(&method.abi_params, &method.abi_ret), - params.join(", ") - )); - w.scope(|w| { - w.line("try {"); - w.scope(|w| { - w.line(&body); - }); - w.line("} catch (e) {"); - w.scope(|w| { - w.line("m.foreign(err, e);"); - if let Some(f) = fallback { - w.line(f); - } - }); - w.line("}"); - }); - w.line("}],"); + for method in &cb.methods { + emit_vtable_method(w, method); } }); }); w.blank(); } + +fn emit_vtable_method(w: &mut CodeWriter, method: &CallbackMethodBinding) { + let args: Vec = method + .params + .iter() + .map(|p| match &p.pass { + ArgPass::Direct { slot } => scalar_value(&slot.ty, &field(slot)), + ArgPass::OptDirect { has, value, .. } => format!( + "{} !== 0 ? {} : null", + field(has), + scalar_value(&value.ty, &field(value)) + ), + ArgPass::Slice { ptr, len, elem } => format!( + "m.readSlice({}, {}, {})", + field(ptr), + field(len), + typed_array(*elem) + ), + ArgPass::String { ptr, len } => format!("m.readStr({}, {})", field(ptr), field(len)), + ArgPass::Bytes { ptr, len } | ArgPass::Buffer { ptr, len } => { + format!("m.readData({}, {})", field(ptr), field(len)) + } + ArgPass::Object { slot, nullable, .. } => { + if *nullable { + format!("{0} === 0 ? null : {0}", field(slot)) + } else { + field(slot) + } + } + ArgPass::Callback { .. } => unreachable!("callback methods take value types only"), + }) + .collect(); + let call = format!("m.adapter(p_ctx).{}({})", method.name, args.join(", ")); + let ret = &method.abi.ret; + let (body, fallback): (Vec, Option<&str>) = match &method.ret_pass { + CallbackRetPass::Void => (vec![format!("{call};")], None), + CallbackRetPass::Direct => { + let value = if *ret == CType::Bool { + format!("{call} ? 1 : 0") + } else { + call + }; + (vec![format!("return {value};")], Some(scalar_zero(ret))) + } + CallbackRetPass::OptDirect { out_value } => ( + vec![ + format!("const v = {call};"), + "if (v === null) return 0;".into(), + format!( + "{};", + store( + pointee(&out_value.ty), + &format!("{} >>> 0", field(out_value)), + "v" + ) + ), + "return 1;".into(), + ], + Some("0"), + ), + CallbackRetPass::Slice { + out_ptr, + out_len, + elem, + } => ( + vec![format!( + "m.giveSlice({}, {}, {call}, {});", + field(out_ptr), + field(out_len), + typed_array(*elem) + )], + None, + ), + CallbackRetPass::String { out_ptr, out_len } => ( + vec![format!( + "m.giveStr({}, {}, {call});", + field(out_ptr), + field(out_len) + )], + None, + ), + CallbackRetPass::Bytes { out_ptr, out_len } + | CallbackRetPass::Buffer { out_ptr, out_len } => ( + vec![format!( + "m.give({}, {}, {call});", + field(out_ptr), + field(out_len) + )], + None, + ), + CallbackRetPass::Object { .. } => (vec![format!("return m.handleOpt({call});")], Some("0")), + }; + let params: Vec = method.abi.params.iter().map(field).collect(); + let err = field(method.abi.params.last().expect("out_err is the last slot")); + w.line(format!( + "[{}, ({}) => {{", + signature(&method.abi.params, ret), + params.join(", ") + )); + w.scope(|w| { + w.line("try {"); + w.scope(|w| { + for l in &body { + w.line(l); + } + }); + w.line("} catch (e) {"); + w.scope(|w| { + w.line(format!("m.foreign({err}, e);")); + if let Some(zero) = fallback { + w.line(format!("return {zero};")); + } + }); + w.line("}"); + }); + w.line("}],"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn narrow_integers_are_normalized_from_their_carrier() { + assert_eq!(scalar_value(&CType::Int8, "r"), "(r << 24) >> 24"); + assert_eq!(scalar_value(&CType::Uint16, "r"), "r & 0xffff"); + assert_eq!(scalar_value(&CType::Uint64, "r"), "BigInt.asUintN(64, r)"); + assert_eq!(scalar_arg(&CType::Uint8, "a0"), "m.u8(a0)"); + assert_eq!(scalar_zero(&CType::Int64), "0n"); + assert_eq!( + store(&CType::Int16, "p", "v"), + "m.view().setInt16(p, v, true)" + ); + } +} diff --git a/crates/weaveffi-cli/src/targets/wasm/mod.rs b/crates/weaveffi-cli/src/targets/wasm/mod.rs index ad9967b9..5f84af50 100644 --- a/crates/weaveffi-cli/src/targets/wasm/mod.rs +++ b/crates/weaveffi-cli/src/targets/wasm/mod.rs @@ -1,33 +1,33 @@ //! WebAssembly binding generator. //! -//! Emits an npm package for a `wasm32-unknown-unknown` build of the library -//! (or, in Emscripten mode, an Emscripten build): the ES module (`index.js`, -//! `index.d.ts`, `runtime.js`) shared with the Node.js target through -//! the shared JavaScript layer, and its transport, the linear-memory glue -//! (`linear.js` plus one generated entry point per C symbol) that stages -//! arguments in the module's memory and installs async completions and -//! callback-interface vtables in its function table. `init()` loads the -//! module and checks its ABI revision and contract tables before any -//! other export works. Implements [`LanguageBackend`]; the shared driver -//! bridges it into the generator pipeline. +//! Emits an npm package for a `wasm32-unknown-unknown` build of the +//! library: the ES module (`index.js`, `index.d.ts`, `runtime.js`, and the +//! `./debug` export) shared with the Node.js target through the +//! [shared JavaScript layer](crate::targets::js), and its transport, the +//! linear-memory glue (`linear.js` plus one generated entry point per C +//! symbol) that stages arguments in the module's memory and installs async +//! completions and callback-interface vtables in its function table. +//! `init()` loads the module and checks its ABI revision and contract +//! tables before any other export works. A trap poisons the instance: every +//! later call throws. mod glue; mod package; -use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_model::model::Model; use weaveffi_model::pkg::Identity; -use crate::backend::{LanguageBackend, OutputFile}; use crate::codegen::CodeWriter; +use crate::codegen::OutputFile; use crate::package::{Artifact, PackageContext, PackagedFile}; use crate::platform::Platform; -use crate::targets::js; -use crate::targets::node::npm_tarball_name; +use crate::targets::js::{self, npm_tarball_name}; +use crate::targets::{Linkage, Target}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use miette::Result; -use self::glue::{bound_symbols, render_bind}; +use self::glue::render_bind; use self::package::{render_package_json, render_readme}; /// The fixed transport: loading, staging, and table functions. @@ -39,12 +39,6 @@ const LINEAR_JS: &str = include_str!("runtime/linear.js"); pub struct WasmConfig { /// npm package name (default: the package identity's name). pub name: Option, - /// Target an Emscripten build instead of a `wasm32-unknown-unknown` - /// one: `init` then takes the initialized Emscripten module (or the - /// promise its `MODULARIZE` factory returns), binds its - /// underscore-prefixed exports, and installs table functions with - /// `addFunction`. - pub emscripten: bool, } impl WasmConfig { @@ -54,24 +48,26 @@ impl WasmConfig { } /// WebAssembly backend: an ES module over linear-memory glue. -pub struct WasmGenerator; +pub struct WasmGenerator { + config: WasmConfig, +} + +impl From for WasmGenerator { + fn from(config: WasmConfig) -> Self { + Self { config } + } +} /// The generated `index.js`: the imports, `init()` with the load-time /// checks, the raw entry points, and the shared API. -fn render_index(model: &Model, identity: &Identity, emscripten: bool) -> String { +fn render_index(model: &Model) -> String { + let identity = &model.identity; let name = js::names::js_string(&identity.name); let prefix = model.prefix(); let mut w = CodeWriter::two_space(); w.raw(render_prelude(CommentStyle::DoubleSlash)); js::render_imports(&mut w, identity); - let loader = if emscripten { - "$loadEmscripten" - } else { - "$loadWasm" - }; - w.line(format!( - "import {{ {loader}, $unloaded }} from './linear.js';" - )); + w.line("import { $loadWasm, $unloaded } from './linear.js';"); w.blank(); w.line(format!("let $raw = $unloaded({name});")); w.line("// Handles are linear-memory addresses; object tokens are u64s."); @@ -80,48 +76,30 @@ fn render_index(model: &Model, identity: &Identity, emscripten: bool) -> String w.blank(); js::render_contract(&mut w, model); w.blank(); - let load = if emscripten { - let list: Vec = bound_symbols(model) - .iter() - .map(|s| format!("'{s}'")) - .collect(); - w.line(format!("const $SYMBOLS = [{}];", list.join(", "))); - w.blank(); - w.line("/**"); - w.line(" * Adopt the initialized Emscripten module (or the promise its factory"); - w.line(" * returns) and check that it matches these bindings. Every other export"); - w.line(" * throws until the returned promise resolves."); - w.line(" */"); - w.line("export function init(module) {"); - format!("$loadEmscripten(module, '{prefix}', $SYMBOLS)") - } else { - w.line("/**"); - w.line(format!( - " * Load the WebAssembly module (by default `{}.wasm` next to this file, or", - identity.library - )); + w.line("/**"); + w.line(format!( + " * Load the WebAssembly module (by default `{}.wasm` next to this file, or", + identity.library + )); + w.line(format!( + " * the file named by `{}` on Node.js) and check that it matches", + identity.library_env_var() + )); + w.line(" * these bindings. Every other export throws until the returned promise"); + w.line(" * resolves."); + w.line(" */"); + w.block("export function init(source) {", "}", |w| { w.line(format!( - " * the file named by `{}` on Node.js) and check that it matches", - identity.library_env_var() - )); - w.line(" * these bindings. Every other export throws until the returned promise"); - w.line(" * resolves."); - w.line(" */"); - w.line("export function init(source) {"); - format!( - "$loadWasm(source, '{prefix}', '{}', new URL('./{}.wasm', import.meta.url))", + "$loading ??= $loadWasm({name}, source, '{prefix}', '{}', new URL('./{}.wasm', import.meta.url))", identity.library_env_var(), identity.library - ) - }; - w.scope(|w| { - w.line(format!("$loading ??= {load}")); + )); w.scope(|w| { w.block(".then((m) => {", "})", |w| { w.line("$raw = $bind(m);"); w.line("try {"); w.scope(|w| { - w.line(js::verify_call(model, identity)); + w.line(js::verify_call(model)); }); w.line("} catch (e) {"); w.scope(|w| { @@ -137,7 +115,6 @@ fn render_index(model: &Model, identity: &Identity, emscripten: bool) -> String }); w.line("return $loading;"); }); - w.line("}"); w.blank(); render_bind(&mut w, model); w.blank(); @@ -147,108 +124,90 @@ fn render_index(model: &Model, identity: &Identity, emscripten: bool) -> String } /// The transport's own declarations in `index.d.ts`. -fn dts_extra(emscripten: bool) -> String { - let mut out = String::from( - "/**\n * Load the WebAssembly module and check that it matches these bindings.\n \ - * Every other export throws until the returned promise resolves.\n */\n", - ); - if emscripten { - out.push_str( - "export declare function init(module: object | Promise): Promise;\n", - ); - } else { - out.push_str( - "export declare function init(\n source?: string | URL | BufferSource | WebAssembly.Module | Response,\n): Promise;\n", - ); - } - out -} +const DTS_EXTRA: &str = "/** + * Load the WebAssembly module and check that it matches these bindings. + * Every other export throws until the returned promise resolves. + */ +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; +"; /// Every file of the generated package, as `(name, contents)` pairs. -fn render_files( - model: &Model, - identity: &Identity, - config: &WasmConfig, - wasm: Option<&str>, -) -> Vec<(String, String)> { +fn render_files(model: &Model, config: &WasmConfig, wasm: Option<&str>) -> Vec<(String, String)> { + let identity = &model.identity; let name = config.package_name(identity); - let modules: Vec = model.roots().map(js::names::module_name).collect(); + let modules: Vec = model + .roots() + .map(|m| js::names::module_name(&m.name)) + .collect(); let mut linear = render_prelude(CommentStyle::DoubleSlash); linear.push_str(LINEAR_JS); linear.push('\n'); linear.push_str(&render_trailer(CommentStyle::DoubleSlash, "linear.js")); vec![ - ( - "index.js".into(), - render_index(model, identity, config.emscripten), - ), - ( - "index.d.ts".into(), - js::render_dts(model, identity, &dts_extra(config.emscripten)), - ), + ("index.js".into(), render_index(model)), + ("index.d.ts".into(), js::render_dts(model, DTS_EXTRA)), ("runtime.js".into(), js::render_runtime(identity)), + ("debug.js".into(), js::render_debug_js()), + ("debug.d.ts".into(), js::render_debug_dts()), ("linear.js".into(), linear), ( "package.json".into(), render_package_json(identity, &name, wasm), ), - ( - "README.md".into(), - render_readme(identity, &name, &modules, config.emscripten), - ), + ("README.md".into(), render_readme(identity, &name, &modules)), ] } -impl LanguageBackend for WasmGenerator { - type Config = WasmConfig; - +impl Target for WasmGenerator { fn name(&self) -> &'static str { "wasm" } + fn linkage(&self) -> Linkage { + Linkage::Wasm + } + + fn fixed_files(&self) -> &'static [&'static str] { + &[ + "README.md", + "debug.d.ts", + "debug.js", + "linear.js", + "package.json", + "runtime.js", + ] + } + /// Every feature is supported. Async completions and callback-interface /// methods are JavaScript functions installed in the module's function /// table. The target is single-threaded, so a callback only ever runs /// while a call into the module is on the stack, and an async call /// usually completes before its launcher returns. - fn files(&self, model: &Model, out_dir: &Utf8Path, config: &Self::Config) -> Vec { - let dir = out_dir.join("wasm"); - render_files(model, &model.identity, config, None) + fn render(&self, model: &Model) -> Vec { + render_files(model, &self.config, None) .into_iter() - .map(|(name, contents)| OutputFile::new(dir.join(name), contents)) + .map(|(name, contents)| OutputFile::new(name, contents)) .collect() } /// The npm tarball with the prebuilt `wasm32-unknown-unknown` module /// bundled as `{library}.wasm`, where `init()` finds it by default. - /// Emscripten mode ships glue only (the consumer builds the module), and - /// without a `wasm32` build there is nothing to package. - fn package( - &self, - model: &Model, - ctx: &PackageContext, - config: &Self::Config, - ) -> Option> { + /// Without a `wasm32` build there is nothing to package. + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { let identity = &model.identity; - let wasm_name = format!("{}.wasm", identity.library); - let binary = if config.emscripten { - None - } else { - match ctx.binaries.get(Platform::Wasm32) { - Some(binary) => Some(binary), - None => return Some(Vec::new()), - } + let Some(binary) = ctx.binaries.get(Platform::Wasm32) else { + return Ok(Vec::new()); }; - let mut files: Vec = - render_files(model, identity, config, binary.map(|_| wasm_name.as_str())) - .into_iter() - .map(|(name, contents)| PackagedFile::text(name, contents)) - .collect(); - if let Some(b) = binary { - files.push(PackagedFile::copy(wasm_name, b.library.clone())); - } - let name = config.package_name(identity); - Some(vec![Artifact::tar_gz( + let wasm_name = format!("{}.wasm", identity.library); + let mut files: Vec = render_files(model, &self.config, Some(&wasm_name)) + .into_iter() + .map(|(name, contents)| PackagedFile::text(name, contents)) + .collect(); + files.push(PackagedFile::copy(wasm_name, binary.library.clone())); + let name = self.config.package_name(identity); + Ok(vec![Artifact::tar_gz( format!("wasm/{}", npm_tarball_name(&name, &identity.version)), "package", files, diff --git a/crates/weaveffi-cli/src/targets/wasm/package.rs b/crates/weaveffi-cli/src/targets/wasm/package.rs index ff07ba19..8c35b9ec 100644 --- a/crates/weaveffi-cli/src/targets/wasm/package.rs +++ b/crates/weaveffi-cli/src/targets/wasm/package.rs @@ -3,28 +3,24 @@ use weaveffi_model::pkg::Identity; use crate::manifest::JsonValue; -use crate::targets::js::npm_metadata; +use crate::targets::js::{npm_metadata, PACKAGE_FILES}; use crate::utils::{render_prelude, render_trailer, CommentStyle}; /// Render `package.json`. `wasm` is the bundled module's file name, when /// the package carries one. pub(crate) fn render_package_json(identity: &Identity, name: &str, wasm: Option<&str>) -> String { - let mut files = vec!["index.js", "index.d.ts", "runtime.js", "linear.js"]; - if let Some(w) = wasm { - files.push(w); - } + let files = PACKAGE_FILES + .iter() + .copied() + .chain(["linear.js"]) + .chain(wasm); npm_metadata(identity, name) .entry("files", JsonValue::str_array(files)) .render() } /// Render the README. -pub(crate) fn render_readme( - identity: &Identity, - name: &str, - modules: &[String], - emscripten: bool, -) -> String { +pub(crate) fn render_readme(identity: &Identity, name: &str, modules: &[String]) -> String { let lib = &identity.library; let env = identity.library_env_var(); let mut out = render_prelude(CommentStyle::Xml); @@ -33,25 +29,15 @@ pub(crate) fn render_readme( identity.description_or_default() )); let imports = modules.join(", "); - if emscripten { - out.push_str(&format!( - "WebAssembly bindings for an Emscripten build of the `{lib}` library. Build \ - the module with `-sMODULARIZE -sALLOW_TABLE_GROWTH -sWASM_BIGINT \ - -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8` and pass it to `init`:\n\n\ - ```js\nimport Module from './{lib}.js';\nimport {{ init, {imports} }} from '{name}';\n\n\ - await init(Module());\n```\n\n" - )); - } else { - out.push_str(&format!( - "WebAssembly bindings for a `wasm32-unknown-unknown` build of the `{lib}` \ - library, linked with `--export-table --growable-table` (for example \ - `RUSTFLAGS=\"-C link-arg=--export-table -C link-arg=--growable-table\"`). \ - `init()` loads `{lib}.wasm` from next to this package, or from the path in \ - `{env}` on Node.js; pass a URL, bytes, or a compiled module to load it from \ - elsewhere:\n\n```js\nimport {{ init, {imports} }} from '{name}';\n\n\ - await init();\n```\n\n" - )); - } + out.push_str(&format!( + "WebAssembly bindings for a `wasm32-unknown-unknown` build of the `{lib}` \ + library, linked with `--export-table --growable-table` (for example \ + `RUSTFLAGS=\"-C link-arg=--export-table -C link-arg=--growable-table\"`). \ + `init()` loads `{lib}.wasm` from next to this package, or from the path in \ + `{env}` on Node.js; pass a URL, bytes, or a compiled module to load it from \ + elsewhere:\n\n```js\nimport {{ init, {imports} }} from '{name}';\n\n\ + await init();\n```\n\n" + )); out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); out } diff --git a/crates/weaveffi-cli/src/targets/wasm/runtime/linear.js b/crates/weaveffi-cli/src/targets/wasm/runtime/linear.js index ec79ab51..528892d3 100644 --- a/crates/weaveffi-cli/src/targets/wasm/runtime/linear.js +++ b/crates/weaveffi-cli/src/targets/wasm/runtime/linear.js @@ -4,71 +4,114 @@ // interface vtables). The generated `index.js` builds one native entry point // per C symbol on top of this. // -// Byte runs from `{prefix}_alloc` have alignment 1, so every multi-byte -// read and write goes through a `DataView`, and the slots the producer -// writes through typed pointers (the error struct, `out_len`, iterator -// items, vtables) live in 8-byte-aligned blocks carved from the transport's -// own arenas. +// Runs from {prefix}_alloc are 8-aligned, but multi-byte reads and writes +// still go through a `DataView` (little-endian, any address). The slots the +// producer writes through typed pointers (the error struct, `out_len`, +// `out_value`, iterator items, vtables) live in 8-byte-aligned blocks carved +// from the transport's own arenas. +// +// A trap (a producer panic aborts on wasm32) leaves the instance's memory in +// an unknown state, so it poisons the instance: every later call throws a +// code -2 fault instead of running, and releases (`_destroy`, cancel +// tokens, freeing runs) do nothing. import { $Fault } from './runtime.js'; const $enc = new TextEncoder(); const $dec = new TextDecoder(); +function $show(v) { + if (v === null) return 'null'; + if (typeof v === 'bigint') return `${v}n`; + if (typeof v === 'number') return String(v); + return typeof v; +} + function $expected(what, v) { - return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); + return new TypeError(`expected ${what}, got ${$show(v)}`); +} + +/** Integer argument checks: `[min, max]` per kind. */ +function $int(v, lo, hi) { + if (typeof v !== 'number') throw $expected('a number', v); + if (!Number.isInteger(v) || v < lo || v > hi) { + throw new RangeError(`expected an integer in [${lo}, ${hi}], got ${v}`); + } + return v; } -function $bigint(v, bits, what) { +function $int64(v, signed) { + const shape = signed ? 'a signed 64-bit integer' : 'an unsigned 64-bit integer'; let b; if (typeof v === 'bigint') { b = v; - } else if (typeof v === 'number' && Number.isInteger(v)) { + } else if (typeof v === 'number') { + if (!Number.isInteger(v)) throw new RangeError(`expected ${shape}, got ${v}`); b = BigInt(v); } else { throw $expected('a bigint', v); } - if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + if ((signed ? BigInt.asIntN(64, b) : BigInt.asUintN(64, b)) !== b) { + throw new RangeError(`expected ${shape}, got ${b}`); + } return b; } /** The size of each arena the transport carves its permanent blocks from. */ const $ARENA = 4096; +/** Offsets in `{prefix}_error` on wasm32: code, message (ptr, len), payload (ptr, len). */ +const $ERR_SIZE = 20; + /** - * One loaded module's linear memory and runtime symbols. `x` holds the - * module's exports by C symbol name; `memory()` returns the current - * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the - * function table and returns its index. + * One loaded module's linear memory and runtime symbols. `exports` holds + * the module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, fn)` installs `fn` in the function + * table and returns its index. + * + * `x` holds every exported function guarded against a poisoned instance (a + * call throws), `q` the same functions for releases (a call does nothing). */ export class $Linear { - constructor(x, prefix, memory, fnptr) { - this.x = x; + constructor(library, exports, prefix, memory, fnptr) { + this.library = library; this.memory = memory; this.fnptr = fnptr; - this.allocFn = x[`${prefix}_alloc`]; - this.freeBytesFn = x[`${prefix}_free_bytes`]; - this.errorClearFn = x[`${prefix}_error_clear`]; - this.errorFreeFn = x[`${prefix}_error_free`]; - this.errorSetFn = x[`${prefix}_error_set`]; - this.errorSetPayloadFn = x[`${prefix}_error_set_payload`]; - if (typeof this.allocFn !== 'function' || typeof this.freeBytesFn !== 'function') { - throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_free_bytes`); + this.trap = null; + this.x = {}; + this.q = {}; + for (const [name, fn] of Object.entries(exports)) { + if (typeof fn !== 'function') continue; + this.x[name] = (...args) => this.enter(fn, args); + this.q[name] = (...args) => (this.trap === null ? this.enter(fn, args) : undefined); + } + for (const symbol of ['alloc', 'free_bytes', 'error_set', 'error_set_payload', 'error_clear', 'error_free']) { + if (typeof exports[`${prefix}_${symbol}`] !== 'function') { + throw new Error(`${library}: the WebAssembly module does not export ${prefix}_${symbol}`); + } } + this.allocFn = this.x[`${prefix}_alloc`]; + this.freeBytesFn = this.q[`${prefix}_free_bytes`]; + this.errorClearFn = this.q[`${prefix}_error_clear`]; + this.errorFreeFn = this.q[`${prefix}_error_free`]; + this.errorSetFn = this.x[`${prefix}_error_set`]; + this.errorSetPayloadFn = this.x[`${prefix}_error_set_payload`]; // Permanent blocks (the scratch slots and the vtables) come from // arenas allocated with {prefix}_alloc and never freed; `arenas` counts // them so the leak counter of byte runs can leave them out. this.arenas = 0; this.arenaNext = 0; this.arenaEnd = 0; - // One scratch block reused by every call: the error slot (16 bytes), - // the out_len slot (4), and the iterator out_item slot (8). The producer - // only writes them as a call returns, so a call made from a callback in - // the middle of another call can reuse them safely. - const scratch = this.reserve(32); + // One scratch block reused by every call: the error slot, `out_len`, + // `out_value` (and an iterator's item), and an iterator's presence + // flag. The producer only writes them as a call returns, so a call made + // from a callback in the middle of another call can reuse them safely. + const scratch = this.reserve(48); this.err = scratch; - this.len = scratch + 16; - this.item = scratch + 24; + this.len = scratch + 24; + this.out = scratch + 32; + this.item = scratch + 32; + this.has = scratch + 40; this.callbacks = new Map(); this.nextCallback = 1; this.pending = new Map(); @@ -77,14 +120,36 @@ export class $Linear { this.vtables = new Map(); } + /** + * Call the module function `fn`. A trap poisons the instance (see the + * file comment); a poisoned instance throws instead of calling. + */ + enter(fn, args) { + if (this.trap !== null) throw this.poisoned(); + try { + return fn(...args); + } catch (e) { + if (e instanceof WebAssembly.RuntimeError && this.trap === null) this.trap = e; + throw e; + } + } + + poisoned() { + return new $Fault( + -2, + `${this.library}: the WebAssembly instance trapped earlier (${this.trap.message}) and can't be used anymore; load the module again in a new process or page`, + null, + ); + } + bytes() { const buffer = this.memory(); if (buffer !== this.buffer) { this.buffer = buffer; - this.u8 = new Uint8Array(buffer); + this.heap = new Uint8Array(buffer); this.dv = new DataView(buffer); } - return this.u8; + return this.heap; } view() { @@ -124,25 +189,33 @@ export class $Linear { return kind === 4 && n > 0n ? n - BigInt(this.arenas) : n; } - // Argument checks and conversions for by-value slots. + // Argument checks and conversions for by-value slots: integers are + // range-checked (a `RangeError` rather than a silent wrap), 64-bit ones + // take a bigint or an integral number. + i8(v) { return $int(v, -128, 127); } + i16(v) { return $int(v, -32768, 32767); } + i32(v) { return $int(v, -2147483648, 2147483647); } + u8(v) { return $int(v, 0, 255); } + u16(v) { return $int(v, 0, 65535); } + u32(v) { return $int(v, 0, 4294967295); } + i64(v) { return $int64(v, true); } + u64(v) { return $int64(v, false); } + num(v) { if (typeof v !== 'number') throw $expected('a number', v); return v; } - i64(v) { - return $bigint(v, BigInt.asIntN, 'a signed'); - } - - u64(v) { - return $bigint(v, BigInt.asUintN, 'an unsigned'); - } - bool(v) { if (typeof v !== 'boolean') throw $expected('a boolean', v); return v ? 1 : 0; } + /** Whether an optional argument is present (neither null nor undefined). */ + some(v) { + return v !== null && v !== undefined; + } + handle(v) { if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); return v; @@ -152,8 +225,9 @@ export class $Linear { return v === null || v === undefined ? 0 : this.handle(v); } - // Staging: copy a string or bytes into a run for one call. A staged value - // is [ptr, len, size]; release it with unstage(). + // Staging: copy a string, bytes, or a typed array into a run for one + // call. A staged value is [ptr, len, size] (`len` counts elements of a + // typed array); release it with unstage(). str(s) { if (typeof s !== 'string') throw $expected('a string', s); if (s.length === 0) return [0, 0, 0]; @@ -171,6 +245,14 @@ export class $Linear { return [ptr, b.length, b.length]; } + slice(a, Typed) { + if (!(a instanceof Typed)) throw $expected(`a ${Typed.name}`, a); + if (a.length === 0) return [0, 0, 0]; + const ptr = this.alloc(a.byteLength); + this.bytes().set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), ptr); + return [ptr, a.length, a.byteLength]; + } + unstage(staged) { if (staged !== null && staged[2] !== 0) this.freeBytesFn(staged[0], staged[2]); } @@ -180,6 +262,11 @@ export class $Linear { return this.view().getUint32(this.len, true); } + /** The pointer stored at `addr`. */ + ptrAt(addr) { + return this.view().getUint32(addr, true); + } + readStr(ptr, len) { ptr >>>= 0; return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); @@ -190,6 +277,13 @@ export class $Linear { return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); } + /** A copy of the typed array of `count` elements at `ptr`. */ + readSlice(ptr, count, Typed) { + ptr >>>= 0; + if (count === 0) return new Typed(0); + return new Typed(this.memory().slice(ptr, ptr + count * Typed.BYTES_PER_ELEMENT)); + } + takeStr(ptr, len) { const s = this.readStr(ptr, len); if (ptr !== 0) this.freeBytesFn(ptr, len); @@ -202,24 +296,21 @@ export class $Linear { return b; } - cstr(ptr) { - ptr >>>= 0; - if (ptr === 0) return ''; - const u8 = this.bytes(); - let end = ptr; - while (u8[end] !== 0) end++; - return $dec.decode(u8.subarray(ptr, end)); + takeSlice(ptr, count, Typed) { + const a = this.readSlice(ptr, count, Typed); + if (ptr !== 0) this.freeBytesFn(ptr, count * Typed.BYTES_PER_ELEMENT); + return a; } fault(errPtr) { const dv = this.view(); const code = dv.getInt32(errPtr, true); if (code === 0) return null; - const payloadPtr = dv.getUint32(errPtr + 8, true); - const payloadLen = dv.getUint32(errPtr + 12, true); + const payloadPtr = dv.getUint32(errPtr + 12, true); + const payloadLen = dv.getUint32(errPtr + 16, true); return new $Fault( code, - this.cstr(dv.getUint32(errPtr + 4, true)), + this.readStr(dv.getUint32(errPtr + 4, true), dv.getUint32(errPtr + 8, true)), payloadPtr === 0 ? null : this.readData(payloadPtr, payloadLen), ); } @@ -262,18 +353,10 @@ export class $Linear { this.callbacks.delete(id); } - /** Stage `text` as a NUL-terminated C string (release it with unstage()). */ - cstrArg(text) { - const bytes = $enc.encode(text); - const ptr = this.alloc(bytes.length + 1); - this.bytes().set(bytes, ptr); - return [ptr, bytes.length, bytes.length + 1]; - } - /** * Report a callback implementation's exception through `errPtr`: a - * `$Fault` (a domain error a throwing method raised) with its code, - * message, and payload, anything else as code -4 with its message. + * `$Fault` (the adapter's report of a method declared `throws`) with its + * code, message, and payload, anything else as code -4 with its message. */ foreign(errPtr, e) { let code = -4; @@ -286,9 +369,9 @@ export class $Linear { } else { message = e instanceof Error ? e.message || e.name : String(e); } - const text = this.cstrArg(message); + const text = this.str(message); try { - this.errorSetFn(errPtr, code, text[0]); + this.errorSetFn(errPtr, code, text[0], text[1]); } finally { this.unstage(text); } @@ -321,22 +404,34 @@ export class $Linear { this.give(outPtr, outLen, $enc.encode(s)); } + /** `give` for a typed array of `Typed`; `out_len` is its element count. */ + giveSlice(outPtr, outLen, a, Typed) { + if (!(a instanceof Typed)) throw $expected(`a ${Typed.name}`, a); + const run = this.alloc(a.byteLength); + if (a.length > 0) this.bytes().set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), run); + const dv = this.view(); + dv.setUint32(outPtr >>> 0, run, true); + dv.setUint32(outLen >>> 0, a.length, true); + } + /** * The static vtable of one callback interface, built on first use: the * header (`size`, `flags`, then the `free` entry), then one table entry - * per method (`methods()` returns each as `[params, results, sig, fn]`). + * per method (`methods()` returns each as `[params, results, fn]`). + * `flags` is 0: methods may be called from any thread (there is only + * one). */ vtable(name, methods) { let ptr = this.vtables.get(name); if (ptr !== undefined) return ptr; - const entries = [[['i32'], [], 'vi', (ctx) => this.unregister(ctx)], ...methods()]; + const entries = [[['i32'], [], (ctx) => this.unregister(ctx)], ...methods()]; const size = 8 + entries.length * 4; ptr = this.reserve(size); const dv = this.view(); dv.setUint32(ptr, size, true); dv.setUint32(ptr + 4, 0, true); - entries.forEach(([params, results, sig, fn], i) => { - dv.setUint32(ptr + 8 + i * 4, this.fnptr(params, results, sig, fn), true); + entries.forEach(([params, results, fn], i) => { + dv.setUint32(ptr + 8 + i * 4, this.fnptr(params, results, fn), true); }); this.vtables.set(name, ptr); return ptr; @@ -354,12 +449,13 @@ export class $Linear { * `convert`. On this single-threaded target the completion normally * fires before the launcher returns. */ - launch(params, results, sig, call, convert) { - let callback = this.completions.get(sig); + launch(params, results, call, convert) { + const key = `${params.join(',')}:${results.join(',')}`; + let callback = this.completions.get(key); if (callback === undefined) { const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); - callback = this.fnptr(params, results, sig, complete); - this.completions.set(sig, callback); + callback = this.fnptr(params, results, complete); + this.completions.set(key, callback); } const id = this.nextPending++; return new Promise((resolve, reject) => { @@ -473,38 +569,20 @@ async function $bytesOf(source, envVar, fallback) { * else `fallback`. The module must export its memory and a growable * function table (`__indirect_function_table`). */ -export async function $loadWasm(source, prefix, envVar, fallback) { +export async function $loadWasm(library, source, prefix, envVar, fallback) { const bytes = await $bytesOf(source, envVar, fallback); const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); const { exports } = await WebAssembly.instantiate(module, {}); const table = exports.__indirect_function_table; - const fnptr = (params, results, sig, fn) => { + const fnptr = (params, results, fn) => { if (!(table instanceof WebAssembly.Table)) { - throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + throw new Error( + `${library}: the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)`, + ); } const index = table.grow(1); table.set(index, $wasmFunction(params, results, fn)); return index; }; - return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); -} - -/** - * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` - * factory returns). Its exports carry a leading underscore, its memory is - * read through `HEAPU8`, and table entries are installed with - * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, - * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. - */ -export async function $loadEmscripten(module, prefix, symbols) { - const m = await module; - const x = {}; - for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; - const fnptr = (params, results, sig, fn) => { - if (typeof m.addFunction !== 'function') { - throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); - } - return m.addFunction(fn, sig); - }; - return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); + return new $Linear(library, exports, prefix, () => exports.memory.buffer, fnptr); } diff --git a/crates/weaveffi-cli/src/targets/wasm/tests.rs b/crates/weaveffi-cli/src/targets/wasm/tests.rs index 8817962c..5537e383 100644 --- a/crates/weaveffi-cli/src/targets/wasm/tests.rs +++ b/crates/weaveffi-cli/src/targets/wasm/tests.rs @@ -2,10 +2,10 @@ use super::*; use crate::platform::{BinarySet, NativeBinary}; use crate::targets::js::test_api; -fn index(config: &WasmConfig) -> String { +fn index() -> String { let model = test_api("acme-kv"); - WasmGenerator - .files(&model, Utf8Path::new("out"), config) + WasmGenerator::from(WasmConfig::default()) + .render(&model) .into_iter() .find(|f| f.path.file_name() == Some("index.js")) .expect("index.js") @@ -19,7 +19,7 @@ fn has(src: &str, needle: &str) { #[test] fn files_are_named_for_the_package() { let model = test_api("acme-kv"); - let out = WasmGenerator.files(&model, Utf8Path::new("out"), &WasmConfig::default()); + let out = WasmGenerator::from(WasmConfig::default()).render(&model); let names: Vec<&str> = out.iter().filter_map(|f| f.path.file_name()).collect(); assert_eq!( names, @@ -27,149 +27,76 @@ fn files_are_named_for_the_package() { "index.js", "index.d.ts", "runtime.js", + "debug.js", + "debug.d.ts", "linear.js", "package.json", "README.md" ] ); - let manifest = &out[4].contents; - has(manifest, "\"name\": \"acme-kv\","); - has(manifest, "\"type\": \"module\","); + let manifest: serde_json::Value = + serde_json::from_str(&out[6].contents).expect("valid package.json"); + assert_eq!(manifest["name"], "acme-kv"); + assert_eq!(manifest["type"], "module"); + assert_eq!( + manifest["files"], + serde_json::json!([ + "index.js", + "index.d.ts", + "runtime.js", + "debug.js", + "debug.d.ts", + "linear.js" + ]) + ); has(&out[1].contents, "export declare function init("); } +#[test] +fn emscripten_mode_is_gone() { + let table: toml::Table = toml::from_str("emscripten = true").unwrap(); + let err = toml::Value::Table(table) + .try_into::() + .expect_err("an unknown key"); + assert!(err.to_string().contains("emscripten"), "{err}"); + let js = index(); + assert!(!js.to_lowercase().contains("emscripten"), "{js}"); +} + #[test] fn init_loads_and_verifies_the_module() { - let js = index(&WasmConfig::default()); + let js = index(); has(&js, "import { $loadWasm, $unloaded } from './linear.js';"); has(&js, "let $raw = $unloaded('acme-kv');"); has( &js, - "$loading ??= $loadWasm(source, 'acme_kv', 'ACME_KV_LIBRARY', new URL('./acme_kv.wasm', import.meta.url))", + "$loading ??= $loadWasm('acme-kv', source, 'acme_kv', 'ACME_KV_LIBRARY', new URL('./acme_kv.wasm', import.meta.url))", ); - has(&js, "const $contract = [\n ['kv', [\n"); has( &js, - " try {\n $verify($raw, 'acme-kv', 'acme_kv', 4, $contract);\n } catch (e) {", - ); - has( - &js, - "acme_kv_kv_contract: () => m.contract(x.acme_kv_kv_contract),", - ); - has( - &js, - "acme_kv_debug_live: (kind) => m.live(x.acme_kv_debug_live(m.num(kind)), kind),", + " try {\n $verify($raw, 'acme-kv', 'acme_kv', 5, $contract);\n } catch (e) {", ); assert!(!js.contains("weaveffi_"), "{js}"); - assert!(!js.contains("checksum"), "{js}"); -} - -#[test] -fn glue_stages_strings_and_reuses_the_error_slot() { - let js = index(&WasmConfig::default()); - has( - &js, - "acme_kv_kv_Store_get: (self, a0) => {\n let s0 = null;\n try {\n s0 = m.str(a0);\n const r = x.acme_kv_kv_Store_get(m.handle(self), s0[0], s0[1], m.len, m.err);\n m.check();\n return m.takeData(r, m.outLen());\n } finally {\n m.unstage(s0);\n }\n },", - ); - has( - &js, - "const r = x.acme_kv_kv_widen(m.i64(a0), m.u64(a1), m.err);", - ); - has(&js, "return BigInt.asUintN(64, r);"); - has( - &js, - "const r = x.acme_kv_kv_delete(m.handleOpt(a0), m.err);", - ); - has(&js, "return r !== 0;"); -} - -#[test] -fn async_and_callbacks_install_table_functions() { - let js = index(&WasmConfig::default()); - has( - &js, - "return m.launch(['i32', 'i32', 'i64'], [], 'viij', (cb, ctx) => x.acme_kv_kv_Store_wait(m.handle(self), m.i64(a0), m.handleOpt(token), cb, ctx), (result) => result);", - ); - has(&js, "(result) => result === 0 ? null : result"); - has( - &js, - "m.register(a0), m.vtable('acme_kv_kv_Listener', () => $vt_acme_kv_kv_Listener(m))", - ); - has( - &js, - "x.acme_kv_kv_install(m.register(a0), m.vtableOpt(a0, 'acme_kv_kv_Policy', () => $vt_acme_kv_kv_Policy(m)), m.err);", - ); - has( - &js, - "[['i32', 'i32', 'i32', 'i64', 'i32'], ['i32'], 'iiiiji', (ctx, p0, p1, p2, err) => {", - ); - has( - &js, - "return m.adapter(ctx).on_message(m.readStr(p0, p1), BigInt.asUintN(64, p2)) ? 1 : 0;", - ); - has(&js, "m.foreign(err, e);"); - has( - &js, - "m.adapter(ctx).on_bundle(m.readData(p0, p1), p2, p3 === 0 ? null : p3);", - ); } #[test] -fn callback_returns_of_every_family_reach_the_producer() { - let js = index(&WasmConfig::default()); - // Strings, bytes, and buffers go out through the out slots as runs. +fn releases_survive_a_poisoned_instance() { + let js = index(); has( &js, - "[['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiiii', (ctx, p0, p1, out, outLen, err) => {\n try {\n m.give(out, outLen, m.adapter(ctx).admit(m.readData(p0, p1)));", + "acme_kv_kv_Store_destroy: (h) => q.acme_kv_kv_Store_destroy(m.handle(h)),", ); - has(&js, "m.giveStr(out, outLen, m.adapter(ctx).label());"); - has(&js, "m.give(out, outLen, m.adapter(ctx).blob());"); - // Objects and direct values are the table function's result. has( &js, - "return m.handleOpt(m.adapter(ctx).pick(m.readStr(p0, p1)));\n } catch (e) {\n m.foreign(err, e);\n return 0;\n }", + "acme_kv_kv_ShapesIterator_destroy: (h) => q.acme_kv_kv_ShapesIterator_destroy(m.handle(h)),", ); - has(&js, "return m.handleOpt(m.adapter(ctx).maybe());"); has( &js, - "return m.adapter(ctx).weight();\n } catch (e) {\n m.foreign(err, e);\n return 0n;", + "acme_kv_cancel_token_destroy: (t) => q.acme_kv_cancel_token_destroy(m.handle(t)),", ); -} - -#[test] -fn iterators_read_the_item_slot() { - let js = index(&WasmConfig::default()); has( &js, - "acme_kv_kv_Store_ScanIterator_next: (h) => {\n const has = x.acme_kv_kv_Store_ScanIterator_next(m.handle(h), m.item, m.err);\n m.check();\n if (has === 0) return undefined;\n const p = m.view().getUint32(m.item, true);\n return p;\n },", - ); - has( - &js, - "const has = x.acme_kv_kv_ShapesIterator_next(m.handle(h), m.item, m.len, m.err);", - ); - has(&js, "return m.takeData(p, m.outLen());"); -} - -#[test] -fn emscripten_mode_binds_underscored_exports() { - let js = index(&WasmConfig { - emscripten: true, - ..WasmConfig::default() - }); - has( - &js, - "import { $loadEmscripten, $unloaded } from './linear.js';", - ); - has( - &js, - "const $SYMBOLS = ['acme_kv_abi_version', 'acme_kv_alloc', 'acme_kv_free_bytes', 'acme_kv_error_set', 'acme_kv_error_set_payload',", - ); - has(&js, "'acme_kv_kv_contract'"); - has(&js, "'acme_kv_kv_Store_wait'"); - assert!(!js.contains("dealloc"), "{js}"); - has( - &js, - "$loading ??= $loadEmscripten(module, 'acme_kv', $SYMBOLS)", + "acme_kv_kv_Store_clone: (h) => x.acme_kv_kv_Store_clone(m.handle(h)),", ); } @@ -179,8 +106,8 @@ fn package_bundles_the_wasm_module() { let mut binaries = BinarySet::new("kv"); binaries.insert(NativeBinary::new(Platform::Wasm32, "target/kv.wasm")); let ctx = PackageContext::new(&binaries); - let artifacts = WasmGenerator - .package(&model, &ctx, &WasmConfig::default()) + let artifacts = WasmGenerator::from(WasmConfig::default()) + .package(&model, &ctx) .expect("a wasm32 binary packages"); assert_eq!(artifacts.len(), 1); let tgz = &artifacts[0]; @@ -194,16 +121,8 @@ fn package_bundles_the_wasm_module() { let none = BinarySet::new("kv"); let ctx = PackageContext::new(&none); - assert!(WasmGenerator - .package(&model, &ctx, &WasmConfig::default()) + assert!(WasmGenerator::from(WasmConfig::default()) + .package(&model, &ctx) .unwrap() .is_empty()); - let emscripten = WasmConfig { - emscripten: true, - ..WasmConfig::default() - }; - let glue = WasmGenerator - .package(&model, &ctx, &emscripten) - .expect("Emscripten mode ships glue only"); - assert!(glue[0].files.iter().all(|f| !f.is_binary())); } diff --git a/crates/weaveffi-cli/tests/abi_version.rs b/crates/weaveffi-cli/tests/abi_version.rs index 24e702c5..642eaad6 100644 --- a/crates/weaveffi-cli/tests/abi_version.rs +++ b/crates/weaveffi-cli/tests/abi_version.rs @@ -5,5 +5,8 @@ #[test] fn runtime_and_generator_abi_revisions_agree() { - assert_eq!(weaveffi::abi::ABI_VERSION, weaveffi_cli::cabi::ABI_VERSION); + assert_eq!( + weaveffi::abi::ABI_VERSION, + weaveffi_model::model::ABI_VERSION + ); } diff --git a/crates/weaveffi-cli/tests/c_buffer.rs b/crates/weaveffi-cli/tests/c_buffer.rs index 5da40b62..abbbfa1d 100644 --- a/crates/weaveffi-cli/tests/c_buffer.rs +++ b/crates/weaveffi-cli/tests/c_buffer.rs @@ -7,15 +7,14 @@ use std::process::Command; use camino::Utf8Path; -use weaveffi_cli::backend::LanguageBackend; -use weaveffi_cli::targets::c::{CConfig, CGenerator}; +use weaveffi_cli::targets; use weaveffi_model::model::Model; use weaveffi_model::parse::parse_api_str; use weaveffi_model::pkg::Identity; use weaveffi_model::validate::validate; const SHOP: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: shop enums: @@ -46,23 +45,23 @@ modules: - { name: blob, type: bytes } - { name: matrix, type: "[[u8]]" } errors: - name: ShopError - codes: - - name: OutOfStock - code: 1 - message: "out of stock" - fields: - - { name: missing, type: "[string]" } + - name: ShopError + codes: + - name: OutOfStock + code: 1 + message: "out of stock" + fields: + - { name: missing, type: "[string]" } functions: - name: echo params: - { name: item, type: Item } return: "Item?" - throws: true + throws: ShopError "#; const PLAIN: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: math functions: @@ -78,9 +77,17 @@ fn load(yaml: &str, name: &str) -> Model { validate(&api, &Identity::named(name), None).expect("validate") } -fn generate(model: &Model, config: &CConfig, dir: &Utf8Path) -> Vec<(String, String)> { - CGenerator - .files(model, dir, config) +/// The C target with the `[generators.c]` table `config`. +fn c_target(config: &str) -> Box { + targets::find("c") + .expect("the C target") + .build(toml::from_str(config).expect("a TOML table")) + .expect("a valid C configuration") +} + +fn generate(model: &Model, config: &str) -> Vec<(String, String)> { + c_target(config) + .render(model) .into_iter() .map(|f| { let name = f.path.file_name().unwrap_or_default().to_string(); @@ -91,7 +98,7 @@ fn generate(model: &Model, config: &CConfig, dir: &Utf8Path) -> Vec<(String, Str fn buffer_header(yaml: &str, name: &str) -> String { let api = load(yaml, name); - let files = generate(&api, &CConfig::default(), Utf8Path::new("out")); + let files = generate(&api, ""); files .into_iter() .find(|(n, _)| n == &format!("{name}_buffer.h")) @@ -102,36 +109,24 @@ fn buffer_header(yaml: &str, name: &str) -> String { #[test] fn emits_the_helper_header_only_when_buffers_cross_the_abi() { let api = load(SHOP, "shop"); - let names: Vec = generate(&api, &CConfig::default(), Utf8Path::new("out")) - .into_iter() - .map(|(n, _)| n) - .collect(); + let names: Vec = generate(&api, "").into_iter().map(|(n, _)| n).collect(); assert_eq!(names, ["shop.h", "shop_buffer.h"]); - let off = CConfig { - buffer_helpers: false, - }; - let names: Vec = generate(&api, &off, Utf8Path::new("out")) + let names: Vec = generate(&api, "buffer_helpers = false") .into_iter() .map(|(n, _)| n) .collect(); assert_eq!(names, ["shop.h"]); let plain = load(PLAIN, "math"); - let names: Vec = generate(&plain, &CConfig::default(), Utf8Path::new("out")) - .into_iter() - .map(|(n, _)| n) - .collect(); + let names: Vec = generate(&plain, "").into_iter().map(|(n, _)| n).collect(); assert_eq!(names, ["math.h"]); } #[test] -fn config_defaults_to_helpers_on_and_rejects_unknown_keys() { - let cfg: CConfig = serde_json::from_str("{}").expect("empty config"); - assert!(cfg.buffer_helpers); - let cfg: CConfig = serde_json::from_str(r#"{"buffer_helpers": false}"#).expect("flag"); - assert!(!cfg.buffer_helpers); - assert!(serde_json::from_str::(r#"{"prefix": "x"}"#).is_err()); +fn config_rejects_unknown_keys() { + let table = toml::from_str("prefix = \"x\"").unwrap(); + assert!(targets::find("c").unwrap().build(table).is_err()); } #[test] @@ -149,7 +144,7 @@ fn renders_structs_unions_and_payloads() { "static inline bool shop_shop_ShopError_OutOfStock_payload_decode(const uint8_t* ptr, size_t len, shop_shop_ShopError_OutOfStock_payload* out);" )); assert!(h.contains( - "static inline void shop_opt_shop_Item_write(shop_writer* w, const shop_shop_Item* v);" + "static inline void shop_opt_Item_write(shop_writer* w, const shop_shop_Item* v);" )); // The price union is defined before the record that embeds it. let price = h.find("struct shop_shop_Price {").expect("price struct"); @@ -192,11 +187,11 @@ int main(void) { shop_writer w; memset(&w, 0, sizeof w); - shop_opt_shop_Item_write(&w, &item); + shop_opt_Item_write(&w, &item); assert(!w.failed); shop_shop_Item* back = NULL; - assert(shop_opt_shop_Item_decode(w.ptr, w.len, &back)); + assert(shop_opt_Item_decode(w.ptr, w.len, &back)); assert(back != NULL); assert(back->name.len == 5 && memcmp(back->name.ptr, "caf\xC3\xA9", 5) == 0); assert(back->name.ptr[5] == '\0'); @@ -210,26 +205,26 @@ int main(void) { assert(strcmp(back->price.as.Note.text.ptr, "half") == 0); assert(back->blob.len == 2 && back->blob.ptr[1] == 2); assert(back->matrix.len == 2 && back->matrix.items[0].len == 2 && back->matrix.items[1].len == 0); - shop_opt_shop_Item_free(&back); + shop_opt_Item_free(&back); assert(back == NULL); /* Truncation anywhere fails cleanly and leaves nothing allocated. */ for (size_t cut = 0; cut < w.len; cut++) { shop_shop_Item* partial = NULL; - assert(!shop_opt_shop_Item_decode(w.ptr, cut, &partial)); + assert(!shop_opt_Item_decode(w.ptr, cut, &partial)); assert(partial == NULL); } /* Trailing bytes are rejected. */ shop_writer_put_u8(&w, 0); - assert(!shop_opt_shop_Item_decode(w.ptr, w.len, &back)); + assert(!shop_opt_Item_decode(w.ptr, w.len, &back)); shop_writer_free(&w); /* An absent optional, a bad presence byte, a bad tag, and a count the buffer can't hold. */ const uint8_t absent[1] = {0}; - assert(shop_opt_shop_Item_decode(absent, 1, &back) && back == NULL); + assert(shop_opt_Item_decode(absent, 1, &back) && back == NULL); const uint8_t bad_flag[1] = {2}; - assert(!shop_opt_shop_Item_decode(bad_flag, 1, &back)); + assert(!shop_opt_Item_decode(bad_flag, 1, &back)); const uint8_t bad_tag[4] = {9, 0, 0, 0}; shop_shop_Price price; assert(!shop_shop_Price_decode(bad_tag, 4, &price)); @@ -238,6 +233,26 @@ int main(void) { assert(!shop_list_string_decode(huge, 4, &list)); assert(list.items == NULL && list.len == 0); + /* A string must be well-formed UTF-8: a stray continuation byte, an + overlong encoding, and an encoded surrogate all fail the decoder. */ + const uint8_t bad_utf8[3][7] = { + {3, 0, 0, 0, 'a', 0x80, 'b'}, + {3, 0, 0, 0, 0xC0, 0xAF, 'b'}, + {3, 0, 0, 0, 0xED, 0xA0, 0x80}, + }; + for (int i = 0; i < 3; i++) { + shop_list_string one; + uint8_t buf[11] = {1, 0, 0, 0}; + memcpy(buf + 4, bad_utf8[i], 7); + assert(!shop_list_string_decode(buf, 11, &one)); + assert(one.items == NULL && one.len == 0); + } + const uint8_t good_utf8[11] = {1, 0, 0, 0, 3, 0, 0, 0, 0xE2, 0x82, 0xAC}; + shop_list_string euro; + assert(shop_list_string_decode(good_utf8, 11, &euro)); + assert(euro.len == 1 && euro.items[0].len == 3); + shop_list_string_free(&euro); + /* Error payloads decode through their own struct. */ shop_writer pw; memset(&pw, 0, sizeof pw); @@ -280,7 +295,7 @@ fn codecs_round_trip_in_c_and_cpp() { let tmp = tempfile::tempdir().expect("tempdir"); let dir = Utf8Path::from_path(tmp.path()).expect("utf8"); let api = load(SHOP, "shop"); - for (name, contents) in generate(&api, &CConfig::default(), dir) { + for (name, contents) in generate(&api, "") { std::fs::write(dir.join(name), contents).expect("write header"); } for (cc, ext, std) in compilers { @@ -320,3 +335,61 @@ fn codecs_round_trip_in_c_and_cpp() { ); } } + +/// Every snapshot fixture's headers compile cleanly as C11 and C++17, so a +/// declaration shape the kitchen-sink snapshot doesn't show (reserved +/// words, deep nesting, rich enums) can't produce an invalid header. +#[test] +fn fixture_headers_compile_in_c_and_cpp() { + let fixtures = [ + "kitchen_sink", + "shapes", + "nested_modules", + "docs_everywhere", + "edge_cases", + ]; + let compilers = [("cc", "c", "-std=c11"), ("c++", "cpp", "-std=c++17")]; + let tmp = tempfile::tempdir().expect("tempdir"); + let dir = Utf8Path::from_path(tmp.path()).expect("utf8"); + for stem in fixtures { + let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(format!("{stem}.yml")); + let yaml = std::fs::read_to_string(&path).expect("read fixture"); + let files = generate(&load(&yaml, stem), ""); + let include = files + .iter() + .map(|(n, _)| n.clone()) + .find(|n| n.ends_with("_buffer.h")) + .unwrap_or_else(|| format!("{stem}.h")); + for (name, contents) in &files { + std::fs::write(dir.join(name), contents).expect("write header"); + } + for (cc, ext, std) in compilers { + if !have(cc) { + continue; + } + let src = dir.join(format!("{stem}.{ext}")); + std::fs::write( + &src, + format!("#include \"{include}\"\nint main(void) {{ return 0; }}\n"), + ) + .expect("write source"); + let out = Command::new(cc) + .args([std, "-Wall", "-Wextra", "-Werror", "-pedantic"]) + .arg("-I") + .arg(dir.as_str()) + .arg("-c") + .arg(src.as_str()) + .arg("-o") + .arg(dir.join(format!("{stem}_{ext}.o")).as_str()) + .output() + .expect("run compiler"); + assert!( + out.status.success(), + "{cc} failed on {stem}:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + } + } +} diff --git a/crates/weaveffi-cli/tests/cli/check.rs b/crates/weaveffi-cli/tests/cli/check.rs index fc510b32..d84fe726 100644 --- a/crates/weaveffi-cli/tests/cli/check.rs +++ b/crates/weaveffi-cli/tests/cli/check.rs @@ -1,5 +1,5 @@ -//! Integration tests for the CI-oriented flags: `weaveffi diff --check` and -//! `weaveffi validate [--warn] --format json`. Each test runs the binary as a +//! Integration tests for the CI-oriented flags: `weaveffi generate --check` +//! and `weaveffi validate [--warn] --format json`. Each test runs the binary as a //! subprocess and either asserts on the structured stdout or on the process //! exit code. @@ -7,7 +7,7 @@ use std::io::Write; use std::path::Path; fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") + crate::weaveffi() } fn write_file(path: &Path, contents: &str) { @@ -26,7 +26,7 @@ fn calculator_crate() -> std::path::PathBuf { } #[test] -fn diff_check_passes_when_output_matches() { +fn check_passes_when_output_matches() { let tmp = tempfile::tempdir().unwrap(); let out = tmp.path().join("generated"); let input = calculator_crate(); @@ -43,40 +43,34 @@ fn diff_check_passes_when_output_matches() { let output = cargo_bin() .args([ - "diff", + "generate", input.to_str().unwrap(), "--out", out.to_str().unwrap(), "--check", ]) .output() - .expect("failed to run weaveffi diff --check"); + .expect("failed to run weaveffi generate --check"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), - "diff --check should exit 0 when output matches; stdout={stdout}, stderr={stderr}" - ); - assert!( - stdout.contains("+ 0 added, - 0 removed, ~ 0 modified"), - "expected zeroed summary, got: {stdout}" - ); - assert!( - !stdout.contains("---") && !stdout.contains("+++"), - "diff --check must not print per-file diff content, got: {stdout}" + "generate --check should exit 0 when output matches; stdout={stdout}, stderr={stderr}" ); + assert!(stdout.is_empty(), "nothing would change, got: {stdout}"); + assert!(stderr.contains("is up to date"), "{stderr}"); } #[test] -fn diff_check_fails_when_idl_changed() { +fn check_fails_when_idl_changed() { let tmp = tempfile::tempdir().unwrap(); let out = tmp.path().join("generated"); let idl = tmp.path().join("api.yml"); write_file( &idl, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: calc\n", " functions:\n", @@ -102,7 +96,7 @@ fn diff_check_fails_when_idl_changed() { write_file( &idl, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: calc\n", " functions:\n", @@ -123,34 +117,30 @@ fn diff_check_fails_when_idl_changed() { let output = cargo_bin() .args([ - "diff", + "generate", idl.to_str().unwrap(), "--out", out.to_str().unwrap(), "--check", + "--quiet", ]) .output() - .expect("failed to run weaveffi diff --check"); + .expect("failed to run weaveffi generate --check"); let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - !output.status.success(), - "diff --check should fail when IDL drifted; stdout={stdout}" - ); - let code = output.status.code().expect("expected an exit code"); - assert!( - code == 2 || code == 3, - "expected exit code 2 (modified) or 3 (added/removed), got {code}; stdout={stdout}" + assert_eq!( + output.status.code(), + Some(1), + "generate --check should exit 1 when the IDL drifted; stdout={stdout}" ); + assert!(stdout.lines().any(|l| l == "~ c/api.h"), "{stdout}"); assert!( - stdout.contains(" added, ") - && stdout.contains(" removed, ") - && stdout.contains(" modified"), - "expected diff summary line, got: {stdout}" + !stdout.contains("---") && !stdout.contains("+++"), + "generate --check must not print per-file diff content, got: {stdout}" ); assert!( - !stdout.contains("---") && !stdout.contains("+++"), - "diff --check must not print per-file diff content, got: {stdout}" + std::fs::read_to_string(out.join("c/api.h")).is_ok_and(|h| !h.contains("api_calc_sub")), + "generate --check must not write" ); } @@ -189,7 +179,7 @@ fn validate_warn_json_format_outputs_warnings_array() { write_file( &path, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: nodocs\n", " functions:\n", @@ -244,7 +234,7 @@ fn validate_json_failures_carry_the_code_and_fields() { write_file( &path, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: a\n", " structs: [{ name: Item, fields: [{ name: n, type: usize }] }]\n", diff --git a/crates/weaveffi-cli/tests/cli/determinism.rs b/crates/weaveffi-cli/tests/cli/determinism.rs index 4a396a3e..ae3e6a7a 100644 --- a/crates/weaveffi-cli/tests/cli/determinism.rs +++ b/crates/weaveffi-cli/tests/cli/determinism.rs @@ -1,35 +1,50 @@ -//! Cross-generator determinism guard: rendering the kitchen-sink fixture -//! twice must produce byte-identical files from every target. Catches +//! Cross-generator determinism guard: rendering each snapshot fixture twice +//! must produce byte-identical files from every target. Catches //! non-deterministic iteration (e.g. `HashMap` walks) before it can flake the -//! snapshot suite. +//! snapshot suite or `generate --check`. use std::path::Path; -use camino::Utf8Path; use weaveffi_model::model::Model; use weaveffi_model::parse::parse_api_str; use weaveffi_model::validate::validate; -fn load_kitchen_sink() -> Model { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/kitchen_sink.yml"); +const FIXTURES: [&str; 5] = [ + "kitchen_sink", + "shapes", + "nested_modules", + "docs_everywhere", + "edge_cases", +]; + +fn load(stem: &str) -> Model { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(format!("{stem}.yml")); let contents = std::fs::read_to_string(&path).expect("read fixture"); let api = parse_api_str(&contents, "yaml").expect("parse fixture"); - validate( - &api, - &weaveffi_model::pkg::Identity::named("kitchen_sink"), - None, - ) - .expect("validate fixture") + validate(&api, &weaveffi_model::pkg::Identity::named(stem), None).expect("validate fixture") } #[test] fn generator_output_is_byte_identical_across_runs() { - let api = load_kitchen_sink(); - let out = Utf8Path::new("out"); - for target in weaveffi_cli::targets::all_default() { - let a = target.render(&api, out); - let b = target.render(&api, out); - assert!(!a.is_empty(), "{} produced no files", target.name()); - assert_eq!(a, b, "{} rendered differently across runs", target.name()); + for stem in FIXTURES { + let model = load(stem); + for desc in weaveffi_cli::targets::REGISTRY { + let target = desc.build_default(); + let a = target.render(&model); + let b = target.render(&model); + assert!( + !a.is_empty(), + "{} produced no files for {stem}", + target.name() + ); + assert_eq!( + a, + b, + "{} rendered {stem} differently across runs", + target.name() + ); + } } } diff --git a/crates/weaveffi-cli/tests/cli/diagnostics.rs b/crates/weaveffi-cli/tests/cli/diagnostics.rs index 8b819400..dc820b79 100644 --- a/crates/weaveffi-cli/tests/cli/diagnostics.rs +++ b/crates/weaveffi-cli/tests/cli/diagnostics.rs @@ -7,7 +7,7 @@ use std::io::Write; fn cargo_bin() -> assert_cmd::Command { - let mut cmd = assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found"); + let mut cmd = crate::weaveffi(); cmd.env("NO_COLOR", "1"); cmd } @@ -25,7 +25,7 @@ fn write_temp_file(dir: &tempfile::TempDir, name: &str, contents: &str) -> std:: #[test] fn parse_yaml_error_includes_line() { let dir = tempfile::tempdir().unwrap(); - let yaml = "version: \"0.11.0\"\nmodules:\n - name: [bad\n"; + let yaml = "version: \"0.12.0\"\nmodules:\n - name: [bad\n"; let path = write_temp_file(&dir, "broken.yml", yaml); let output = cargo_bin() @@ -56,7 +56,7 @@ fn parse_yaml_error_includes_line() { #[test] fn validate_unknown_typeref_includes_offending_name() { let dir = tempfile::tempdir().unwrap(); - let yaml = r#"version: "0.11.0" + let yaml = r#"version: "0.12.0" modules: - name: m functions: @@ -94,7 +94,7 @@ modules: #[test] fn validate_duplicate_module_includes_offending_name() { let dir = tempfile::tempdir().unwrap(); - let yaml = r#"version: "0.11.0" + let yaml = r#"version: "0.12.0" modules: - name: alpha functions: diff --git a/crates/weaveffi-cli/tests/cli/diff.rs b/crates/weaveffi-cli/tests/cli/diff.rs index 86dd4fb4..1d91d132 100644 --- a/crates/weaveffi-cli/tests/cli/diff.rs +++ b/crates/weaveffi-cli/tests/cli/diff.rs @@ -1,109 +1,109 @@ +//! `weaveffi generate --diff`: the unified diff of what regenerating would +//! change, without writing anything. + use std::path::Path; -use predicates::prelude::*; +use crate::weaveffi; -#[test] -fn diff_against_empty_dir() { +fn sample(name: &str) -> std::path::PathBuf { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/calculator"); + repo_root.join("samples").join(name) +} +#[test] +fn diff_against_an_empty_dir_adds_every_file_and_writes_nothing() { let tmp = tempfile::tempdir().expect("failed to create temp dir"); let empty_out = tmp.path().join("empty"); std::fs::create_dir_all(&empty_out).unwrap(); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "diff", - input.to_str().unwrap(), - "--out", - empty_out.to_str().unwrap(), - ]) + let output = weaveffi() + .args(["generate", "--diff", "--target", "c"]) + .arg(sample("calculator")) + .arg("--out") + .arg(&empty_out) .output() - .expect("failed to run weaveffi diff"); + .expect("failed to run weaveffi generate --diff"); let stdout = String::from_utf8_lossy(&output.stdout); - assert!(output.status.success(), "diff command failed: {stdout}"); - assert!(!stdout.is_empty(), "diff output should not be empty"); - - for line in stdout.lines() { - assert!( - line.contains("[new file]"), - "expected every line to contain [new file], got: {line}" - ); - } + assert!(output.status.success(), "generate --diff failed: {stdout}"); + assert!( + stdout.contains("--- /dev/null\n+++ b/c/calculator.h\n"), + "{stdout}" + ); + assert!( + std::fs::read_dir(&empty_out).unwrap().next().is_none(), + "--diff must not write" + ); } #[test] -fn diff_no_changes() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/calculator"); - +fn diff_is_empty_when_nothing_changed() { let tmp = tempfile::tempdir().expect("failed to create temp dir"); let out_path = tmp.path().join("generated"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - ]) + weaveffi() + .arg("generate") + .arg(sample("calculator")) + .arg("-o") + .arg(&out_path) .assert() .success(); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "diff", - input.to_str().unwrap(), - "--out", - out_path.to_str().unwrap(), - ]) + let edited = out_path.join("c/calculator.h"); + let original = std::fs::read_to_string(&edited).unwrap(); + std::fs::write(&edited, format!("{original}// edited\n")).unwrap(); + let output = weaveffi() + .args(["generate", "--diff"]) + .arg(sample("calculator")) + .arg("--out") + .arg(&out_path) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success(), "{stdout}"); + assert!( + stdout.starts_with("--- a/c/calculator.h\n+++ b/c/calculator.h\n") + && stdout.contains("-// edited\n"), + "{stdout}" + ); + assert!(std::fs::read_to_string(&edited) + .unwrap() + .ends_with("// edited\n")); + + std::fs::write(&edited, original).unwrap(); + weaveffi() + .args(["generate", "--diff"]) + .arg(sample("calculator")) + .arg("--out") + .arg(&out_path) .assert() .success() - .stdout(predicate::str::contains("No differences found.")); + .stdout(""); } -/// Regression: `weaveffi diff` must apply the in-IDL `generators:` block -/// the same way `weaveffi generate` does, otherwise an IDL with custom -/// per-language naming (cpp namespace, dart package, dotnet assembly, -/// etc.) reports spurious diffs against its own freshly generated output. +/// `generate --check` applies the `[generators.*]` tables of the sample's +/// `weaveffi.toml` the same way `generate` does, so a freshly generated +/// tree with custom naming checks clean. #[test] -fn diff_check_honors_inline_generators() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/kvstore"); - +fn check_honors_generator_tables() { let tmp = tempfile::tempdir().expect("failed to create temp dir"); let out_path = tmp.path().join("generated"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - ]) + weaveffi() + .arg("generate") + .arg(sample("kvstore")) + .arg("-o") + .arg(&out_path) .assert() .success(); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "diff", - input.to_str().unwrap(), - "--out", - out_path.to_str().unwrap(), - "--check", - ]) + weaveffi() + .args(["generate", "--check"]) + .arg(sample("kvstore")) + .arg("--out") + .arg(&out_path) .assert() .success() - .stdout(predicate::str::contains( - "0 added, - 0 removed, ~ 0 modified", - )); + .stdout(""); } diff --git a/crates/weaveffi-cli/tests/cli/errors.rs b/crates/weaveffi-cli/tests/cli/errors.rs index 2373a27b..a28f9bf7 100644 --- a/crates/weaveffi-cli/tests/cli/errors.rs +++ b/crates/weaveffi-cli/tests/cli/errors.rs @@ -1,7 +1,7 @@ use std::io::Write; fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") + crate::weaveffi() } fn write_temp_file(dir: &tempfile::TempDir, name: &str, contents: &str) -> std::path::PathBuf { @@ -59,33 +59,11 @@ fn parse_error_json_shows_filename_and_location() { ); } -#[test] -fn parse_error_toml_shows_filename() { - let dir = tempfile::tempdir().unwrap(); - let path = write_temp_file(&dir, "bad.toml", "version = [invalid\n"); - - let output = cargo_bin() - .args(["validate", path.to_str().unwrap()]) - .output() - .unwrap(); - - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(!output.status.success()); - assert!( - stderr.contains("bad.toml"), - "expected filename in error output, got: {stderr}" - ); - assert!( - stderr.contains("TOML parse error"), - "expected TOML parse error message, got: {stderr}" - ); -} - #[test] fn validation_error_duplicate_module_shows_suggestion() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: foo functions: @@ -123,7 +101,7 @@ modules: fn async_function_validates_successfully() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: mymod functions: @@ -149,7 +127,7 @@ modules: fn validation_error_duplicate_function_shows_suggestion() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: mymod functions: @@ -209,7 +187,7 @@ fn generate_parse_error_shows_filename_and_suggestion() { fn generate_validation_error_shows_suggestion() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: dup functions: @@ -249,7 +227,7 @@ modules: fn validate_with_warnings() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: nodocs functions: diff --git a/crates/weaveffi-cli/tests/cli/extract.rs b/crates/weaveffi-cli/tests/cli/extract.rs index e230924c..985e02fd 100644 --- a/crates/weaveffi-cli/tests/cli/extract.rs +++ b/crates/weaveffi-cli/tests/cli/extract.rs @@ -5,14 +5,12 @@ use std::path::{Path, PathBuf}; +use crate::weaveffi; + fn fixture() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/producer") } -fn weaveffi() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") -} - fn stdout_of(output: &std::process::Output) -> String { assert!( output.status.success(), @@ -94,7 +92,7 @@ fn extract_formats_and_a_library_alone() { .output() .unwrap(), ); - let api: weaveffi_model::ir::Api = serde_yaml::from_str(&yaml).unwrap(); + let api: weaveffi_model::ir::Api = serde_saphyr::from_str(&yaml).unwrap(); let json = stdout_of( &weaveffi() .args(["-q", "extract", "-f", "json"]) @@ -106,17 +104,13 @@ fn extract_formats_and_a_library_alone() { serde_json::from_str::(&json).unwrap(), api ); - let toml_text = stdout_of( - &weaveffi() - .args(["-q", "extract", "-f", "toml"]) - .arg(fixture()) - .output() - .unwrap(), - ); - assert_eq!( - toml::from_str::(&toml_text).unwrap(), - api - ); + // An IDL is YAML or JSON; TOML is only for `weaveffi.toml`. + let toml = weaveffi() + .args(["-q", "extract", "-f", "toml"]) + .arg(fixture()) + .output() + .unwrap(); + assert!(!toml.status.success()); } /// What isn't a Rust producer's library is refused with a reason: an IDL, diff --git a/crates/weaveffi-cli/tests/cli/formats.rs b/crates/weaveffi-cli/tests/cli/formats.rs index 0f5296c9..a60ca749 100644 --- a/crates/weaveffi-cli/tests/cli/formats.rs +++ b/crates/weaveffi-cli/tests/cli/formats.rs @@ -1,4 +1,5 @@ -//! The JSON and TOML spellings of an IDL generate and validate like YAML. +//! The JSON spelling of an IDL generates and validates like YAML, and a TOML +//! IDL is rejected (TOML is only for `weaveffi.toml`). use weaveffi_model::ir::Api; @@ -6,15 +7,15 @@ use weaveffi_model::ir::Api; fn load_calculator_api() -> Api { weaveffi_model::parse::parse_api_str( concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: calculator\n", " errors:\n", - " name: CalcError\n", - " codes: [{ name: DivisionByZero, code: 1, message: division by zero }]\n", + " - name: CalcError\n", + " codes: [{ name: DivisionByZero, code: 1, message: division by zero }]\n", " functions:\n", " - { name: add, params: [{ name: a, type: i32 }, { name: b, type: i32 }], return: i32 }\n", - " - { name: divide, params: [{ name: a, type: i32 }, { name: b, type: i32 }], return: i32, throws: true }\n", + " - { name: divide, params: [{ name: a, type: i32 }, { name: b, type: i32 }], return: i32, throws: CalcError }\n", " - { name: greet, params: [{ name: name, type: string }], return: string }\n", ), "yaml", @@ -33,8 +34,7 @@ fn generate_from_json_input() { let out_dir = tempfile::tempdir().expect("failed to create output dir"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "generate", json_path.to_str().unwrap(), @@ -51,30 +51,23 @@ fn generate_from_json_input() { } #[test] -fn generate_from_toml_input() { - let api = load_calculator_api(); - let toml_str = toml::to_string_pretty(&api).expect("failed to serialize to TOML"); - +fn toml_idl_is_rejected() { let tmp = tempfile::tempdir().expect("failed to create temp dir"); let toml_path = tmp.path().join("calculator.toml"); - std::fs::write(&toml_path, &toml_str).expect("failed to write TOML file"); - - let out_dir = tempfile::tempdir().expect("failed to create output dir"); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - toml_path.to_str().unwrap(), - "-o", - out_dir.path().to_str().unwrap(), - ]) - .assert() - .success(); + std::fs::write(&toml_path, "version = \"0.12.0\"\nmodules = []\n") + .expect("failed to write TOML file"); + let output = crate::weaveffi() + .args(["validate", toml_path.to_str().unwrap()]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = crate::stderr(&output); assert!( - out_dir.path().join("c").exists(), - "c/ output directory should exist" + // miette may wrap anywhere in the message, so compare without spaces. + stderr.replace(' ', "").contains("calculator.toml(.toml)") + && stderr.replace(' ', "").contains("yml|yaml|json"), + "{stderr}" ); } @@ -87,8 +80,7 @@ fn validate_from_json() { let json_path = tmp.path().join("calculator.json"); std::fs::write(&json_path, &json).expect("failed to write JSON file"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args(["validate", json_path.to_str().unwrap()]) .assert() .success() diff --git a/crates/weaveffi-cli/tests/cli/generate.rs b/crates/weaveffi-cli/tests/cli/generate.rs index 60dfc6a7..ec465f6b 100644 --- a/crates/weaveffi-cli/tests/cli/generate.rs +++ b/crates/weaveffi-cli/tests/cli/generate.rs @@ -10,8 +10,7 @@ fn generate_produces_expected_files() { let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "generate", input.to_str().unwrap(), @@ -48,8 +47,7 @@ fn generate_with_target_filter() { let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "generate", input.to_str().unwrap(), @@ -80,8 +78,7 @@ fn generate_cpp_target_filter() { let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "generate", input.to_str().unwrap(), @@ -109,8 +106,7 @@ fn validate_command_succeeds() { let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let input = repo_root.join("samples/calculator"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args(["validate", input.to_str().unwrap()]) .assert() .success() @@ -126,8 +122,7 @@ fn quiet_flag_suppresses_output() { let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "--quiet", "generate", @@ -156,8 +151,7 @@ fn dev_bundles_the_debug_library_into_the_python_package() { let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + let output = crate::weaveffi() .args(["dev", input.to_str().unwrap(), "-o"]) .arg(out_path) .args(["--target", "c,python,dotnet"]) diff --git a/crates/weaveffi-cli/tests/cli/main.rs b/crates/weaveffi-cli/tests/cli/main.rs index 3c42ff62..6d1dc745 100644 --- a/crates/weaveffi-cli/tests/cli/main.rs +++ b/crates/weaveffi-cli/tests/cli/main.rs @@ -13,8 +13,23 @@ mod generate; mod no_silent_stubs; mod package; mod project_config; +mod registry; mod schema; +/// The `weaveffi` binary, building Rust producers into a target directory of +/// this test binary's own (`CARGO_TARGET_DIR`), so every library a test +/// reads is one a `weaveffi` command built for it, never an artifact another +/// test binary (or `cargo test` itself, with other features) wrote or is +/// rewriting in the workspace's `target/debug`. +fn weaveffi() -> assert_cmd::Command { + let mut cmd = assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found"); + cmd.env( + "CARGO_TARGET_DIR", + std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli-producers"), + ); + cmd +} + /// A command's stderr with miette's wrapping undone: box-drawing gutters /// removed and whitespace runs collapsed, so a message matches as one line /// whatever width the report was wrapped to. diff --git a/crates/weaveffi-cli/tests/cli/no_silent_stubs.rs b/crates/weaveffi-cli/tests/cli/no_silent_stubs.rs index 8479ab3e..f191525f 100644 --- a/crates/weaveffi-cli/tests/cli/no_silent_stubs.rs +++ b/crates/weaveffi-cli/tests/cli/no_silent_stubs.rs @@ -106,9 +106,8 @@ fn generated_output_has_no_stub_markers() { // Case-insensitive marker list. Bare "not supported" is deliberately // absent: an explicit, permanently declared unsupported-feature surface - // (e.g. wasm async/listener stubs in Emscripten mode, which throw "is - // not supported in Emscripten mode") is the *correct* loud behavior. The - // "yet" in "not yet supported" is what marks a runtime-throwing TODO stub + // that throws when called is the *correct* loud behavior. The "yet" in + // "not yet supported" is what marks a runtime-throwing TODO stub // for a feature the target claims to implement, the Kotlin `iter` // regression this test exists to prevent, so it must stay a hard build // failure. @@ -127,8 +126,7 @@ fn generated_output_has_no_stub_markers() { let dir = tempfile::tempdir().expect("temp dir"); let out = dir.path().join("out"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "generate", idl.to_str().unwrap(), @@ -141,8 +139,8 @@ fn generated_output_has_no_stub_markers() { let mut files = Vec::new(); walk_files(&out, &mut files); assert!( - files.len() > 10, - "expected many generated files for {}, got {}", + files.len() >= weaveffi_cli::targets::REGISTRY.len(), + "expected a file per target for {}, got {}", idl.display(), files.len() ); diff --git a/crates/weaveffi-cli/tests/cli/package.rs b/crates/weaveffi-cli/tests/cli/package.rs index 1a96ba0f..bc920c3e 100644 --- a/crates/weaveffi-cli/tests/cli/package.rs +++ b/crates/weaveffi-cli/tests/cli/package.rs @@ -5,6 +5,8 @@ use std::io::Read; use std::path::{Path, PathBuf}; +use crate::weaveffi; + fn repo_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() @@ -14,9 +16,25 @@ fn repo_root() -> PathBuf { .to_path_buf() } +/// A 64-bit little-endian ELF shared-object header with no sections: a +/// Linux library that needs no glibc version beyond the 2.17 floor, which +/// is what the wheel's manylinux tag is read from. +fn minimal_elf() -> Vec { + let mut elf = vec![0u8; 64]; + elf[..4].copy_from_slice(b"\x7fELF"); + elf[4] = 2; // ELFCLASS64 + elf[5] = 1; // ELFDATA2LSB + elf[6] = 1; // EV_CURRENT + elf[16..18].copy_from_slice(&3u16.to_le_bytes()); // ET_DYN + elf[18..20].copy_from_slice(&62u16.to_le_bytes()); // EM_X86_64 + elf[20..24].copy_from_slice(&1u32.to_le_bytes()); // e_version + elf[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize + elf +} + /// Lay out a fake `--binaries` tree (`//`) covering the /// desktop matrix, with placeholder library bytes so packaging has something -/// to copy. +/// to copy (a bare ELF header on Linux). fn write_prebuilt(root: &Path, lib_base: &str) { let entries = [ ("darwin-arm64", format!("lib{lib_base}.dylib")), @@ -28,7 +46,12 @@ fn write_prebuilt(root: &Path, lib_base: &str) { for (platform, lib) in entries { let dir = root.join(platform); std::fs::create_dir_all(&dir).expect("create platform dir"); - std::fs::write(dir.join(&lib), b"\x00fake-native\x01").expect("write fake lib"); + let bytes = if platform.starts_with("linux") { + minimal_elf() + } else { + b"\x00fake-native\x01".to_vec() + }; + std::fs::write(dir.join(&lib), bytes).expect("write fake lib"); } std::fs::write( root.join("darwin-arm64") @@ -45,7 +68,7 @@ fn calculator_idl(dir: &Path) -> PathBuf { std::fs::write( &idl, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: calculator\n", " functions:\n", @@ -61,10 +84,6 @@ fn calculator_idl(dir: &Path) -> PathBuf { idl } -fn weaveffi() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") -} - /// The `(path, bytes)` entries of a gzipped tarball. fn untar_gz(path: &Path) -> Vec<(String, Vec)> { let file = std::fs::File::open(path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); @@ -91,15 +110,36 @@ fn package_writes_installable_artifacts_from_binaries() { let out = tempfile::tempdir().expect("temp out dir"); let dist = out.path(); + let targets = ["python", "node", "ruby", "c", "go"]; weaveffi() .args(["package", input.to_str().unwrap(), "--binaries"]) .arg(bins.path()) - .args(["--target", "python,node,ruby,c,go", "-o"]) + .args(["--target", &targets.join(","), "-o"]) .arg(dist) .assert() .success(); - // Python: one platform-tagged wheel per desktop platform. + python_wheels(dist); + node_tarballs(dist); + ruby_gems(dist); + + // C: headers and every platform's library in one tarball. + let c = untar_gz(&dist.join("c/calculator-1.0.0-c.tar.gz")); + let names: Vec<&str> = c.iter().map(|(n, _)| n.as_str()).collect(); + assert!( + names.contains(&"calculator-1.0.0/include/calculator.h"), + "{names:?}" + ); + assert!( + names.contains(&"calculator-1.0.0/lib/windows-x64/calculator.dll"), + "{names:?}" + ); + + go_module(dist); +} + +/// Python: one platform-tagged wheel per desktop platform. +fn python_wheels(dist: &Path) { for wheel in [ "calculator-1.0.0-py3-none-macosx_11_0_arm64.whl", "calculator-1.0.0-py3-none-macosx_11_0_x86_64.whl", @@ -109,9 +149,11 @@ fn package_writes_installable_artifacts_from_binaries() { ] { assert!(dist.join("python").join(wheel).is_file(), "missing {wheel}"); } +} - // Node: per-platform tarballs with the library (and the prebuilt addon - // where one was built), plus the main package. +/// Node: per-platform tarballs with the library (and the prebuilt addon +/// where one was built), plus the main package. +fn node_tarballs(dist: &Path) { let mac = untar_gz(&dist.join("node/calculator-darwin-arm64-1.0.0.tgz")); let names: Vec<&str> = mac.iter().map(|(n, _)| n.as_str()).collect(); assert_eq!( @@ -134,8 +176,10 @@ fn package_writes_installable_artifacts_from_binaries() { package_json.contains("\"calculator-darwin-arm64\": \"1.0.0\""), "{package_json}" ); +} - // Ruby: one precompiled gem per desktop platform. +/// Ruby: one precompiled gem per desktop platform. +fn ruby_gems(dist: &Path) { for gem in [ "calculator-1.0.0-arm64-darwin.gem", "calculator-1.0.0-x86_64-linux.gem", @@ -143,20 +187,10 @@ fn package_writes_installable_artifacts_from_binaries() { ] { assert!(dist.join("ruby").join(gem).is_file(), "missing {gem}"); } +} - // C: headers and every platform's library in one tarball. - let c = untar_gz(&dist.join("c/calculator-1.0.0-c.tar.gz")); - let names: Vec<&str> = c.iter().map(|(n, _)| n.as_str()).collect(); - assert!( - names.contains(&"calculator-1.0.0/include/calculator.h"), - "{names:?}" - ); - assert!( - names.contains(&"calculator-1.0.0/lib/windows-x64/calculator.dll"), - "{names:?}" - ); - - // Go: the module directory with per-platform libraries and cgo flags. +/// Go: the module directory with per-platform libraries and cgo flags. +fn go_module(dist: &Path) { let bindings = std::fs::read_to_string(dist.join("go/calculator/bindings.go")).unwrap(); assert!( bindings.contains("#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linux-x64"), @@ -201,7 +235,7 @@ fn package_names_the_missing_platform_library() { let output = weaveffi() .args(["package", input.to_str().unwrap(), "--binaries"]) .arg(bins.path()) - .args(["--platforms", "android-arm64", "--target", "kotlin"]) + .args(["--platforms", "android-arm64", "--target", "c"]) .output() .unwrap(); assert!(!output.status.success()); @@ -219,7 +253,7 @@ fn build_without_a_cargo_toml_explains_the_alternatives() { let idl = dir.path().join("greeter.yml"); std::fs::write( &idl, - "version: \"0.11.0\"\nmodules:\n - name: greeter\n functions:\n - { name: hi, params: [], return: i32 }\n", + "version: \"0.12.0\"\nmodules:\n - name: greeter\n functions:\n - { name: hi, params: [], return: i32 }\n", ) .unwrap(); for command in ["build", "package"] { @@ -263,5 +297,9 @@ fn build_rejects_unknown_and_foreign_platforms_before_compiling() { assert!(!output.status.success()); let err = crate::stderr(&output); assert!(err.contains("can only be built on a"), "{err}"); + assert!( + err.contains("none of the requested platforms can be built"), + "{err}" + ); assert!(!err.contains("Compiling"), "checks run before cargo: {err}"); } diff --git a/crates/weaveffi-cli/tests/cli/project_config.rs b/crates/weaveffi-cli/tests/cli/project_config.rs index a000130a..e52ad59d 100644 --- a/crates/weaveffi-cli/tests/cli/project_config.rs +++ b/crates/weaveffi-cli/tests/cli/project_config.rs @@ -1,7 +1,7 @@ //! `weaveffi.toml` behavior end to end: automatic discovery next to (or //! above) the input, `--config` overriding discovery, `[package]` identity //! reaching every manifest, `[generators.]` options reaching their -//! backend, and the identity's C prefix reaching all eleven targets. +//! backend, and the identity's C prefix reaching every target. use std::fs; use std::path::{Path, PathBuf}; @@ -38,8 +38,7 @@ fn read(path: &Path) -> String { } fn generate(input: &Path, out: &Path, extra: &[&str]) { - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + crate::weaveffi() .args([ "generate", input.to_str().unwrap(), @@ -51,9 +50,10 @@ fn generate(input: &Path, out: &Path, extra: &[&str]) { .success(); } -const ALL_TARGETS: [&str; 11] = [ - "c", "cpp", "swift", "kotlin", "node", "wasm", "python", "dotnet", "dart", "go", "ruby", -]; +/// Every registered target. +fn all_targets() -> Vec<&'static str> { + weaveffi_cli::targets::names().collect() +} /// The kvstore sample's `weaveffi.toml` sits beside its `Cargo.toml`; /// generating from the crate must pick it up without `--config` and honor @@ -65,13 +65,14 @@ fn discovers_weaveffi_toml_above_the_input() { let out = dir.path(); generate(&input, out, &[]); - for target in ALL_TARGETS { + for target in all_targets() { assert!( out.join(target).is_dir(), "missing target directory: {target}" ); } + assert!(read(&out.join("c/kvstore.h")).contains("#define KVSTORE_ABI_VERSION")); assert!(read(&out.join("cpp/kvstore.hpp")).contains("namespace kvstore")); assert!(read(&out.join("dotnet/Kvstore.cs")).contains("namespace Kvstore")); assert!(out.join("swift/Sources/Kvstore/Kvstore.swift").is_file()); @@ -139,8 +140,7 @@ fn explicit_config_overrides_discovery() { #[test] fn rust_source_input_points_at_the_crate() { let input = repo_root().join("samples/calculator/src/lib.rs"); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + let output = crate::weaveffi() .args(["generate", input.to_str().unwrap()]) .output() .unwrap(); @@ -159,7 +159,7 @@ fn inline_package_block_in_idl_is_an_error() { fs::write( &idl, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "package:\n", " name: legacy\n", "modules:\n", @@ -168,8 +168,7 @@ fn inline_package_block_in_idl_is_an_error() { ), ) .unwrap(); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + let output = crate::weaveffi() .args(["validate", idl.to_str().unwrap()]) .output() .unwrap(); @@ -186,8 +185,7 @@ fn unknown_config_table_is_an_error() { let dir = tempfile::tempdir().unwrap(); let cfg = dir.path().join("cfg.toml"); fs::write(&cfg, "[swift]\nmodule_name = \"Old\"\n").unwrap(); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + let output = crate::weaveffi() .args([ "generate", input.to_str().unwrap(), @@ -209,8 +207,7 @@ fn unknown_config_table_is_an_error() { fn unknown_target_is_an_error() { let input = repo_root().join("samples/calculator"); let dir = tempfile::tempdir().unwrap(); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + let output = crate::weaveffi() .args([ "generate", input.to_str().unwrap(), @@ -237,7 +234,7 @@ fn idl_c_prefix_reaches_every_target() { fs::write( &idl, concat!( - "version: \"0.11.0\"\n", + "version: \"0.12.0\"\n", "modules:\n", " - name: calculator\n", " functions:\n", @@ -254,7 +251,7 @@ fn idl_c_prefix_reaches_every_target() { let out = dir.path().join("out"); generate(&idl, &out, &[]); - for target in ALL_TARGETS { + for target in all_targets() { let tree = read_tree(&out.join(target)); assert!( tree.contains("myffi_calculator_add"), @@ -276,8 +273,7 @@ fn rust_producer_rejects_c_prefix() { let dir = tempfile::tempdir().unwrap(); let cfg = dir.path().join("cfg.toml"); fs::write(&cfg, "[package]\nc_prefix = \"myffi\"\n").unwrap(); - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") + let output = crate::weaveffi() .args([ "generate", input.to_str().unwrap(), diff --git a/crates/weaveffi-cli/tests/cli/registry.rs b/crates/weaveffi-cli/tests/cli/registry.rs new file mode 100644 index 00000000..27e874b7 --- /dev/null +++ b/crates/weaveffi-cli/tests/cli/registry.rs @@ -0,0 +1,103 @@ +//! The target registry is the one list of targets: every place outside the +//! crate that enumerates them (the CI matrices, the fixture compile check, +//! and the conformance driver) must list exactly the registered targets. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +use weaveffi_cli::targets; + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn registry() -> BTreeSet { + targets::names().map(str::to_string).collect() +} + +/// The values of the `target` and `lang` matrix keys of one job, from a +/// plain list or from `include` entries. +fn matrix_values(matrix: &serde_json::Value) -> BTreeSet { + let mut values = BTreeSet::new(); + for key in ["target", "lang"] { + if let Some(list) = matrix.get(key).and_then(|v| v.as_array()) { + values.extend(list.iter().filter_map(|v| v.as_str()).map(str::to_string)); + } + if let Some(include) = matrix.get("include").and_then(|v| v.as_array()) { + values.extend( + include + .iter() + .filter_map(|entry| entry.get(key)?.as_str()) + .map(str::to_string), + ); + } + } + values +} + +/// Every job matrix that lists language targets (any value is a registered +/// target name; fuzz and release-triple matrices aren't) lists them all. +#[test] +fn workflow_matrices_list_every_target() { + let registry = registry(); + let mut checked = 0; + let dir = repo_root().join(".github/workflows"); + for entry in std::fs::read_dir(&dir).unwrap() { + let path = entry.unwrap().path(); + if path.extension().and_then(|e| e.to_str()) != Some("yml") { + continue; + } + let text = std::fs::read_to_string(&path).unwrap(); + let doc: serde_json::Value = serde_saphyr::from_str(&text).unwrap(); + let Some(jobs) = doc.get("jobs").and_then(|j| j.as_object()) else { + continue; + }; + for (name, job) in jobs { + let Some(matrix) = job.get("strategy").and_then(|s| s.get("matrix")) else { + continue; + }; + let values = matrix_values(matrix); + if values.iter().any(|v| registry.contains(v)) { + assert_eq!( + values, + registry, + "the {:?} matrix in {} must list exactly the registered targets", + name, + path.display() + ); + checked += 1; + } + } + } + assert!( + checked >= 2, + "expected the fixtures and conformance matrices" + ); +} + +/// The words of the shell assignment starting with `prefix` in `script`, +/// up to the closing `quote`. +fn shell_list(script: &str, prefix: &str, quote: char) -> BTreeSet { + let path = repo_root().join(script); + let text = std::fs::read_to_string(&path).unwrap(); + let line = text + .lines() + .find_map(|l| l.trim().strip_prefix(prefix)) + .unwrap_or_else(|| panic!("no `{prefix}` line in {script}")); + let end = line.find(quote).unwrap_or(line.len()); + line[..end].split_whitespace().map(str::to_string).collect() +} + +#[test] +fn scripts_list_every_target() { + assert_eq!( + shell_list("scripts/check-fixtures.sh", "TARGETS=${*:-", '}'), + registry(), + "scripts/check-fixtures.sh's default TARGETS" + ); + assert_eq!( + shell_list("conformance/run.sh", "LANGS=\"", '"'), + registry(), + "conformance/run.sh's LANGS" + ); +} diff --git a/crates/weaveffi-cli/tests/cli/schema.rs b/crates/weaveffi-cli/tests/cli/schema.rs index 1b8e9c69..71a264c7 100644 --- a/crates/weaveffi-cli/tests/cli/schema.rs +++ b/crates/weaveffi-cli/tests/cli/schema.rs @@ -1,5 +1,5 @@ fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") + crate::weaveffi() } #[test] diff --git a/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml b/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml index dd079ce4..337cd54b 100644 --- a/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml +++ b/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" modules: - name: docs doc: | @@ -37,10 +37,10 @@ modules: - { name: doc_id, type: i64, doc: Identifier of the saved document } - { name: revision, type: i32, doc: Monotonically increasing revision } errors: - name: DocsErrors - codes: - - { name: not_found, code: 1, message: "Document not found", doc: Returned when no document matches the requested id } - - { name: invalid_status, code: 2, message: "Invalid status transition", doc: Raised when attempting to move into a forbidden state } + - name: DocsErrors + codes: + - { name: not_found, code: 1, message: "Document not found", doc: Returned when no document matches the requested id } + - { name: invalid_status, code: 2, message: "Invalid status transition", doc: Raised when attempting to move into a forbidden state } interfaces: - name: Notebook doc: | @@ -62,7 +62,7 @@ modules: params: - { name: id, type: i64, doc: Identifier of the document to find } return: Document - throws: true + throws: DocsErrors - name: size doc: The number of documents currently held params: [] diff --git a/crates/weaveffi-cli/tests/fixtures/edge_cases.yml b/crates/weaveffi-cli/tests/fixtures/edge_cases.yml index 87505855..c09f9553 100644 --- a/crates/weaveffi-cli/tests/fixtures/edge_cases.yml +++ b/crates/weaveffi-cli/tests/fixtures/edge_cases.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" # Shapes the mainstream fixtures do not reach: identifiers that are reserved # words in one or more target languages, interface members spelled like the # members object wrappers declare or inherit (close, dispose, handle, @@ -94,7 +94,7 @@ modules: - { name: lambda, type: i32 } - { name: yield, type: i32 } - { name: nil, type: i32 } - - { name: null, type: i32 } + - { name: "null", type: i32 } - { name: begin, type: i32 } - { name: rescue, type: i32 } - { name: module, type: i32 } @@ -157,9 +157,9 @@ modules: params: [] return: i32 errors: - name: EdgeErrors - codes: - - { name: Busy, code: 7, message: "Resource is busy" } + - name: EdgeErrors + codes: + - { name: Busy, code: 7, message: "Resource is busy" } interfaces: - name: Widget doc: Interface with no constructor; instances come from make_widget @@ -167,7 +167,7 @@ modules: - name: refresh doc: Void method that can fail params: [] - throws: true + throws: EdgeErrors - name: rename params: - { name: class, type: string } @@ -179,7 +179,7 @@ modules: - { name: id, type: i64 } return: Outer async: true - throws: true + throws: EdgeErrors - name: old_name deprecated: "Use rename instead" params: [] @@ -200,7 +200,7 @@ modules: doc: Fails when the vault is busy params: - { name: path, type: string } - throws: true + throws: EdgeErrors methods: - name: peek params: [] @@ -214,7 +214,7 @@ modules: doc: Throwing static params: [] return: bool - throws: true + throws: EdgeErrors - name: to_string doc: Static spelled like an inherited object member params: [] @@ -268,7 +268,7 @@ modules: params: [] return: Widget async: true - throws: true + throws: EdgeErrors - name: make_widget doc: Factory for an interface with no constructor params: @@ -279,7 +279,7 @@ modules: params: - { name: id, type: i64 } return: "Widget?" - throws: true + throws: EdgeErrors - name: inspect_widget doc: Interface parameter, including an optional one params: @@ -311,13 +311,13 @@ modules: params: [] return: Outer async: true - throws: true + throws: EdgeErrors - name: async_list params: [] return: "[Outer]" async: true cancellable: true - throws: true + throws: EdgeErrors - name: async_optional params: [] return: "i64?" diff --git a/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml b/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml index 6ff3b8da..ae9dd970 100644 --- a/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml +++ b/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" modules: - name: shared structs: @@ -56,7 +56,7 @@ modules: doc: A display label, failing with a kitchen error params: [] return: string - throws: true + throws: KitchenErrors - name: latest doc: The most recent item the listener kept, if any params: [] @@ -65,11 +65,35 @@ modules: doc: The listener's favorite gadget params: [] return: Gadget + - name: Sampler + doc: Consumer-implemented source of scalar samples + methods: + - name: threshold + doc: The cutoff for a level, if the sampler has one + params: + - { name: level, type: "u32?" } + return: "f64?" + - name: samples + doc: Up to `limit` samples, failing when the pantry is empty + params: + - { name: limit, type: "u32?" } + - { name: weights, type: "[f64]" } + return: "[i16]" + throws: PantryError + - name: describe + doc: A free-form description, failing with an untyped error + params: [] + return: string + throws: any errors: - name: KitchenErrors - codes: - - { name: NotFound, code: 1, message: "Item not found" } - - { name: InvalidInput, code: 2, message: "Invalid input" } + - name: KitchenErrors + codes: + - { name: NotFound, code: 1, message: "Item not found" } + - { name: InvalidInput, code: 2, message: "Invalid input" } + - name: PantryError + codes: + - { name: OutOfStock, code: 1, message: "Out of stock", fields: [{ name: item, type: string }] } + - { name: Spoiled, code: 2, message: "Spoiled" } interfaces: - name: Gadget doc: A pocket-sized interface exercising the object surface @@ -88,7 +112,13 @@ modules: params: - { name: times, type: i32 } return: i32 - throws: true + throws: KitchenErrors + - name: readings + doc: The gadget's sensor readings, optionally limited + params: + - { name: limit, type: "u32?" } + return: "[f64]" + throws: any statics: - name: version doc: The gadget subsystem version string @@ -152,7 +182,7 @@ modules: params: - { name: id, type: i64 } return: "Item?" - throws: true + throws: KitchenErrors - name: list_items doc: List of structs params: [] @@ -186,11 +216,97 @@ modules: doc: Legacy operation kept for compatibility params: [] return: i32 - deprecated: "Use new_op instead" + deprecated: "Use `new_op` instead" - name: new_op - doc: Replacement for legacy_op + doc: Replacement for `legacy_op` + params: [] + return: i32 + - name: opt_i32_id + doc: Optional scalar in both directions + params: + - { name: x, type: "i32?" } + return: "i32?" + - name: opt_bool_id + params: + - { name: x, type: "bool?" } + return: "bool?" + - name: opt_priority + doc: Optional C-style enum in both directions + params: + - { name: p, type: "Priority?" } + return: "Priority?" + - name: opt_mixed + doc: Optional scalars next to ordinary parameters + params: + - { name: base, type: f64 } + - { name: scale, type: "f32?" } + - { name: offset, type: "i64?" } + - { name: label, type: "string?" } + return: "u16?" + - name: sum_f64 + doc: A typed-array parameter + params: + - { name: xs, type: "[f64]" } + return: f64 + - name: scale_i32 + doc: Typed arrays in both directions + params: + - { name: xs, type: "[i32]" } + - { name: factor, type: i32 } + return: "[i32]" + - name: ids_u64 + params: + - { name: ids, type: "[u64]" } + return: "[u64]" + - name: mixed_slices + doc: Every slice element type + params: + - { name: a, type: "[i8]" } + - { name: b, type: "[i16]" } + - { name: c, type: "[u16]" } + - { name: d, type: "[u32]" } + - { name: e, type: "[i64]" } + - { name: f, type: "[f32]" } + return: "[u32]" + - name: async_maybe_count + doc: Async returning an optional scalar + params: [] + return: "i64?" + async: true + - name: async_samples + doc: Async returning a typed array + params: + - { name: n, type: u32 } + return: "[f32]" + async: true + cancellable: true + - name: stream_maybe + doc: Iterator of optional scalars params: [] + return: "iter" + - name: stream_chunks + doc: Iterator of typed arrays + params: [] + return: "iter<[i32]>" + - name: sample_with + doc: Drive a consumer-implemented sampler + params: + - { name: sampler, type: Sampler } + - { name: level, type: "u32?" } + return: "[i16]" + throws: PantryError + - name: take_from_pantry + doc: Fails with the second domain + params: + - { name: item, type: string } return: i32 + throws: PantryError + - name: parse_any + doc: Fails with an untyped error + params: + - { name: text, type: string } + return: i64 + throws: any modules: - name: nested functions: diff --git a/crates/weaveffi-cli/tests/fixtures/nested_modules.yml b/crates/weaveffi-cli/tests/fixtures/nested_modules.yml index 136e3791..2aa30bfb 100644 --- a/crates/weaveffi-cli/tests/fixtures/nested_modules.yml +++ b/crates/weaveffi-cli/tests/fixtures/nested_modules.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" # Exercises multi-level nested modules and type references that cross nesting # boundaries in every direction: # * parent -> nested child (graphics.Canvas.root : shapes.Shape) @@ -10,13 +10,14 @@ version: "0.11.0" # names each type after its declaring module's underscore-joined path # (e.g. Detail in graphics.shapes.geometry -> {prefix}_graphics_shapes_geometry_Detail). # The root module also declares an error domain that geometry.compute (two -# levels down) relies on through domain inheritance for its `throws: true`. +# levels down) throws by name (`throws: GraphicsError`; domain names are +# global). modules: - name: graphics errors: - name: GraphicsError - codes: - - { name: OutOfBounds, code: 1, message: "coordinate out of bounds" } + - name: GraphicsError + codes: + - { name: OutOfBounds, code: 1, message: "coordinate out of bounds" } enums: - name: Unit doc: A measurement unit shared with nested submodules. @@ -64,8 +65,8 @@ modules: functions: - name: compute doc: Deeper function taking an ancestor-module struct. Throws - through the root module's inherited GraphicsError domain. + the root module's GraphicsError domain. params: - { name: shape, type: Shape } return: Detail - throws: true + throws: GraphicsError diff --git a/crates/weaveffi-cli/tests/fixtures/producer/Cargo.toml b/crates/weaveffi-cli/tests/fixtures/producer/Cargo.toml index 05c44ae4..916a71e0 100644 --- a/crates/weaveffi-cli/tests/fixtures/producer/Cargo.toml +++ b/crates/weaveffi-cli/tests/fixtures/producer/Cargo.toml @@ -1,14 +1,12 @@ # A small Rust producer for the CLI's library-mode tests: `weaveffi extract` -# and `weaveffi generate` read its API from the library it builds. +# and `weaveffi generate` read its API from the library it builds. It +# declares no `crate-type`: WeaveFFI builds the `cdylib` itself. [package] name = "producer-fixture" version = "0.3.0" edition.workspace = true publish = false -[lib] -crate-type = ["cdylib", "rlib"] - [features] # Compiles in the declarations marked `#[cfg(feature = "extra")]`. extra = [] diff --git a/crates/weaveffi-cli/tests/fixtures/producer/expected.yml b/crates/weaveffi-cli/tests/fixtures/producer/expected.yml index 36c7d6f9..e626d249 100644 --- a/crates/weaveffi-cli/tests/fixtures/producer/expected.yml +++ b/crates/weaveffi-cli/tests/fixtures/producer/expected.yml @@ -1,4 +1,4 @@ -version: 0.11.0 +version: "0.12.0" modules: - name: receipt doc: Receipts, in a sibling tree that uses the shop's record. @@ -26,7 +26,7 @@ modules: doc: |- Buy one `name`. Fails with `ShopError::SoldOut` for anything but `"tea"`. - throws: true + throws: ShopError - name: list_price return: u64 doc: The list price of everything. @@ -55,7 +55,8 @@ modules: params: - name: item type: Item - doc: '`item` was added.' + doc: "`item` was added." + throws: any structs: - name: Item doc: Something for sale. @@ -80,7 +81,7 @@ modules: value: 1 doc: Large. errors: - name: ShopError + - name: ShopError codes: - name: SoldOut code: 1 diff --git a/crates/weaveffi-cli/tests/fixtures/producer/src/lib.rs b/crates/weaveffi-cli/tests/fixtures/producer/src/lib.rs index 6b3951d3..d989b09d 100644 --- a/crates/weaveffi-cli/tests/fixtures/producer/src/lib.rs +++ b/crates/weaveffi-cli/tests/fixtures/producer/src/lib.rs @@ -20,20 +20,13 @@ pub mod shop { #[repr(i32)] pub enum ShopError { /// sold out + #[weaveffi(message = "{item} is sold out")] SoldOut { /// The item that ran out. item: String, } = 1, } - impl std::fmt::Display for ShopError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::SoldOut { item } => write!(f, "{item} is sold out"), - } - } - } - /// A price in cents. pub type Price = u64; diff --git a/crates/weaveffi-cli/tests/fixtures/shapes.yml b/crates/weaveffi-cli/tests/fixtures/shapes.yml index f2b3e653..d96d21b3 100644 --- a/crates/weaveffi-cli/tests/fixtures/shapes.yml +++ b/crates/weaveffi-cli/tests/fixtures/shapes.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" modules: - name: shapes doc: Rich (algebraic) enums and the expanded numeric primitives. diff --git a/crates/weaveffi-cli/tests/snapshots.rs b/crates/weaveffi-cli/tests/snapshots.rs index 317f04d6..08f5ba08 100644 --- a/crates/weaveffi-cli/tests/snapshots.rs +++ b/crates/weaveffi-cli/tests/snapshots.rs @@ -1,50 +1,36 @@ -//! Snapshot tests covering every generator against a small, feature-complete -//! IDL corpus. +//! Snapshot tests covering every generator. //! //! Five fixtures cover the whole IDL surface between them: `kitchen_sink` -//! (every scalar and composite type, an interface with objects in optional, -//! list, iterator, and record positions, a callback interface, an error -//! domain, iterators, async and cancellable functions, deprecation, and a -//! nested submodule), `shapes` (rich enums and the full numeric primitive -//! set), `nested_modules` (a three-deep module tree with cross-module -//! references), `docs_everywhere` (doc comments on every declaration kind), -//! and `edge_cases` (identifiers that are reserved words in some target, -//! deeply nested composites, optional parameters, interfaces and callback -//! interfaces in every legal position, async functions with non-string -//! results, type-level deprecation, and scalar and string iterators). One -//! test per generator renders all five fixtures and checks each file, in -//! sorted order, by one of three rules: +//! (every scalar and composite type, OptDirect and Slice values in every +//! position, an interface with objects in optional, list, iterator, and +//! record positions, two callback interfaces, two error domains, typed and +//! untyped `throws`, iterators, async and cancellable functions, +//! deprecation, and a nested submodule), `shapes` (rich enums and the full +//! numeric primitive set), `nested_modules` (a three-deep module tree with +//! cross-module references), `docs_everywhere` (doc comments on every +//! declaration kind), and `edge_cases` (identifiers that are reserved words +//! in some target, deeply nested composites, and every legal position of +//! interfaces and callback interfaces). //! -//! - A file that shares its name with a C target output is that target's -//! copy of the C header. It must be byte-equal to the C target's own -//! header, which `snapshot_c` snapshots, so it gets no snapshot of its own. -//! - A file matching the target's entry in [`FIXED_FILES`] (fixed runtimes, -//! package manifests, READMEs) is the same for every fixture apart from -//! its name, so it's snapshotted once, from [`FIXED_FROM`]. -//! - Every other file depends on the fixture and is snapshotted for every -//! fixture. +//! Every target renders all five fixtures. Each rendered file must carry the +//! WeaveFFI prelude, and a file that shares its name with a C target output +//! (a target's copy of the C header) must be byte-equal to the C target's. +//! Only `kitchen_sink` is snapshotted, as full files, and only the files +//! that depend on the API: a file matching one of the target's +//! [`fixed_files`](weaveffi_cli::targets::Target::fixed_files) patterns +//! (fixed runtimes, package manifests, READMEs) isn't snapshotted at all. +//! The other fixtures are covered by the fixture compile checks +//! (`scripts/check-fixtures.sh`) and by targeted assertions in each target's +//! unit tests. //! -//! Snapshots live under `tests/snapshots/`. Regressions in any generator's -//! output fail the affected `cargo insta test` job; behavioral regressions -//! are the conformance harness's job. +//! Snapshots live under `tests/snapshots/` and are named +//! `{target}_kitchen_sink__{path}`. use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::Path; -use camino::Utf8Path; -use weaveffi_cli::codegen::{ConfiguredBackend, Target}; -use weaveffi_cli::targets::c::{CConfig, CGenerator}; -use weaveffi_cli::targets::cpp::{CppConfig, CppGenerator}; -use weaveffi_cli::targets::dart::{DartConfig, DartGenerator}; -use weaveffi_cli::targets::dotnet::{DotnetConfig, DotnetGenerator}; -use weaveffi_cli::targets::go::{GoConfig, GoGenerator}; -use weaveffi_cli::targets::kotlin::{KotlinConfig, KotlinGenerator}; -use weaveffi_cli::targets::node::{NodeConfig, NodeGenerator}; -use weaveffi_cli::targets::python::{PythonConfig, PythonGenerator}; -use weaveffi_cli::targets::ruby::{RubyConfig, RubyGenerator}; -use weaveffi_cli::targets::swift::{SwiftConfig, SwiftGenerator}; -use weaveffi_cli::targets::wasm::{WasmConfig, WasmGenerator}; +use weaveffi_cli::targets::{self, Target, REGISTRY}; use weaveffi_model::model::Model; use weaveffi_model::parse::parse_api_str; use weaveffi_model::pkg::Identity; @@ -58,60 +44,11 @@ const FIXTURES: [&str; 5] = [ "edge_cases", ]; -/// The fixture that fixed files are snapshotted from. It exercises every -/// feature, so it also emits every optional fixed file (such as Kotlin's -/// `Async.kt`). -const FIXED_FROM: &str = "kitchen_sink"; - -/// Per target, the files whose content doesn't depend on the fixture beyond -/// its name: fixed runtimes, package manifests, and READMEs. (A README's -/// usage example names the top-level modules, and Kotlin's -/// `build.gradle.kts` adds the coroutines dependency only for async APIs; -/// [`FIXED_FROM`] covers both.) A pattern starting with `*` matches a -/// file-name suffix; any other pattern matches the whole file name. Every -/// pattern must match a [`FIXED_FROM`] output, so a renamed or removed file -/// can't leave a stale entry behind. -const FIXED_FILES: &[(&str, &[&str])] = &[ - ("c", &[]), - ("cpp", &["CMakeLists.txt", "README.md"]), - ("swift", &["Package.swift", "module.modulemap"]), - ( - "kotlin", - &[ - "Async.kt", - "Buffers.kt", - "CMakeLists.txt", - "Runtime.kt", - "build.gradle.kts", - "consumer-rules.pro", - "settings.gradle.kts", - ], - ), - ( - "node", - &["README.md", "binding.gyp", "package.json", "runtime.js"], - ), - ( - "wasm", - &["README.md", "linear.js", "package.json", "runtime.js"], - ), - ( - "python", - &["README.md", "__init__.py", "py.typed", "pyproject.toml"], - ), - ("dotnet", &["*.csproj", "README.md", "Runtime.cs"]), - ("dart", &["README.md", "pubspec.yaml"]), - ("go", &["README.md", "codec.go", "go.mod", "runtime.go"]), - ("ruby", &["*.gemspec", "README.md", "runtime.rb"]), -]; - -fn fixed_patterns(target: &str) -> &'static [&'static str] { - FIXED_FILES - .iter() - .find(|(name, _)| *name == target) - .map(|(_, patterns)| *patterns) - .unwrap_or_else(|| panic!("target {target} has no FIXED_FILES entry")) -} +/// The one fixture whose outputs are snapshotted. It exercises every +/// feature, so it also emits every optional file (such as Kotlin's +/// `Async.kt`), and every fixed-file pattern must match one of its outputs, +/// so a renamed or removed file can't leave a stale entry behind. +const SNAPSHOTTED: &str = "kitchen_sink"; fn matches_pattern(pattern: &str, file_name: &str) -> bool { match pattern.strip_prefix('*') { @@ -123,8 +60,10 @@ fn matches_pattern(pattern: &str, file_name: &str) -> bool { /// The C target's outputs for `api`, keyed by file name. Other targets copy /// the C header verbatim, under the same file name. fn c_outputs(model: &Model) -> BTreeMap { - ConfiguredBackend::new(CGenerator, CConfig::default()) - .render(model, Utf8Path::new("out")) + targets::find("c") + .expect("the C target") + .build_default() + .render(model) .into_iter() .map(|file| { let name = file.path.file_name().expect("file name").to_owned(); @@ -182,8 +121,7 @@ fn assert_prelude_present(contents: &str, file: &Path) { } fn run_snapshots(target: &dyn Target) { - let out_dir = Utf8Path::new("out"); - let fixed = fixed_patterns(target.name()); + let fixed = target.fixed_files(); let mut unmatched: BTreeSet<&str> = fixed.iter().copied().collect(); for stem in FIXTURES { let model = load_model(stem); @@ -192,14 +130,13 @@ fn run_snapshots(target: &dyn Target) { } else { c_outputs(&model) }; - let mut files = target.render(&model, out_dir); + let mut files = target.render(&model); files.sort_by(|a, b| a.path.cmp(&b.path)); assert!( !files.is_empty(), "generator {} produced no files for fixture {stem}", target.name(), ); - let root = out_dir.join(target.name()); insta::with_settings!({ snapshot_path => "snapshots", @@ -217,49 +154,33 @@ fn run_snapshots(target: &dyn Target) { ); continue; } + if stem != SNAPSHOTTED { + continue; + } if let Some(pattern) = fixed.iter().find(|p| matches_pattern(p, file_name)) { - if stem != FIXED_FROM { - continue; - } unmatched.remove(pattern); + continue; } - let rel = file - .path - .strip_prefix(&root) - .expect("file under generator root"); - let name = format!("{}_{stem}__{}", target.name(), sanitize(rel.as_std_path())); + let name = format!( + "{}_{stem}__{}", + target.name(), + sanitize(file.path.as_std_path()) + ); insta::assert_snapshot!(name, redact_version(&file.contents)); } }); } assert!( unmatched.is_empty(), - "FIXED_FILES patterns for {} match no {FIXED_FROM} output: {unmatched:?}", + "fixed-file patterns for {} match no {SNAPSHOTTED} output: {unmatched:?}", target.name(), ); } -macro_rules! snapshot_tests { - ($( $fn_name:ident => $gen:expr, $cfg:ty; )*) => { - $( - #[test] - fn $fn_name() { - run_snapshots(&ConfiguredBackend::new($gen, <$cfg>::default())); - } - )* - }; -} - -snapshot_tests! { - snapshot_c => CGenerator, CConfig; - snapshot_cpp => CppGenerator, CppConfig; - snapshot_swift => SwiftGenerator, SwiftConfig; - snapshot_kotlin => KotlinGenerator, KotlinConfig; - snapshot_node => NodeGenerator, NodeConfig; - snapshot_wasm => WasmGenerator, WasmConfig; - snapshot_python => PythonGenerator, PythonConfig; - snapshot_dotnet => DotnetGenerator, DotnetConfig; - snapshot_dart => DartGenerator, DartConfig; - snapshot_go => GoGenerator, GoConfig; - snapshot_ruby => RubyGenerator, RubyConfig; +/// Every registered target, each with its default configuration. +#[test] +fn snapshot_every_target() { + for desc in REGISTRY { + run_snapshots(desc.build_default().as_ref()); + } } diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_buffer_h.snap deleted file mode 100644 index c31d6ffa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_buffer_h.snap +++ /dev/null @@ -1,539 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef DOCS_EVERYWHERE_BUFFER_H -#define DOCS_EVERYWHERE_BUFFER_H - -#include "docs_everywhere.h" - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* --------------------------------------------------------------------------- - * Value buffer runtime. - * - * The wire format is little-endian and packed: fixed-width scalars at their - * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length - * followed by the raw bytes (no NUL terminator), optionals as a presence byte - * followed by the value, lists as a `uint32_t` count followed by the - * elements, maps as a `uint32_t` count followed by alternating keys and - * values, and objects as a `uint64_t` token holding one strong reference. - * ------------------------------------------------------------------------- */ - -/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. - A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be - passed to C string functions when the text has no interior NUL. */ -typedef struct docs_everywhere_str { - const char* ptr; - size_t len; -} docs_everywhere_str; - -/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ -typedef struct docs_everywhere_bytes { - const uint8_t* ptr; - size_t len; -} docs_everywhere_bytes; - -/* Borrow a NUL-terminated C string (NULL is the empty string). */ -static inline docs_everywhere_str docs_everywhere_str_of(const char* s) { - docs_everywhere_str v; - v.ptr = s; - v.len = s ? strlen(s) : 0; - return v; -} - -/* Borrow `len` bytes at `ptr`. */ -static inline docs_everywhere_bytes docs_everywhere_bytes_of(const uint8_t* ptr, size_t len) { - docs_everywhere_bytes v; - v.ptr = ptr; - v.len = len; - return v; -} - -/* Release a decoded string. Never call this on a borrowed view. */ -static inline void docs_everywhere_str_free(docs_everywhere_str* s) { - free((void*)s->ptr); - s->ptr = NULL; - s->len = 0; -} - -/* Release decoded bytes. Never call this on a borrowed view. */ -static inline void docs_everywhere_bytes_free(docs_everywhere_bytes* b) { - free((void*)b->ptr); - b->ptr = NULL; - b->len = 0; -} - -/* A growable output buffer. Zero-initialize it, write values, pass - `ptr`/`len` to the call, then release it with docs_everywhere_writer_free. An - allocation failure sets `failed` and turns later writes into no-ops. */ -typedef struct docs_everywhere_writer { - uint8_t* ptr; - size_t len; - size_t cap; - bool failed; -} docs_everywhere_writer; - -static inline void docs_everywhere_writer_free(docs_everywhere_writer* w) { - free(w->ptr); - w->ptr = NULL; - w->len = 0; - w->cap = 0; - w->failed = false; -} - -static inline void docs_everywhere_writer_put_raw(docs_everywhere_writer* w, const void* data, size_t n) { - if (w->failed || n == 0) { - return; - } - if (n > w->cap - w->len) { - size_t cap = w->cap ? w->cap : 64; - while (cap - w->len < n) { - if (cap > SIZE_MAX / 2) { - w->failed = true; - return; - } - cap *= 2; - } - uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); - if (grown == NULL) { - w->failed = true; - return; - } - w->ptr = grown; - w->cap = cap; - } - memcpy(w->ptr + w->len, data, n); - w->len += n; -} - -static inline void docs_everywhere_writer_put_u64(docs_everywhere_writer* w, uint64_t v) { - uint8_t b[8]; - for (int i = 0; i < 8; i++) { - b[i] = (uint8_t)(v >> (8 * i)); - } - docs_everywhere_writer_put_raw(w, b, 8); -} - -static inline void docs_everywhere_writer_put_u32(docs_everywhere_writer* w, uint32_t v) { - uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; - docs_everywhere_writer_put_raw(w, b, 4); -} - -static inline void docs_everywhere_writer_put_u16(docs_everywhere_writer* w, uint16_t v) { - uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; - docs_everywhere_writer_put_raw(w, b, 2); -} - -static inline void docs_everywhere_writer_put_u8(docs_everywhere_writer* w, uint8_t v) { - docs_everywhere_writer_put_raw(w, &v, 1); -} - -static inline void docs_everywhere_writer_put_i8(docs_everywhere_writer* w, int8_t v) { - docs_everywhere_writer_put_u8(w, (uint8_t)v); -} - -static inline void docs_everywhere_writer_put_i16(docs_everywhere_writer* w, int16_t v) { - docs_everywhere_writer_put_u16(w, (uint16_t)v); -} - -static inline void docs_everywhere_writer_put_i32(docs_everywhere_writer* w, int32_t v) { - docs_everywhere_writer_put_u32(w, (uint32_t)v); -} - -static inline void docs_everywhere_writer_put_i64(docs_everywhere_writer* w, int64_t v) { - docs_everywhere_writer_put_u64(w, (uint64_t)v); -} - -static inline void docs_everywhere_writer_put_bool(docs_everywhere_writer* w, bool v) { - docs_everywhere_writer_put_u8(w, v ? 1 : 0); -} - -static inline void docs_everywhere_writer_put_f32(docs_everywhere_writer* w, float v) { - uint32_t bits; - memcpy(&bits, &v, 4); - docs_everywhere_writer_put_u32(w, bits); -} - -static inline void docs_everywhere_writer_put_f64(docs_everywhere_writer* w, double v) { - uint64_t bits; - memcpy(&bits, &v, 8); - docs_everywhere_writer_put_u64(w, bits); -} - -static inline void docs_everywhere_writer_put_len(docs_everywhere_writer* w, size_t n) { - if (n > UINT32_MAX) { - w->failed = true; - return; - } - docs_everywhere_writer_put_u32(w, (uint32_t)n); -} - -static inline void docs_everywhere_writer_put_string(docs_everywhere_writer* w, docs_everywhere_str v) { - docs_everywhere_writer_put_len(w, v.len); - docs_everywhere_writer_put_raw(w, v.ptr, v.len); -} - -static inline void docs_everywhere_writer_put_bytes(docs_everywhere_writer* w, docs_everywhere_bytes v) { - docs_everywhere_writer_put_len(w, v.len); - docs_everywhere_writer_put_raw(w, v.ptr, v.len); -} - -/* Write an object token. The token carries one strong reference, so pass - a reference the buffer may own (the generated codecs pass a fresh - `_clone`). */ -static inline void docs_everywhere_writer_put_object(docs_everywhere_writer* w, const void* owned) { - docs_everywhere_writer_put_u64(w, (uint64_t)(uintptr_t)owned); -} - -/* A cursor over an input buffer. A malformed buffer sets `failed`; every - later read then returns a zero value, so a decoder can always finish and - release what it built. */ -typedef struct docs_everywhere_reader { - const uint8_t* ptr; - size_t len; - size_t pos; - bool failed; -} docs_everywhere_reader; - -static inline docs_everywhere_reader docs_everywhere_reader_of(const uint8_t* ptr, size_t len) { - docs_everywhere_reader r; - r.ptr = ptr; - r.len = ptr ? len : 0; - r.pos = 0; - r.failed = false; - return r; -} - -/* `true` when every byte was consumed and nothing was malformed. */ -static inline bool docs_everywhere_reader_finish(const docs_everywhere_reader* r) { - return !r->failed && r->pos == r->len; -} - -static inline const uint8_t* docs_everywhere_reader_take(docs_everywhere_reader* r, size_t n) { - if (r->failed || n > r->len - r->pos) { - r->failed = true; - return NULL; - } - const uint8_t* at = r->ptr + r->pos; - r->pos += n; - return at; -} - -/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is - zero). `min_wire` is the smallest encoding of one element; a count the - remaining bytes can't hold fails the reader before anything is - allocated. */ -static inline void* docs_everywhere_reader_alloc(docs_everywhere_reader* r, size_t n, size_t size, size_t min_wire) { - if (r->failed || n == 0) { - return NULL; - } - if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { - r->failed = true; - return NULL; - } - void* p = calloc(n, size); - if (p == NULL) { - r->failed = true; - } - return p; -} - -static inline uint64_t docs_everywhere_reader_get_u64(docs_everywhere_reader* r) { - const uint8_t* b = docs_everywhere_reader_take(r, 8); - uint64_t v = 0; - if (b != NULL) { - for (int i = 0; i < 8; i++) { - v |= (uint64_t)b[i] << (8 * i); - } - } - return v; -} - -static inline uint32_t docs_everywhere_reader_get_u32(docs_everywhere_reader* r) { - const uint8_t* b = docs_everywhere_reader_take(r, 4); - if (b == NULL) { - return 0; - } - return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); -} - -static inline uint16_t docs_everywhere_reader_get_u16(docs_everywhere_reader* r) { - const uint8_t* b = docs_everywhere_reader_take(r, 2); - return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; -} - -static inline uint8_t docs_everywhere_reader_get_u8(docs_everywhere_reader* r) { - const uint8_t* b = docs_everywhere_reader_take(r, 1); - return b ? b[0] : (uint8_t)0; -} - -static inline int8_t docs_everywhere_reader_get_i8(docs_everywhere_reader* r) { - return (int8_t)docs_everywhere_reader_get_u8(r); -} - -static inline int16_t docs_everywhere_reader_get_i16(docs_everywhere_reader* r) { - return (int16_t)docs_everywhere_reader_get_u16(r); -} - -static inline int32_t docs_everywhere_reader_get_i32(docs_everywhere_reader* r) { - return (int32_t)docs_everywhere_reader_get_u32(r); -} - -static inline int64_t docs_everywhere_reader_get_i64(docs_everywhere_reader* r) { - return (int64_t)docs_everywhere_reader_get_u64(r); -} - -static inline bool docs_everywhere_reader_get_bool(docs_everywhere_reader* r) { - uint8_t v = docs_everywhere_reader_get_u8(r); - if (v > 1) { - r->failed = true; - return false; - } - return v == 1; -} - -static inline float docs_everywhere_reader_get_f32(docs_everywhere_reader* r) { - uint32_t bits = docs_everywhere_reader_get_u32(r); - float v; - memcpy(&v, &bits, 4); - return v; -} - -static inline double docs_everywhere_reader_get_f64(docs_everywhere_reader* r) { - uint64_t bits = docs_everywhere_reader_get_u64(r); - double v; - memcpy(&v, &bits, 8); - return v; -} - -/* A `uint32_t` length followed by that many bytes, copied into a fresh - NUL-terminated allocation. */ -static inline uint8_t* docs_everywhere_reader_get_run(docs_everywhere_reader* r, size_t* out_len) { - size_t n = docs_everywhere_reader_get_u32(r); - const uint8_t* at = docs_everywhere_reader_take(r, n); - *out_len = 0; - if (at == NULL) { - return NULL; - } - uint8_t* copy = (uint8_t*)malloc(n + 1); - if (copy == NULL) { - r->failed = true; - return NULL; - } - memcpy(copy, at, n); - copy[n] = 0; - *out_len = n; - return copy; -} - -static inline docs_everywhere_str docs_everywhere_reader_get_string(docs_everywhere_reader* r) { - docs_everywhere_str v; - v.ptr = (const char*)docs_everywhere_reader_get_run(r, &v.len); - return v; -} - -static inline docs_everywhere_bytes docs_everywhere_reader_get_bytes(docs_everywhere_reader* r) { - docs_everywhere_bytes v; - v.ptr = docs_everywhere_reader_get_run(r, &v.len); - return v; -} - -/* Read an object token and adopt the strong reference it carries. A zero - token in a required position fails the reader. */ -static inline void* docs_everywhere_reader_get_object(docs_everywhere_reader* r) { - uint64_t token = docs_everywhere_reader_get_u64(r); - if (token == 0) { - r->failed = true; - } - return (void*)(uintptr_t)token; -} - -/* --------------------------------------------------------------------------- - * Value types. A record is a struct of its fields; a rich enum is a `tag` - * plus a union `as` with one struct per payload-carrying variant; a list is - * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an - * optional is a pointer that is NULL when absent; an object is its pointer; - * a string or byte string is a view (`ptr` + `len`). - * ------------------------------------------------------------------------- */ - -typedef struct docs_everywhere_docs_Document docs_everywhere_docs_Document; -typedef struct docs_everywhere_list_string docs_everywhere_list_string; - -/** `[string]` */ -struct docs_everywhere_list_string { - docs_everywhere_str* items; - size_t len; -}; - -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -struct docs_everywhere_docs_Document { - /** Stable opaque identifier */ - int64_t id; - /** Human-readable title shown in lists */ - docs_everywhere_str title; - /** Current lifecycle status */ - docs_everywhere_docs_Status status; - /** Free-form tags for grouping */ - docs_everywhere_list_string tags; -}; - -/* --------------------------------------------------------------------------- - * Codecs. Each value type T above, and each optional shape, has four - * functions: - * - * T_write(w, v) appends the value at `v` to writer `w` - * T_read(r, out) decodes one value from reader `r` - * T_decode(ptr, len, out) decodes a whole buffer; on a malformed - * buffer it frees what it built and - * returns false - * T_free(v) releases a decoded value - * - * An optional's T_write takes the nullable pointer itself. Writing an - * object stores a fresh `_clone` reference for the reader to adopt; - * decoding adopts each object reference, and T_free destroys it. T_free - * releases only what T_read or T_decode allocated; never call it on a - * value that borrows caller memory. - * ------------------------------------------------------------------------- */ - -static inline void docs_everywhere_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v); -static inline void docs_everywhere_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document* out); -static inline bool docs_everywhere_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document* out); -static inline void docs_everywhere_docs_Document_free(docs_everywhere_docs_Document* v); -static inline void docs_everywhere_list_string_write(docs_everywhere_writer* w, const docs_everywhere_list_string* v); -static inline void docs_everywhere_list_string_read(docs_everywhere_reader* r, docs_everywhere_list_string* out); -static inline bool docs_everywhere_list_string_decode(const uint8_t* ptr, size_t len, docs_everywhere_list_string* out); -static inline void docs_everywhere_list_string_free(docs_everywhere_list_string* v); -static inline void docs_everywhere_opt_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v); -static inline void docs_everywhere_opt_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document** out); -static inline bool docs_everywhere_opt_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document** out); -static inline void docs_everywhere_opt_docs_Document_free(docs_everywhere_docs_Document** v); - -static inline void docs_everywhere_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v) { - docs_everywhere_writer_put_i64(w, v->id); - docs_everywhere_writer_put_string(w, v->title); - docs_everywhere_writer_put_i32(w, (int32_t)v->status); - docs_everywhere_list_string_write(w, &v->tags); -} - -static inline void docs_everywhere_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document* out) { - memset(out, 0, sizeof *out); - out->id = docs_everywhere_reader_get_i64(r); - out->title = docs_everywhere_reader_get_string(r); - out->status = (docs_everywhere_docs_Status)docs_everywhere_reader_get_i32(r); - docs_everywhere_list_string_read(r, &out->tags); -} - -static inline bool docs_everywhere_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document* out) { - docs_everywhere_reader r = docs_everywhere_reader_of(ptr, len); - docs_everywhere_docs_Document_read(&r, out); - if (!docs_everywhere_reader_finish(&r)) { - docs_everywhere_docs_Document_free(out); - return false; - } - return true; -} - -static inline void docs_everywhere_docs_Document_free(docs_everywhere_docs_Document* v) { - docs_everywhere_str_free(&v->title); - docs_everywhere_list_string_free(&v->tags); - memset(v, 0, sizeof *v); -} - -static inline void docs_everywhere_list_string_write(docs_everywhere_writer* w, const docs_everywhere_list_string* v) { - docs_everywhere_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - docs_everywhere_writer_put_string(w, v->items[i]); - } -} - -static inline void docs_everywhere_list_string_read(docs_everywhere_reader* r, docs_everywhere_list_string* out) { - memset(out, 0, sizeof *out); - size_t n = docs_everywhere_reader_get_u32(r); - out->items = (docs_everywhere_str*)docs_everywhere_reader_alloc(r, n, sizeof(docs_everywhere_str), 4); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->items[i] = docs_everywhere_reader_get_string(r); - } -} - -static inline bool docs_everywhere_list_string_decode(const uint8_t* ptr, size_t len, docs_everywhere_list_string* out) { - docs_everywhere_reader r = docs_everywhere_reader_of(ptr, len); - docs_everywhere_list_string_read(&r, out); - if (!docs_everywhere_reader_finish(&r)) { - docs_everywhere_list_string_free(out); - return false; - } - return true; -} - -static inline void docs_everywhere_list_string_free(docs_everywhere_list_string* v) { - for (size_t i = 0; i < v->len; i++) { - docs_everywhere_str_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void docs_everywhere_opt_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v) { - docs_everywhere_writer_put_bool(w, v != NULL); - if (v != NULL) { - docs_everywhere_docs_Document_write(w, &(*v)); - } -} - -static inline void docs_everywhere_opt_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document** out) { - *out = NULL; - if (!docs_everywhere_reader_get_bool(r)) { - return; - } - docs_everywhere_docs_Document* value = (docs_everywhere_docs_Document*)calloc(1, sizeof(docs_everywhere_docs_Document)); - if (value == NULL) { - r->failed = true; - return; - } - docs_everywhere_docs_Document_read(r, &(*value)); - *out = value; -} - -static inline bool docs_everywhere_opt_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document** out) { - docs_everywhere_reader r = docs_everywhere_reader_of(ptr, len); - docs_everywhere_opt_docs_Document_read(&r, out); - if (!docs_everywhere_reader_finish(&r)) { - docs_everywhere_opt_docs_Document_free(out); - return false; - } - return true; -} - -static inline void docs_everywhere_opt_docs_Document_free(docs_everywhere_docs_Document** v) { - if (*v == NULL) { - return; - } - docs_everywhere_docs_Document_free(&(**v)); - free(*v); - *v = NULL; -} - -#ifdef __cplusplus -} -#endif - -#endif // DOCS_EVERYWHERE_BUFFER_H - -// END docs_everywhere_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_h.snap deleted file mode 100644 index 116d8703..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_h.snap +++ /dev/null @@ -1,245 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef DOCS_EVERYWHERE_H -#define DOCS_EVERYWHERE_H - -#include -#include -#include - -#ifndef DOCS_EVERYWHERE_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef DOCS_EVERYWHERE_BUILD -# define DOCS_EVERYWHERE_API __declspec(dllexport) -# else -# define DOCS_EVERYWHERE_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) -# else -# define DOCS_EVERYWHERE_API -# endif -#endif - -#ifndef DOCS_EVERYWHERE_DEPRECATED -# if defined(_MSC_VER) -# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define DOCS_EVERYWHERE_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* The C ABI revision this header was generated against. The producer - exports docs_everywhere_abi_version() so a consumer can refuse to load a - library built for a different revision. */ -#define DOCS_EVERYWHERE_ABI_VERSION 4u -DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); - -/* Error slot written by every fallible call. `message` is NUL-terminated - UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields - serialized in the value buffer format (null when the code declares no - fields). Both are released by docs_everywhere_error_clear. Positive codes are - the API's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation failed, -5 cancelled. */ -typedef struct docs_everywhere_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} docs_everywhere_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface implementations call this to report a failure without - allocating with a foreign allocator. */ -DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); -/* Replace `err`'s payload with a producer-owned copy of `len` bytes: the - fields of a declared error code, which a callback method that throws - attaches after docs_everywhere_error_set. */ -DOCS_EVERYWHERE_API void docs_everywhere_error_set_payload(docs_everywhere_error* err, const uint8_t* ptr, size_t len); -DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); - -/* Async completions receive a heap-boxed error the consumer owns; - docs_everywhere_error_free releases the message, the payload, and the box. - NULL is a no-op. */ -DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); - -/* Allocate a zero-filled run of `len` bytes (NULL for 0). A callback - method returns a string, bytes, or buffer as such a run, which the - producer adopts; any other run is released with docs_everywhere_free_bytes. */ -DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(size_t len); -/* Release a string, bytes, or value buffer the producer returned, or a - run from docs_everywhere_alloc, with its exact length. NULL or 0 is a no-op. */ -DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); - -/* Cancellation for `cancellable` async functions. A token starts with - one reference owned by the consumer; the producer takes its own for - each call it is passed to, so the consumer may cancel and destroy its - reference at any time. A cancelled call completes with code -5. */ -typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; -DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); -DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); -DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); -DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); - -/* Live-allocation counters for leak checks in tests: 0 objects, - 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. - Kind -1 is 1 when the producer counts at all (its `leak-check` - feature) and 0 when every counter reads 0 regardless. */ -DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); - -/* One declaration's fingerprint in a module's contract table: FNV-1a 64 - of its dotted path and of its canonical signature. */ -typedef struct docs_everywhere_contract_entry { - uint64_t id; - uint64_t hash; -} docs_everywhere_contract_entry; - -/* The contract of module `docs`: one entry per declaration, sorted by - id. The producer's table comes from the function below; a consumer - checks each entry it was generated with against it once, before any - other call (producer entries it doesn't know are fine). */ -DOCS_EVERYWHERE_API const docs_everywhere_contract_entry* docs_everywhere_docs_contract(size_t* out_len); -#define DOCS_EVERYWHERE_DOCS_CONTRACT_LEN 14 -#define DOCS_EVERYWHERE_DOCS_CONTRACT { \ - {0x00b2187fd75634ecull, 0x2263c6f787bf6081ull}, /* docs.publish_async */ \ - {0x12fc242b9dc6444cull, 0x96405f484a642b31ull}, /* docs.Notebook.insert */ \ - {0x2213822ed05cd5bdull, 0x26c5833392aa63ceull}, /* docs.Notebook.new */ \ - {0x2bd762bcdaa207ffull, 0x1b0ebab2d04c49b0ull}, /* docs.SavedListener */ \ - {0x2e086c45e9d617deull, 0x4d43bf32358c01ffull}, /* docs.Notebook.find */ \ - {0x4817dc98197ad960ull, 0xf8017b5a7d58b2c7ull}, /* docs.Notebook.size */ \ - {0x614de47452d5a55cull, 0xd9aae1472494ffefull}, /* docs.get_document */ \ - {0x67c5cb9fc119bcd5ull, 0x08d5b25a1a22a7faull}, /* docs.Document */ \ - {0x8d484bf2d78c9b70ull, 0xe9093cf96912194cull}, /* docs.Notebook.max_documents */ \ - {0xaea61d6451691c67ull, 0xe4704128b3524b3dull}, /* docs.watch */ \ - {0xd68d6ad1540cdec5ull, 0x01afeb40b0a8bfe1ull}, /* docs.Notebook */ \ - {0xdb0601444a4760ccull, 0xe56c69629b444797ull}, /* docs.create_document */ \ - {0xe12d6f4838347b3eull, 0xa9bab86646d84f51ull}, /* docs.Status */ \ - {0xee2f8d25b3313b50ull, 0x9691232f35439a2bull}, /* docs.DocsErrors */ \ -} -/* The id of the first expected entry the loaded library lacks or - declares differently, or 0 when these bindings match it. */ -static inline uint64_t docs_everywhere_docs_contract_check(void) { - static const docs_everywhere_contract_entry expected[] = DOCS_EVERYWHERE_DOCS_CONTRACT; - size_t len = 0; - const docs_everywhere_contract_entry* table = docs_everywhere_docs_contract(&len); - for (size_t i = 0; i < DOCS_EVERYWHERE_DOCS_CONTRACT_LEN; i++) { - size_t lo = 0, hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) lo = mid + 1; else hi = mid; - } - if (lo == len || table[lo].id != expected[i].id || table[lo].hash != expected[i].hash) { - return expected[i].id; - } - } - return 0; -} - -/* - * Strings, bytes, and value buffers cross as (ptr, len) pairs. A - * parameter named "v" expands to a view borrowed for the call: - * const uint8_t* v_ptr, size_t v_len - * Strings are UTF-8 without a NUL terminator; NULL with length 0 is - * the empty value. A returned string, bytes, or buffer is - * `const uint8_t*` with a trailing `size_t* out_len`, owned by the - * caller and released with docs_everywhere_free_bytes. - * Records, rich enums, lists, maps, and optionals are serialized - * in the little-endian value buffer format; when generated, - * docs_everywhere_buffer.h declares a C struct and codec for each. - */ - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -typedef enum { - /** Document is being edited */ - docs_everywhere_docs_Status_Draft = 0, - /** Document is publicly visible */ - docs_everywhere_docs_Status_Published = 1, - /** Document is hidden from listings */ - docs_everywhere_docs_Status_Archived = 2 -} docs_everywhere_docs_Status; -/** Error codes reported by throwing functions in the `docs` module tree. */ -typedef enum { - /** Returned when no document matches the requested id */ - docs_everywhere_docs_DocsErrors_not_found = 1, - /** Raised when attempting to move into a forbidden state */ - docs_everywhere_docs_DocsErrors_invalid_status = 2 -} docs_everywhere_docs_DocsErrors; -typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - * - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable, with `size` set to `sizeof` the vtable and `flags` to 0 (the producer rejects a smaller vtable). The producer may call any entry from any thread until it calls `free(ctx)` exactly once, also from any thread. A method reports failure with `error_set(out_err, ...)`; a string, bytes, or buffer return is a run the consumer allocates with `alloc` and writes to `out_ptr` and `out_len`, which the producer adopts. - */ -typedef struct docs_everywhere_docs_SavedListener_vtable { - uint32_t size; - uint32_t flags; - void (*free)(void* ctx); - /** Fires after a document has been persisted successfully */ - void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); -} docs_everywhere_docs_SavedListener_vtable; -typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); - -// Module: docs -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -/** Create an empty notebook */ -DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); -/** The largest number of documents one notebook will hold */ -DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); -/** Add a document, returning its position in the notebook */ -DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); -/** Look up a document by identifier, failing when none matches */ -DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); -/** The number of documents currently held */ -DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); - -/** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - */ -DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); -/** Look up a document by identifier */ -DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); -/** Publish a document in the background */ -DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); -/** Register a listener for saved notifications */ -DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // DOCS_EVERYWHERE_H - -// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_buffer_h.snap deleted file mode 100644 index 294a06e1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_buffer_h.snap +++ /dev/null @@ -1,1907 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef EDGE_CASES_BUFFER_H -#define EDGE_CASES_BUFFER_H - -#include "edge_cases.h" - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* --------------------------------------------------------------------------- - * Value buffer runtime. - * - * The wire format is little-endian and packed: fixed-width scalars at their - * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length - * followed by the raw bytes (no NUL terminator), optionals as a presence byte - * followed by the value, lists as a `uint32_t` count followed by the - * elements, maps as a `uint32_t` count followed by alternating keys and - * values, and objects as a `uint64_t` token holding one strong reference. - * ------------------------------------------------------------------------- */ - -/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. - A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be - passed to C string functions when the text has no interior NUL. */ -typedef struct edge_cases_str { - const char* ptr; - size_t len; -} edge_cases_str; - -/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ -typedef struct edge_cases_bytes { - const uint8_t* ptr; - size_t len; -} edge_cases_bytes; - -/* Borrow a NUL-terminated C string (NULL is the empty string). */ -static inline edge_cases_str edge_cases_str_of(const char* s) { - edge_cases_str v; - v.ptr = s; - v.len = s ? strlen(s) : 0; - return v; -} - -/* Borrow `len` bytes at `ptr`. */ -static inline edge_cases_bytes edge_cases_bytes_of(const uint8_t* ptr, size_t len) { - edge_cases_bytes v; - v.ptr = ptr; - v.len = len; - return v; -} - -/* Release a decoded string. Never call this on a borrowed view. */ -static inline void edge_cases_str_free(edge_cases_str* s) { - free((void*)s->ptr); - s->ptr = NULL; - s->len = 0; -} - -/* Release decoded bytes. Never call this on a borrowed view. */ -static inline void edge_cases_bytes_free(edge_cases_bytes* b) { - free((void*)b->ptr); - b->ptr = NULL; - b->len = 0; -} - -/* A growable output buffer. Zero-initialize it, write values, pass - `ptr`/`len` to the call, then release it with edge_cases_writer_free. An - allocation failure sets `failed` and turns later writes into no-ops. */ -typedef struct edge_cases_writer { - uint8_t* ptr; - size_t len; - size_t cap; - bool failed; -} edge_cases_writer; - -static inline void edge_cases_writer_free(edge_cases_writer* w) { - free(w->ptr); - w->ptr = NULL; - w->len = 0; - w->cap = 0; - w->failed = false; -} - -static inline void edge_cases_writer_put_raw(edge_cases_writer* w, const void* data, size_t n) { - if (w->failed || n == 0) { - return; - } - if (n > w->cap - w->len) { - size_t cap = w->cap ? w->cap : 64; - while (cap - w->len < n) { - if (cap > SIZE_MAX / 2) { - w->failed = true; - return; - } - cap *= 2; - } - uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); - if (grown == NULL) { - w->failed = true; - return; - } - w->ptr = grown; - w->cap = cap; - } - memcpy(w->ptr + w->len, data, n); - w->len += n; -} - -static inline void edge_cases_writer_put_u64(edge_cases_writer* w, uint64_t v) { - uint8_t b[8]; - for (int i = 0; i < 8; i++) { - b[i] = (uint8_t)(v >> (8 * i)); - } - edge_cases_writer_put_raw(w, b, 8); -} - -static inline void edge_cases_writer_put_u32(edge_cases_writer* w, uint32_t v) { - uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; - edge_cases_writer_put_raw(w, b, 4); -} - -static inline void edge_cases_writer_put_u16(edge_cases_writer* w, uint16_t v) { - uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; - edge_cases_writer_put_raw(w, b, 2); -} - -static inline void edge_cases_writer_put_u8(edge_cases_writer* w, uint8_t v) { - edge_cases_writer_put_raw(w, &v, 1); -} - -static inline void edge_cases_writer_put_i8(edge_cases_writer* w, int8_t v) { - edge_cases_writer_put_u8(w, (uint8_t)v); -} - -static inline void edge_cases_writer_put_i16(edge_cases_writer* w, int16_t v) { - edge_cases_writer_put_u16(w, (uint16_t)v); -} - -static inline void edge_cases_writer_put_i32(edge_cases_writer* w, int32_t v) { - edge_cases_writer_put_u32(w, (uint32_t)v); -} - -static inline void edge_cases_writer_put_i64(edge_cases_writer* w, int64_t v) { - edge_cases_writer_put_u64(w, (uint64_t)v); -} - -static inline void edge_cases_writer_put_bool(edge_cases_writer* w, bool v) { - edge_cases_writer_put_u8(w, v ? 1 : 0); -} - -static inline void edge_cases_writer_put_f32(edge_cases_writer* w, float v) { - uint32_t bits; - memcpy(&bits, &v, 4); - edge_cases_writer_put_u32(w, bits); -} - -static inline void edge_cases_writer_put_f64(edge_cases_writer* w, double v) { - uint64_t bits; - memcpy(&bits, &v, 8); - edge_cases_writer_put_u64(w, bits); -} - -static inline void edge_cases_writer_put_len(edge_cases_writer* w, size_t n) { - if (n > UINT32_MAX) { - w->failed = true; - return; - } - edge_cases_writer_put_u32(w, (uint32_t)n); -} - -static inline void edge_cases_writer_put_string(edge_cases_writer* w, edge_cases_str v) { - edge_cases_writer_put_len(w, v.len); - edge_cases_writer_put_raw(w, v.ptr, v.len); -} - -static inline void edge_cases_writer_put_bytes(edge_cases_writer* w, edge_cases_bytes v) { - edge_cases_writer_put_len(w, v.len); - edge_cases_writer_put_raw(w, v.ptr, v.len); -} - -/* Write an object token. The token carries one strong reference, so pass - a reference the buffer may own (the generated codecs pass a fresh - `_clone`). */ -static inline void edge_cases_writer_put_object(edge_cases_writer* w, const void* owned) { - edge_cases_writer_put_u64(w, (uint64_t)(uintptr_t)owned); -} - -/* A cursor over an input buffer. A malformed buffer sets `failed`; every - later read then returns a zero value, so a decoder can always finish and - release what it built. */ -typedef struct edge_cases_reader { - const uint8_t* ptr; - size_t len; - size_t pos; - bool failed; -} edge_cases_reader; - -static inline edge_cases_reader edge_cases_reader_of(const uint8_t* ptr, size_t len) { - edge_cases_reader r; - r.ptr = ptr; - r.len = ptr ? len : 0; - r.pos = 0; - r.failed = false; - return r; -} - -/* `true` when every byte was consumed and nothing was malformed. */ -static inline bool edge_cases_reader_finish(const edge_cases_reader* r) { - return !r->failed && r->pos == r->len; -} - -static inline const uint8_t* edge_cases_reader_take(edge_cases_reader* r, size_t n) { - if (r->failed || n > r->len - r->pos) { - r->failed = true; - return NULL; - } - const uint8_t* at = r->ptr + r->pos; - r->pos += n; - return at; -} - -/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is - zero). `min_wire` is the smallest encoding of one element; a count the - remaining bytes can't hold fails the reader before anything is - allocated. */ -static inline void* edge_cases_reader_alloc(edge_cases_reader* r, size_t n, size_t size, size_t min_wire) { - if (r->failed || n == 0) { - return NULL; - } - if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { - r->failed = true; - return NULL; - } - void* p = calloc(n, size); - if (p == NULL) { - r->failed = true; - } - return p; -} - -static inline uint64_t edge_cases_reader_get_u64(edge_cases_reader* r) { - const uint8_t* b = edge_cases_reader_take(r, 8); - uint64_t v = 0; - if (b != NULL) { - for (int i = 0; i < 8; i++) { - v |= (uint64_t)b[i] << (8 * i); - } - } - return v; -} - -static inline uint32_t edge_cases_reader_get_u32(edge_cases_reader* r) { - const uint8_t* b = edge_cases_reader_take(r, 4); - if (b == NULL) { - return 0; - } - return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); -} - -static inline uint16_t edge_cases_reader_get_u16(edge_cases_reader* r) { - const uint8_t* b = edge_cases_reader_take(r, 2); - return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; -} - -static inline uint8_t edge_cases_reader_get_u8(edge_cases_reader* r) { - const uint8_t* b = edge_cases_reader_take(r, 1); - return b ? b[0] : (uint8_t)0; -} - -static inline int8_t edge_cases_reader_get_i8(edge_cases_reader* r) { - return (int8_t)edge_cases_reader_get_u8(r); -} - -static inline int16_t edge_cases_reader_get_i16(edge_cases_reader* r) { - return (int16_t)edge_cases_reader_get_u16(r); -} - -static inline int32_t edge_cases_reader_get_i32(edge_cases_reader* r) { - return (int32_t)edge_cases_reader_get_u32(r); -} - -static inline int64_t edge_cases_reader_get_i64(edge_cases_reader* r) { - return (int64_t)edge_cases_reader_get_u64(r); -} - -static inline bool edge_cases_reader_get_bool(edge_cases_reader* r) { - uint8_t v = edge_cases_reader_get_u8(r); - if (v > 1) { - r->failed = true; - return false; - } - return v == 1; -} - -static inline float edge_cases_reader_get_f32(edge_cases_reader* r) { - uint32_t bits = edge_cases_reader_get_u32(r); - float v; - memcpy(&v, &bits, 4); - return v; -} - -static inline double edge_cases_reader_get_f64(edge_cases_reader* r) { - uint64_t bits = edge_cases_reader_get_u64(r); - double v; - memcpy(&v, &bits, 8); - return v; -} - -/* A `uint32_t` length followed by that many bytes, copied into a fresh - NUL-terminated allocation. */ -static inline uint8_t* edge_cases_reader_get_run(edge_cases_reader* r, size_t* out_len) { - size_t n = edge_cases_reader_get_u32(r); - const uint8_t* at = edge_cases_reader_take(r, n); - *out_len = 0; - if (at == NULL) { - return NULL; - } - uint8_t* copy = (uint8_t*)malloc(n + 1); - if (copy == NULL) { - r->failed = true; - return NULL; - } - memcpy(copy, at, n); - copy[n] = 0; - *out_len = n; - return copy; -} - -static inline edge_cases_str edge_cases_reader_get_string(edge_cases_reader* r) { - edge_cases_str v; - v.ptr = (const char*)edge_cases_reader_get_run(r, &v.len); - return v; -} - -static inline edge_cases_bytes edge_cases_reader_get_bytes(edge_cases_reader* r) { - edge_cases_bytes v; - v.ptr = edge_cases_reader_get_run(r, &v.len); - return v; -} - -/* Read an object token and adopt the strong reference it carries. A zero - token in a required position fails the reader. */ -static inline void* edge_cases_reader_get_object(edge_cases_reader* r) { - uint64_t token = edge_cases_reader_get_u64(r); - if (token == 0) { - r->failed = true; - } - return (void*)(uintptr_t)token; -} - -/* --------------------------------------------------------------------------- - * Value types. A record is a struct of its fields; a rich enum is a `tag` - * plus a union `as` with one struct per payload-carrying variant; a list is - * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an - * optional is a pointer that is NULL when absent; an object is its pointer; - * a string or byte string is a view (`ptr` + `len`). - * ------------------------------------------------------------------------- */ - -typedef struct edge_cases_edge_cases_Leaf edge_cases_edge_cases_Leaf; -typedef struct edge_cases_edge_cases_Inner edge_cases_edge_cases_Inner; -typedef struct edge_cases_edge_cases_Outer edge_cases_edge_cases_Outer; -typedef struct edge_cases_edge_cases_Keywords edge_cases_edge_cases_Keywords; -typedef struct edge_cases_edge_cases_Retired edge_cases_edge_cases_Retired; -typedef struct edge_cases_edge_cases_WidgetHolder edge_cases_edge_cases_WidgetHolder; -typedef struct edge_cases_list_edge_cases_Leaf edge_cases_list_edge_cases_Leaf; -typedef struct edge_cases_map_string_edge_cases_Inner edge_cases_map_string_edge_cases_Inner; -typedef struct edge_cases_list_edge_cases_Mode edge_cases_list_edge_cases_Mode; -typedef struct edge_cases_list_i32 edge_cases_list_i32; -typedef struct edge_cases_list_list_i32 edge_cases_list_list_i32; -typedef struct edge_cases_list_opt_i64 edge_cases_list_opt_i64; -typedef struct edge_cases_map_i64_string edge_cases_map_i64_string; -typedef struct edge_cases_list_edge_cases_Widget edge_cases_list_edge_cases_Widget; -typedef struct edge_cases_map_string_edge_cases_Widget edge_cases_map_string_edge_cases_Widget; -typedef struct edge_cases_list_string edge_cases_list_string; -typedef struct edge_cases_list_opt_edge_cases_Outer edge_cases_list_opt_edge_cases_Outer; -typedef struct edge_cases_list_opt_edge_cases_Widget edge_cases_list_opt_edge_cases_Widget; -typedef struct edge_cases_list_edge_cases_Outer edge_cases_list_edge_cases_Outer; - -/** `[Leaf]` */ -struct edge_cases_list_edge_cases_Leaf { - edge_cases_edge_cases_Leaf* items; - size_t len; -}; - -/** `{string:Inner}` */ -struct edge_cases_map_string_edge_cases_Inner { - edge_cases_str* keys; - edge_cases_edge_cases_Inner* values; - size_t len; -}; - -/** `[Mode]` */ -struct edge_cases_list_edge_cases_Mode { - edge_cases_edge_cases_Mode* items; - size_t len; -}; - -/** `[i32]` */ -struct edge_cases_list_i32 { - int32_t* items; - size_t len; -}; - -/** `[[i32]]` */ -struct edge_cases_list_list_i32 { - edge_cases_list_i32* items; - size_t len; -}; - -/** `[i64?]` */ -struct edge_cases_list_opt_i64 { - int64_t** items; - size_t len; -}; - -/** `{i64:string}` */ -struct edge_cases_map_i64_string { - int64_t* keys; - edge_cases_str* values; - size_t len; -}; - -/** `[Widget]` */ -struct edge_cases_list_edge_cases_Widget { - edge_cases_edge_cases_Widget** items; - size_t len; -}; - -/** `{string:Widget}` */ -struct edge_cases_map_string_edge_cases_Widget { - edge_cases_str* keys; - edge_cases_edge_cases_Widget** values; - size_t len; -}; - -/** `[string]` */ -struct edge_cases_list_string { - edge_cases_str* items; - size_t len; -}; - -/** `[Outer?]` */ -struct edge_cases_list_opt_edge_cases_Outer { - edge_cases_edge_cases_Outer** items; - size_t len; -}; - -/** `[Widget?]` */ -struct edge_cases_list_opt_edge_cases_Widget { - edge_cases_edge_cases_Widget** items; - size_t len; -}; - -/** `[Outer]` */ -struct edge_cases_list_edge_cases_Outer { - edge_cases_edge_cases_Outer* items; - size_t len; -}; - -struct edge_cases_edge_cases_Leaf { - float weight; -}; - -/** Middle layer of a three-deep struct nesting */ -struct edge_cases_edge_cases_Inner { - edge_cases_edge_cases_Leaf leaf; - edge_cases_list_edge_cases_Leaf leaves; -}; - -struct edge_cases_edge_cases_Outer { - edge_cases_edge_cases_Inner inner; - edge_cases_edge_cases_Inner* maybe_inner; - edge_cases_map_string_edge_cases_Inner by_name; - edge_cases_list_edge_cases_Mode modes; - edge_cases_list_list_i32 matrix; - edge_cases_list_opt_i64 sparse; - edge_cases_bytes* blob; - edge_cases_str* note; - edge_cases_map_i64_string by_id; - bool _hidden; - uint8_t v2; -}; - -/** Every field is a reserved word somewhere */ -struct edge_cases_edge_cases_Keywords { - int32_t class_; - int32_t def; - int32_t import; - int32_t from; - int32_t func; - int32_t range; - int32_t register_; - int32_t volatile_; - int32_t new_; - int32_t default_; - int32_t end; - int32_t in; - int32_t is; - int32_t int_; - int32_t var; - int32_t val; - int32_t self; - int32_t this_; - int32_t object; - int32_t string; - int32_t case_; - int32_t select; - int32_t switch_; - int32_t void_; - int32_t union_; - int32_t namespace_; - int32_t operator_; - int32_t template_; - int32_t package; - int32_t protocol; - int32_t guard; - int32_t defer; - int32_t inout; - int32_t internal; - int32_t when; - int32_t fun; - int32_t data; - int32_t late; - int32_t part; - int32_t out; - int32_t ref; - int32_t event; - int32_t base; - int32_t lambda; - int32_t yield; - int32_t nil; - int32_t null; - int32_t begin; - int32_t rescue; - int32_t module; - int32_t chan; - int32_t go; - int32_t let; - int32_t export_; - int32_t delete_; - int32_t typedef_; - int32_t sizeof_; - int32_t static_; - int32_t super; -}; - -/** - * Deprecated struct - * - * Deprecated: Use Outer instead - */ -struct edge_cases_edge_cases_Retired { - int64_t id; -}; - -/** Record carrying objects in every buffered position */ -struct edge_cases_edge_cases_WidgetHolder { - edge_cases_edge_cases_Widget* primary; - edge_cases_edge_cases_Widget* fallback; - edge_cases_list_edge_cases_Widget extras; - edge_cases_map_string_edge_cases_Widget by_name; -}; - -/* --------------------------------------------------------------------------- - * Codecs. Each value type T above, and each optional shape, has four - * functions: - * - * T_write(w, v) appends the value at `v` to writer `w` - * T_read(r, out) decodes one value from reader `r` - * T_decode(ptr, len, out) decodes a whole buffer; on a malformed - * buffer it frees what it built and - * returns false - * T_free(v) releases a decoded value - * - * An optional's T_write takes the nullable pointer itself. Writing an - * object stores a fresh `_clone` reference for the reader to adopt; - * decoding adopts each object reference, and T_free destroys it. T_free - * releases only what T_read or T_decode allocated; never call it on a - * value that borrows caller memory. - * ------------------------------------------------------------------------- */ - -static inline void edge_cases_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_edge_cases_Leaf* v); -static inline void edge_cases_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_edge_cases_Leaf* out); -static inline bool edge_cases_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Leaf* out); -static inline void edge_cases_edge_cases_Leaf_free(edge_cases_edge_cases_Leaf* v); -static inline void edge_cases_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v); -static inline void edge_cases_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner* out); -static inline bool edge_cases_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner* out); -static inline void edge_cases_edge_cases_Inner_free(edge_cases_edge_cases_Inner* v); -static inline void edge_cases_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v); -static inline void edge_cases_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer* out); -static inline bool edge_cases_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer* out); -static inline void edge_cases_edge_cases_Outer_free(edge_cases_edge_cases_Outer* v); -static inline void edge_cases_edge_cases_Keywords_write(edge_cases_writer* w, const edge_cases_edge_cases_Keywords* v); -static inline void edge_cases_edge_cases_Keywords_read(edge_cases_reader* r, edge_cases_edge_cases_Keywords* out); -static inline bool edge_cases_edge_cases_Keywords_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Keywords* out); -static inline void edge_cases_edge_cases_Keywords_free(edge_cases_edge_cases_Keywords* v); -static inline void edge_cases_edge_cases_Retired_write(edge_cases_writer* w, const edge_cases_edge_cases_Retired* v); -static inline void edge_cases_edge_cases_Retired_read(edge_cases_reader* r, edge_cases_edge_cases_Retired* out); -static inline bool edge_cases_edge_cases_Retired_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Retired* out); -static inline void edge_cases_edge_cases_Retired_free(edge_cases_edge_cases_Retired* v); -static inline void edge_cases_edge_cases_WidgetHolder_write(edge_cases_writer* w, const edge_cases_edge_cases_WidgetHolder* v); -static inline void edge_cases_edge_cases_WidgetHolder_read(edge_cases_reader* r, edge_cases_edge_cases_WidgetHolder* out); -static inline bool edge_cases_edge_cases_WidgetHolder_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_WidgetHolder* out); -static inline void edge_cases_edge_cases_WidgetHolder_free(edge_cases_edge_cases_WidgetHolder* v); -static inline void edge_cases_list_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Leaf* v); -static inline void edge_cases_list_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_list_edge_cases_Leaf* out); -static inline bool edge_cases_list_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Leaf* out); -static inline void edge_cases_list_edge_cases_Leaf_free(edge_cases_list_edge_cases_Leaf* v); -static inline void edge_cases_opt_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v); -static inline void edge_cases_opt_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner** out); -static inline bool edge_cases_opt_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner** out); -static inline void edge_cases_opt_edge_cases_Inner_free(edge_cases_edge_cases_Inner** v); -static inline void edge_cases_map_string_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Inner* v); -static inline void edge_cases_map_string_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Inner* out); -static inline bool edge_cases_map_string_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Inner* out); -static inline void edge_cases_map_string_edge_cases_Inner_free(edge_cases_map_string_edge_cases_Inner* v); -static inline void edge_cases_list_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Mode* v); -static inline void edge_cases_list_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_list_edge_cases_Mode* out); -static inline bool edge_cases_list_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Mode* out); -static inline void edge_cases_list_edge_cases_Mode_free(edge_cases_list_edge_cases_Mode* v); -static inline void edge_cases_list_i32_write(edge_cases_writer* w, const edge_cases_list_i32* v); -static inline void edge_cases_list_i32_read(edge_cases_reader* r, edge_cases_list_i32* out); -static inline bool edge_cases_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_i32* out); -static inline void edge_cases_list_i32_free(edge_cases_list_i32* v); -static inline void edge_cases_list_list_i32_write(edge_cases_writer* w, const edge_cases_list_list_i32* v); -static inline void edge_cases_list_list_i32_read(edge_cases_reader* r, edge_cases_list_list_i32* out); -static inline bool edge_cases_list_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_list_i32* out); -static inline void edge_cases_list_list_i32_free(edge_cases_list_list_i32* v); -static inline void edge_cases_opt_i64_write(edge_cases_writer* w, const int64_t* v); -static inline void edge_cases_opt_i64_read(edge_cases_reader* r, int64_t** out); -static inline bool edge_cases_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out); -static inline void edge_cases_opt_i64_free(int64_t** v); -static inline void edge_cases_list_opt_i64_write(edge_cases_writer* w, const edge_cases_list_opt_i64* v); -static inline void edge_cases_list_opt_i64_read(edge_cases_reader* r, edge_cases_list_opt_i64* out); -static inline bool edge_cases_list_opt_i64_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_i64* out); -static inline void edge_cases_list_opt_i64_free(edge_cases_list_opt_i64* v); -static inline void edge_cases_opt_bytes_write(edge_cases_writer* w, const edge_cases_bytes* v); -static inline void edge_cases_opt_bytes_read(edge_cases_reader* r, edge_cases_bytes** out); -static inline bool edge_cases_opt_bytes_decode(const uint8_t* ptr, size_t len, edge_cases_bytes** out); -static inline void edge_cases_opt_bytes_free(edge_cases_bytes** v); -static inline void edge_cases_opt_string_write(edge_cases_writer* w, const edge_cases_str* v); -static inline void edge_cases_opt_string_read(edge_cases_reader* r, edge_cases_str** out); -static inline bool edge_cases_opt_string_decode(const uint8_t* ptr, size_t len, edge_cases_str** out); -static inline void edge_cases_opt_string_free(edge_cases_str** v); -static inline void edge_cases_map_i64_string_write(edge_cases_writer* w, const edge_cases_map_i64_string* v); -static inline void edge_cases_map_i64_string_read(edge_cases_reader* r, edge_cases_map_i64_string* out); -static inline bool edge_cases_map_i64_string_decode(const uint8_t* ptr, size_t len, edge_cases_map_i64_string* out); -static inline void edge_cases_map_i64_string_free(edge_cases_map_i64_string* v); -static inline void edge_cases_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_edge_cases_Widget* v); -static inline void edge_cases_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_edge_cases_Widget** out); -static inline bool edge_cases_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Widget** out); -static inline void edge_cases_opt_edge_cases_Widget_free(edge_cases_edge_cases_Widget** v); -static inline void edge_cases_list_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Widget* v); -static inline void edge_cases_list_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_edge_cases_Widget* out); -static inline bool edge_cases_list_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Widget* out); -static inline void edge_cases_list_edge_cases_Widget_free(edge_cases_list_edge_cases_Widget* v); -static inline void edge_cases_map_string_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Widget* v); -static inline void edge_cases_map_string_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Widget* out); -static inline bool edge_cases_map_string_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Widget* out); -static inline void edge_cases_map_string_edge_cases_Widget_free(edge_cases_map_string_edge_cases_Widget* v); -static inline void edge_cases_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v); -static inline void edge_cases_list_string_read(edge_cases_reader* r, edge_cases_list_string* out); -static inline bool edge_cases_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string* out); -static inline void edge_cases_list_string_free(edge_cases_list_string* v); -static inline void edge_cases_opt_i32_write(edge_cases_writer* w, const int32_t* v); -static inline void edge_cases_opt_i32_read(edge_cases_reader* r, int32_t** out); -static inline bool edge_cases_opt_i32_decode(const uint8_t* ptr, size_t len, int32_t** out); -static inline void edge_cases_opt_i32_free(int32_t** v); -static inline void edge_cases_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v); -static inline void edge_cases_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer** out); -static inline bool edge_cases_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer** out); -static inline void edge_cases_opt_edge_cases_Outer_free(edge_cases_edge_cases_Outer** v); -static inline void edge_cases_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v); -static inline void edge_cases_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer* out); -static inline bool edge_cases_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer* out); -static inline void edge_cases_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer* v); -static inline void edge_cases_opt_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v); -static inline void edge_cases_opt_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer** out); -static inline bool edge_cases_opt_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer** out); -static inline void edge_cases_opt_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer** v); -static inline void edge_cases_opt_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v); -static inline void edge_cases_opt_list_string_read(edge_cases_reader* r, edge_cases_list_string** out); -static inline bool edge_cases_opt_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string** out); -static inline void edge_cases_opt_list_string_free(edge_cases_list_string** v); -static inline void edge_cases_opt_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_edge_cases_Mode* v); -static inline void edge_cases_opt_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_edge_cases_Mode** out); -static inline bool edge_cases_opt_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Mode** out); -static inline void edge_cases_opt_edge_cases_Mode_free(edge_cases_edge_cases_Mode** v); -static inline void edge_cases_list_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Widget* v); -static inline void edge_cases_list_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Widget* out); -static inline bool edge_cases_list_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Widget* out); -static inline void edge_cases_list_opt_edge_cases_Widget_free(edge_cases_list_opt_edge_cases_Widget* v); -static inline void edge_cases_list_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Outer* v); -static inline void edge_cases_list_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_edge_cases_Outer* out); -static inline bool edge_cases_list_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Outer* out); -static inline void edge_cases_list_edge_cases_Outer_free(edge_cases_list_edge_cases_Outer* v); - -static inline void edge_cases_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_edge_cases_Leaf* v) { - edge_cases_writer_put_f32(w, v->weight); -} - -static inline void edge_cases_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_edge_cases_Leaf* out) { - memset(out, 0, sizeof *out); - out->weight = edge_cases_reader_get_f32(r); -} - -static inline bool edge_cases_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Leaf* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_edge_cases_Leaf_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_edge_cases_Leaf_free(out); - return false; - } - return true; -} - -static inline void edge_cases_edge_cases_Leaf_free(edge_cases_edge_cases_Leaf* v) { - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v) { - edge_cases_edge_cases_Leaf_write(w, &v->leaf); - edge_cases_list_edge_cases_Leaf_write(w, &v->leaves); -} - -static inline void edge_cases_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner* out) { - memset(out, 0, sizeof *out); - edge_cases_edge_cases_Leaf_read(r, &out->leaf); - edge_cases_list_edge_cases_Leaf_read(r, &out->leaves); -} - -static inline bool edge_cases_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_edge_cases_Inner_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_edge_cases_Inner_free(out); - return false; - } - return true; -} - -static inline void edge_cases_edge_cases_Inner_free(edge_cases_edge_cases_Inner* v) { - edge_cases_list_edge_cases_Leaf_free(&v->leaves); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v) { - edge_cases_edge_cases_Inner_write(w, &v->inner); - edge_cases_opt_edge_cases_Inner_write(w, v->maybe_inner); - edge_cases_map_string_edge_cases_Inner_write(w, &v->by_name); - edge_cases_list_edge_cases_Mode_write(w, &v->modes); - edge_cases_list_list_i32_write(w, &v->matrix); - edge_cases_list_opt_i64_write(w, &v->sparse); - edge_cases_opt_bytes_write(w, v->blob); - edge_cases_opt_string_write(w, v->note); - edge_cases_map_i64_string_write(w, &v->by_id); - edge_cases_writer_put_bool(w, v->_hidden); - edge_cases_writer_put_u8(w, v->v2); -} - -static inline void edge_cases_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer* out) { - memset(out, 0, sizeof *out); - edge_cases_edge_cases_Inner_read(r, &out->inner); - edge_cases_opt_edge_cases_Inner_read(r, &out->maybe_inner); - edge_cases_map_string_edge_cases_Inner_read(r, &out->by_name); - edge_cases_list_edge_cases_Mode_read(r, &out->modes); - edge_cases_list_list_i32_read(r, &out->matrix); - edge_cases_list_opt_i64_read(r, &out->sparse); - edge_cases_opt_bytes_read(r, &out->blob); - edge_cases_opt_string_read(r, &out->note); - edge_cases_map_i64_string_read(r, &out->by_id); - out->_hidden = edge_cases_reader_get_bool(r); - out->v2 = edge_cases_reader_get_u8(r); -} - -static inline bool edge_cases_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_edge_cases_Outer_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_edge_cases_Outer_free(out); - return false; - } - return true; -} - -static inline void edge_cases_edge_cases_Outer_free(edge_cases_edge_cases_Outer* v) { - edge_cases_edge_cases_Inner_free(&v->inner); - edge_cases_opt_edge_cases_Inner_free(&v->maybe_inner); - edge_cases_map_string_edge_cases_Inner_free(&v->by_name); - edge_cases_list_edge_cases_Mode_free(&v->modes); - edge_cases_list_list_i32_free(&v->matrix); - edge_cases_list_opt_i64_free(&v->sparse); - edge_cases_opt_bytes_free(&v->blob); - edge_cases_opt_string_free(&v->note); - edge_cases_map_i64_string_free(&v->by_id); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_edge_cases_Keywords_write(edge_cases_writer* w, const edge_cases_edge_cases_Keywords* v) { - edge_cases_writer_put_i32(w, v->class_); - edge_cases_writer_put_i32(w, v->def); - edge_cases_writer_put_i32(w, v->import); - edge_cases_writer_put_i32(w, v->from); - edge_cases_writer_put_i32(w, v->func); - edge_cases_writer_put_i32(w, v->range); - edge_cases_writer_put_i32(w, v->register_); - edge_cases_writer_put_i32(w, v->volatile_); - edge_cases_writer_put_i32(w, v->new_); - edge_cases_writer_put_i32(w, v->default_); - edge_cases_writer_put_i32(w, v->end); - edge_cases_writer_put_i32(w, v->in); - edge_cases_writer_put_i32(w, v->is); - edge_cases_writer_put_i32(w, v->int_); - edge_cases_writer_put_i32(w, v->var); - edge_cases_writer_put_i32(w, v->val); - edge_cases_writer_put_i32(w, v->self); - edge_cases_writer_put_i32(w, v->this_); - edge_cases_writer_put_i32(w, v->object); - edge_cases_writer_put_i32(w, v->string); - edge_cases_writer_put_i32(w, v->case_); - edge_cases_writer_put_i32(w, v->select); - edge_cases_writer_put_i32(w, v->switch_); - edge_cases_writer_put_i32(w, v->void_); - edge_cases_writer_put_i32(w, v->union_); - edge_cases_writer_put_i32(w, v->namespace_); - edge_cases_writer_put_i32(w, v->operator_); - edge_cases_writer_put_i32(w, v->template_); - edge_cases_writer_put_i32(w, v->package); - edge_cases_writer_put_i32(w, v->protocol); - edge_cases_writer_put_i32(w, v->guard); - edge_cases_writer_put_i32(w, v->defer); - edge_cases_writer_put_i32(w, v->inout); - edge_cases_writer_put_i32(w, v->internal); - edge_cases_writer_put_i32(w, v->when); - edge_cases_writer_put_i32(w, v->fun); - edge_cases_writer_put_i32(w, v->data); - edge_cases_writer_put_i32(w, v->late); - edge_cases_writer_put_i32(w, v->part); - edge_cases_writer_put_i32(w, v->out); - edge_cases_writer_put_i32(w, v->ref); - edge_cases_writer_put_i32(w, v->event); - edge_cases_writer_put_i32(w, v->base); - edge_cases_writer_put_i32(w, v->lambda); - edge_cases_writer_put_i32(w, v->yield); - edge_cases_writer_put_i32(w, v->nil); - edge_cases_writer_put_i32(w, v->null); - edge_cases_writer_put_i32(w, v->begin); - edge_cases_writer_put_i32(w, v->rescue); - edge_cases_writer_put_i32(w, v->module); - edge_cases_writer_put_i32(w, v->chan); - edge_cases_writer_put_i32(w, v->go); - edge_cases_writer_put_i32(w, v->let); - edge_cases_writer_put_i32(w, v->export_); - edge_cases_writer_put_i32(w, v->delete_); - edge_cases_writer_put_i32(w, v->typedef_); - edge_cases_writer_put_i32(w, v->sizeof_); - edge_cases_writer_put_i32(w, v->static_); - edge_cases_writer_put_i32(w, v->super); -} - -static inline void edge_cases_edge_cases_Keywords_read(edge_cases_reader* r, edge_cases_edge_cases_Keywords* out) { - memset(out, 0, sizeof *out); - out->class_ = edge_cases_reader_get_i32(r); - out->def = edge_cases_reader_get_i32(r); - out->import = edge_cases_reader_get_i32(r); - out->from = edge_cases_reader_get_i32(r); - out->func = edge_cases_reader_get_i32(r); - out->range = edge_cases_reader_get_i32(r); - out->register_ = edge_cases_reader_get_i32(r); - out->volatile_ = edge_cases_reader_get_i32(r); - out->new_ = edge_cases_reader_get_i32(r); - out->default_ = edge_cases_reader_get_i32(r); - out->end = edge_cases_reader_get_i32(r); - out->in = edge_cases_reader_get_i32(r); - out->is = edge_cases_reader_get_i32(r); - out->int_ = edge_cases_reader_get_i32(r); - out->var = edge_cases_reader_get_i32(r); - out->val = edge_cases_reader_get_i32(r); - out->self = edge_cases_reader_get_i32(r); - out->this_ = edge_cases_reader_get_i32(r); - out->object = edge_cases_reader_get_i32(r); - out->string = edge_cases_reader_get_i32(r); - out->case_ = edge_cases_reader_get_i32(r); - out->select = edge_cases_reader_get_i32(r); - out->switch_ = edge_cases_reader_get_i32(r); - out->void_ = edge_cases_reader_get_i32(r); - out->union_ = edge_cases_reader_get_i32(r); - out->namespace_ = edge_cases_reader_get_i32(r); - out->operator_ = edge_cases_reader_get_i32(r); - out->template_ = edge_cases_reader_get_i32(r); - out->package = edge_cases_reader_get_i32(r); - out->protocol = edge_cases_reader_get_i32(r); - out->guard = edge_cases_reader_get_i32(r); - out->defer = edge_cases_reader_get_i32(r); - out->inout = edge_cases_reader_get_i32(r); - out->internal = edge_cases_reader_get_i32(r); - out->when = edge_cases_reader_get_i32(r); - out->fun = edge_cases_reader_get_i32(r); - out->data = edge_cases_reader_get_i32(r); - out->late = edge_cases_reader_get_i32(r); - out->part = edge_cases_reader_get_i32(r); - out->out = edge_cases_reader_get_i32(r); - out->ref = edge_cases_reader_get_i32(r); - out->event = edge_cases_reader_get_i32(r); - out->base = edge_cases_reader_get_i32(r); - out->lambda = edge_cases_reader_get_i32(r); - out->yield = edge_cases_reader_get_i32(r); - out->nil = edge_cases_reader_get_i32(r); - out->null = edge_cases_reader_get_i32(r); - out->begin = edge_cases_reader_get_i32(r); - out->rescue = edge_cases_reader_get_i32(r); - out->module = edge_cases_reader_get_i32(r); - out->chan = edge_cases_reader_get_i32(r); - out->go = edge_cases_reader_get_i32(r); - out->let = edge_cases_reader_get_i32(r); - out->export_ = edge_cases_reader_get_i32(r); - out->delete_ = edge_cases_reader_get_i32(r); - out->typedef_ = edge_cases_reader_get_i32(r); - out->sizeof_ = edge_cases_reader_get_i32(r); - out->static_ = edge_cases_reader_get_i32(r); - out->super = edge_cases_reader_get_i32(r); -} - -static inline bool edge_cases_edge_cases_Keywords_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Keywords* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_edge_cases_Keywords_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_edge_cases_Keywords_free(out); - return false; - } - return true; -} - -static inline void edge_cases_edge_cases_Keywords_free(edge_cases_edge_cases_Keywords* v) { - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_edge_cases_Retired_write(edge_cases_writer* w, const edge_cases_edge_cases_Retired* v) { - edge_cases_writer_put_i64(w, v->id); -} - -static inline void edge_cases_edge_cases_Retired_read(edge_cases_reader* r, edge_cases_edge_cases_Retired* out) { - memset(out, 0, sizeof *out); - out->id = edge_cases_reader_get_i64(r); -} - -static inline bool edge_cases_edge_cases_Retired_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Retired* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_edge_cases_Retired_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_edge_cases_Retired_free(out); - return false; - } - return true; -} - -static inline void edge_cases_edge_cases_Retired_free(edge_cases_edge_cases_Retired* v) { - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_edge_cases_WidgetHolder_write(edge_cases_writer* w, const edge_cases_edge_cases_WidgetHolder* v) { - edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v->primary)); - edge_cases_opt_edge_cases_Widget_write(w, v->fallback); - edge_cases_list_edge_cases_Widget_write(w, &v->extras); - edge_cases_map_string_edge_cases_Widget_write(w, &v->by_name); -} - -static inline void edge_cases_edge_cases_WidgetHolder_read(edge_cases_reader* r, edge_cases_edge_cases_WidgetHolder* out) { - memset(out, 0, sizeof *out); - out->primary = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); - edge_cases_opt_edge_cases_Widget_read(r, &out->fallback); - edge_cases_list_edge_cases_Widget_read(r, &out->extras); - edge_cases_map_string_edge_cases_Widget_read(r, &out->by_name); -} - -static inline bool edge_cases_edge_cases_WidgetHolder_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_WidgetHolder* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_edge_cases_WidgetHolder_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_edge_cases_WidgetHolder_free(out); - return false; - } - return true; -} - -static inline void edge_cases_edge_cases_WidgetHolder_free(edge_cases_edge_cases_WidgetHolder* v) { - edge_cases_edge_cases_Widget_destroy(v->primary); - edge_cases_opt_edge_cases_Widget_free(&v->fallback); - edge_cases_list_edge_cases_Widget_free(&v->extras); - edge_cases_map_string_edge_cases_Widget_free(&v->by_name); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_list_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Leaf* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_edge_cases_Leaf_write(w, &v->items[i]); - } -} - -static inline void edge_cases_list_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_list_edge_cases_Leaf* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_edge_cases_Leaf*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Leaf), 4); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - edge_cases_edge_cases_Leaf_read(r, &out->items[i]); - } -} - -static inline bool edge_cases_list_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Leaf* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_edge_cases_Leaf_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_edge_cases_Leaf_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_edge_cases_Leaf_free(edge_cases_list_edge_cases_Leaf* v) { - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_opt_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_edge_cases_Inner_write(w, &(*v)); - } -} - -static inline void edge_cases_opt_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_edge_cases_Inner* value = (edge_cases_edge_cases_Inner*)calloc(1, sizeof(edge_cases_edge_cases_Inner)); - if (value == NULL) { - r->failed = true; - return; - } - edge_cases_edge_cases_Inner_read(r, &(*value)); - *out = value; -} - -static inline bool edge_cases_opt_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_edge_cases_Inner_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_edge_cases_Inner_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_edge_cases_Inner_free(edge_cases_edge_cases_Inner** v) { - if (*v == NULL) { - return; - } - edge_cases_edge_cases_Inner_free(&(**v)); - free(*v); - *v = NULL; -} - -static inline void edge_cases_map_string_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Inner* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_string(w, v->keys[i]); - edge_cases_edge_cases_Inner_write(w, &v->values[i]); - } -} - -static inline void edge_cases_map_string_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Inner* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->keys = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 12); - out->values = (edge_cases_edge_cases_Inner*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Inner), 0); - if (out->keys == NULL || out->values == NULL) { - free(out->keys); - free(out->values); - out->keys = NULL; - out->values = NULL; - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->keys[i] = edge_cases_reader_get_string(r); - edge_cases_edge_cases_Inner_read(r, &out->values[i]); - } -} - -static inline bool edge_cases_map_string_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Inner* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_map_string_edge_cases_Inner_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_map_string_edge_cases_Inner_free(out); - return false; - } - return true; -} - -static inline void edge_cases_map_string_edge_cases_Inner_free(edge_cases_map_string_edge_cases_Inner* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_str_free(&v->keys[i]); - edge_cases_edge_cases_Inner_free(&v->values[i]); - } - free(v->keys); - free(v->values); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_list_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Mode* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_i32(w, (int32_t)v->items[i]); - } -} - -static inline void edge_cases_list_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_list_edge_cases_Mode* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_edge_cases_Mode*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Mode), 4); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->items[i] = (edge_cases_edge_cases_Mode)edge_cases_reader_get_i32(r); - } -} - -static inline bool edge_cases_list_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Mode* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_edge_cases_Mode_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_edge_cases_Mode_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_edge_cases_Mode_free(edge_cases_list_edge_cases_Mode* v) { - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_list_i32_write(edge_cases_writer* w, const edge_cases_list_i32* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_i32(w, v->items[i]); - } -} - -static inline void edge_cases_list_i32_read(edge_cases_reader* r, edge_cases_list_i32* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (int32_t*)edge_cases_reader_alloc(r, n, sizeof(int32_t), 4); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->items[i] = edge_cases_reader_get_i32(r); - } -} - -static inline bool edge_cases_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_i32* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_i32_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_i32_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_i32_free(edge_cases_list_i32* v) { - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_list_list_i32_write(edge_cases_writer* w, const edge_cases_list_list_i32* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_list_i32_write(w, &v->items[i]); - } -} - -static inline void edge_cases_list_list_i32_read(edge_cases_reader* r, edge_cases_list_list_i32* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_list_i32*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_list_i32), 4); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - edge_cases_list_i32_read(r, &out->items[i]); - } -} - -static inline bool edge_cases_list_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_list_i32* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_list_i32_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_list_i32_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_list_i32_free(edge_cases_list_list_i32* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_list_i32_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_opt_i64_write(edge_cases_writer* w, const int64_t* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_writer_put_i64(w, (*v)); - } -} - -static inline void edge_cases_opt_i64_read(edge_cases_reader* r, int64_t** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - int64_t* value = (int64_t*)calloc(1, sizeof(int64_t)); - if (value == NULL) { - r->failed = true; - return; - } - (*value) = edge_cases_reader_get_i64(r); - *out = value; -} - -static inline bool edge_cases_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_i64_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_i64_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_i64_free(int64_t** v) { - if (*v == NULL) { - return; - } - free(*v); - *v = NULL; -} - -static inline void edge_cases_list_opt_i64_write(edge_cases_writer* w, const edge_cases_list_opt_i64* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_opt_i64_write(w, v->items[i]); - } -} - -static inline void edge_cases_list_opt_i64_read(edge_cases_reader* r, edge_cases_list_opt_i64* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (int64_t**)edge_cases_reader_alloc(r, n, sizeof(int64_t*), 1); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - edge_cases_opt_i64_read(r, &out->items[i]); - } -} - -static inline bool edge_cases_list_opt_i64_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_i64* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_opt_i64_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_opt_i64_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_opt_i64_free(edge_cases_list_opt_i64* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_opt_i64_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_opt_bytes_write(edge_cases_writer* w, const edge_cases_bytes* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_writer_put_bytes(w, (*v)); - } -} - -static inline void edge_cases_opt_bytes_read(edge_cases_reader* r, edge_cases_bytes** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_bytes* value = (edge_cases_bytes*)calloc(1, sizeof(edge_cases_bytes)); - if (value == NULL) { - r->failed = true; - return; - } - (*value) = edge_cases_reader_get_bytes(r); - *out = value; -} - -static inline bool edge_cases_opt_bytes_decode(const uint8_t* ptr, size_t len, edge_cases_bytes** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_bytes_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_bytes_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_bytes_free(edge_cases_bytes** v) { - if (*v == NULL) { - return; - } - edge_cases_bytes_free(&(**v)); - free(*v); - *v = NULL; -} - -static inline void edge_cases_opt_string_write(edge_cases_writer* w, const edge_cases_str* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_writer_put_string(w, (*v)); - } -} - -static inline void edge_cases_opt_string_read(edge_cases_reader* r, edge_cases_str** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_str* value = (edge_cases_str*)calloc(1, sizeof(edge_cases_str)); - if (value == NULL) { - r->failed = true; - return; - } - (*value) = edge_cases_reader_get_string(r); - *out = value; -} - -static inline bool edge_cases_opt_string_decode(const uint8_t* ptr, size_t len, edge_cases_str** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_string_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_string_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_string_free(edge_cases_str** v) { - if (*v == NULL) { - return; - } - edge_cases_str_free(&(**v)); - free(*v); - *v = NULL; -} - -static inline void edge_cases_map_i64_string_write(edge_cases_writer* w, const edge_cases_map_i64_string* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_i64(w, v->keys[i]); - edge_cases_writer_put_string(w, v->values[i]); - } -} - -static inline void edge_cases_map_i64_string_read(edge_cases_reader* r, edge_cases_map_i64_string* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->keys = (int64_t*)edge_cases_reader_alloc(r, n, sizeof(int64_t), 12); - out->values = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 0); - if (out->keys == NULL || out->values == NULL) { - free(out->keys); - free(out->values); - out->keys = NULL; - out->values = NULL; - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->keys[i] = edge_cases_reader_get_i64(r); - out->values[i] = edge_cases_reader_get_string(r); - } -} - -static inline bool edge_cases_map_i64_string_decode(const uint8_t* ptr, size_t len, edge_cases_map_i64_string* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_map_i64_string_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_map_i64_string_free(out); - return false; - } - return true; -} - -static inline void edge_cases_map_i64_string_free(edge_cases_map_i64_string* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_str_free(&v->values[i]); - } - free(v->keys); - free(v->values); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_edge_cases_Widget* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v)); - } -} - -static inline void edge_cases_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_edge_cases_Widget** out) { - *out = NULL; - if (edge_cases_reader_get_bool(r)) { - *out = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); - } -} - -static inline bool edge_cases_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Widget** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_edge_cases_Widget_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_edge_cases_Widget_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_edge_cases_Widget_free(edge_cases_edge_cases_Widget** v) { - edge_cases_edge_cases_Widget_destroy(*v); - *v = NULL; -} - -static inline void edge_cases_list_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Widget* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v->items[i])); - } -} - -static inline void edge_cases_list_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_edge_cases_Widget* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_edge_cases_Widget**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Widget*), 8); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->items[i] = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); - } -} - -static inline bool edge_cases_list_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Widget* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_edge_cases_Widget_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_edge_cases_Widget_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_edge_cases_Widget_free(edge_cases_list_edge_cases_Widget* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_edge_cases_Widget_destroy(v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_map_string_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Widget* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_string(w, v->keys[i]); - edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v->values[i])); - } -} - -static inline void edge_cases_map_string_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Widget* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->keys = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 12); - out->values = (edge_cases_edge_cases_Widget**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Widget*), 0); - if (out->keys == NULL || out->values == NULL) { - free(out->keys); - free(out->values); - out->keys = NULL; - out->values = NULL; - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->keys[i] = edge_cases_reader_get_string(r); - out->values[i] = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); - } -} - -static inline bool edge_cases_map_string_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Widget* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_map_string_edge_cases_Widget_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_map_string_edge_cases_Widget_free(out); - return false; - } - return true; -} - -static inline void edge_cases_map_string_edge_cases_Widget_free(edge_cases_map_string_edge_cases_Widget* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_str_free(&v->keys[i]); - edge_cases_edge_cases_Widget_destroy(v->values[i]); - } - free(v->keys); - free(v->values); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_writer_put_string(w, v->items[i]); - } -} - -static inline void edge_cases_list_string_read(edge_cases_reader* r, edge_cases_list_string* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 4); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - out->items[i] = edge_cases_reader_get_string(r); - } -} - -static inline bool edge_cases_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_string_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_string_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_string_free(edge_cases_list_string* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_str_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_opt_i32_write(edge_cases_writer* w, const int32_t* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_writer_put_i32(w, (*v)); - } -} - -static inline void edge_cases_opt_i32_read(edge_cases_reader* r, int32_t** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - int32_t* value = (int32_t*)calloc(1, sizeof(int32_t)); - if (value == NULL) { - r->failed = true; - return; - } - (*value) = edge_cases_reader_get_i32(r); - *out = value; -} - -static inline bool edge_cases_opt_i32_decode(const uint8_t* ptr, size_t len, int32_t** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_i32_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_i32_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_i32_free(int32_t** v) { - if (*v == NULL) { - return; - } - free(*v); - *v = NULL; -} - -static inline void edge_cases_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_edge_cases_Outer_write(w, &(*v)); - } -} - -static inline void edge_cases_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_edge_cases_Outer* value = (edge_cases_edge_cases_Outer*)calloc(1, sizeof(edge_cases_edge_cases_Outer)); - if (value == NULL) { - r->failed = true; - return; - } - edge_cases_edge_cases_Outer_read(r, &(*value)); - *out = value; -} - -static inline bool edge_cases_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_edge_cases_Outer_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_edge_cases_Outer_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_edge_cases_Outer_free(edge_cases_edge_cases_Outer** v) { - if (*v == NULL) { - return; - } - edge_cases_edge_cases_Outer_free(&(**v)); - free(*v); - *v = NULL; -} - -static inline void edge_cases_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_opt_edge_cases_Outer_write(w, v->items[i]); - } -} - -static inline void edge_cases_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_edge_cases_Outer**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Outer*), 1); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - edge_cases_opt_edge_cases_Outer_read(r, &out->items[i]); - } -} - -static inline bool edge_cases_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_opt_edge_cases_Outer_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_opt_edge_cases_Outer_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_opt_edge_cases_Outer_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_opt_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_list_opt_edge_cases_Outer_write(w, &(*v)); - } -} - -static inline void edge_cases_opt_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_list_opt_edge_cases_Outer* value = (edge_cases_list_opt_edge_cases_Outer*)calloc(1, sizeof(edge_cases_list_opt_edge_cases_Outer)); - if (value == NULL) { - r->failed = true; - return; - } - edge_cases_list_opt_edge_cases_Outer_read(r, &(*value)); - *out = value; -} - -static inline bool edge_cases_opt_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_list_opt_edge_cases_Outer_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_list_opt_edge_cases_Outer_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer** v) { - if (*v == NULL) { - return; - } - edge_cases_list_opt_edge_cases_Outer_free(&(**v)); - free(*v); - *v = NULL; -} - -static inline void edge_cases_opt_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_list_string_write(w, &(*v)); - } -} - -static inline void edge_cases_opt_list_string_read(edge_cases_reader* r, edge_cases_list_string** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_list_string* value = (edge_cases_list_string*)calloc(1, sizeof(edge_cases_list_string)); - if (value == NULL) { - r->failed = true; - return; - } - edge_cases_list_string_read(r, &(*value)); - *out = value; -} - -static inline bool edge_cases_opt_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_list_string_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_list_string_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_list_string_free(edge_cases_list_string** v) { - if (*v == NULL) { - return; - } - edge_cases_list_string_free(&(**v)); - free(*v); - *v = NULL; -} - -static inline void edge_cases_opt_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_edge_cases_Mode* v) { - edge_cases_writer_put_bool(w, v != NULL); - if (v != NULL) { - edge_cases_writer_put_i32(w, (int32_t)(*v)); - } -} - -static inline void edge_cases_opt_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_edge_cases_Mode** out) { - *out = NULL; - if (!edge_cases_reader_get_bool(r)) { - return; - } - edge_cases_edge_cases_Mode* value = (edge_cases_edge_cases_Mode*)calloc(1, sizeof(edge_cases_edge_cases_Mode)); - if (value == NULL) { - r->failed = true; - return; - } - (*value) = (edge_cases_edge_cases_Mode)edge_cases_reader_get_i32(r); - *out = value; -} - -static inline bool edge_cases_opt_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Mode** out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_opt_edge_cases_Mode_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_opt_edge_cases_Mode_free(out); - return false; - } - return true; -} - -static inline void edge_cases_opt_edge_cases_Mode_free(edge_cases_edge_cases_Mode** v) { - if (*v == NULL) { - return; - } - free(*v); - *v = NULL; -} - -static inline void edge_cases_list_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Widget* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_opt_edge_cases_Widget_write(w, v->items[i]); - } -} - -static inline void edge_cases_list_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Widget* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_edge_cases_Widget**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Widget*), 1); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - edge_cases_opt_edge_cases_Widget_read(r, &out->items[i]); - } -} - -static inline bool edge_cases_list_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Widget* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_opt_edge_cases_Widget_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_opt_edge_cases_Widget_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_opt_edge_cases_Widget_free(edge_cases_list_opt_edge_cases_Widget* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_opt_edge_cases_Widget_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -static inline void edge_cases_list_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Outer* v) { - edge_cases_writer_put_len(w, v->len); - for (size_t i = 0; i < v->len; i++) { - edge_cases_edge_cases_Outer_write(w, &v->items[i]); - } -} - -static inline void edge_cases_list_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_edge_cases_Outer* out) { - memset(out, 0, sizeof *out); - size_t n = edge_cases_reader_get_u32(r); - out->items = (edge_cases_edge_cases_Outer*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Outer), 33); - if (out->items == NULL) { - return; - } - out->len = n; - for (size_t i = 0; i < n; i++) { - edge_cases_edge_cases_Outer_read(r, &out->items[i]); - } -} - -static inline bool edge_cases_list_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Outer* out) { - edge_cases_reader r = edge_cases_reader_of(ptr, len); - edge_cases_list_edge_cases_Outer_read(&r, out); - if (!edge_cases_reader_finish(&r)) { - edge_cases_list_edge_cases_Outer_free(out); - return false; - } - return true; -} - -static inline void edge_cases_list_edge_cases_Outer_free(edge_cases_list_edge_cases_Outer* v) { - for (size_t i = 0; i < v->len; i++) { - edge_cases_edge_cases_Outer_free(&v->items[i]); - } - free(v->items); - memset(v, 0, sizeof *v); -} - -#ifdef __cplusplus -} -#endif - -#endif // EDGE_CASES_BUFFER_H - -// END edge_cases_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_h.snap deleted file mode 100644 index fa3c9950..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_h.snap +++ /dev/null @@ -1,337 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef EDGE_CASES_H -#define EDGE_CASES_H - -#include -#include -#include - -#ifndef EDGE_CASES_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef EDGE_CASES_BUILD -# define EDGE_CASES_API __declspec(dllexport) -# else -# define EDGE_CASES_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define EDGE_CASES_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define EDGE_CASES_API __attribute__((visibility("default"))) -# else -# define EDGE_CASES_API -# endif -#endif - -#ifndef EDGE_CASES_DEPRECATED -# if defined(_MSC_VER) -# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define EDGE_CASES_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* The C ABI revision this header was generated against. The producer - exports edge_cases_abi_version() so a consumer can refuse to load a - library built for a different revision. */ -#define EDGE_CASES_ABI_VERSION 4u -EDGE_CASES_API uint32_t edge_cases_abi_version(void); - -/* Error slot written by every fallible call. `message` is NUL-terminated - UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields - serialized in the value buffer format (null when the code declares no - fields). Both are released by edge_cases_error_clear. Positive codes are - the API's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation failed, -5 cancelled. */ -typedef struct edge_cases_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} edge_cases_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface implementations call this to report a failure without - allocating with a foreign allocator. */ -EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); -/* Replace `err`'s payload with a producer-owned copy of `len` bytes: the - fields of a declared error code, which a callback method that throws - attaches after edge_cases_error_set. */ -EDGE_CASES_API void edge_cases_error_set_payload(edge_cases_error* err, const uint8_t* ptr, size_t len); -EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); - -/* Async completions receive a heap-boxed error the consumer owns; - edge_cases_error_free releases the message, the payload, and the box. - NULL is a no-op. */ -EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); - -/* Allocate a zero-filled run of `len` bytes (NULL for 0). A callback - method returns a string, bytes, or buffer as such a run, which the - producer adopts; any other run is released with edge_cases_free_bytes. */ -EDGE_CASES_API uint8_t* edge_cases_alloc(size_t len); -/* Release a string, bytes, or value buffer the producer returned, or a - run from edge_cases_alloc, with its exact length. NULL or 0 is a no-op. */ -EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); - -/* Cancellation for `cancellable` async functions. A token starts with - one reference owned by the consumer; the producer takes its own for - each call it is passed to, so the consumer may cancel and destroy its - reference at any time. A cancelled call completes with code -5. */ -typedef struct edge_cases_cancel_token edge_cases_cancel_token; -EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); -EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); -EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); -EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); - -/* Live-allocation counters for leak checks in tests: 0 objects, - 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. - Kind -1 is 1 when the producer counts at all (its `leak-check` - feature) and 0 when every counter reads 0 regardless. */ -EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); - -/* One declaration's fingerprint in a module's contract table: FNV-1a 64 - of its dotted path and of its canonical signature. */ -typedef struct edge_cases_contract_entry { - uint64_t id; - uint64_t hash; -} edge_cases_contract_entry; - -/* The contract of module `edge_cases`: one entry per declaration, sorted by - id. The producer's table comes from the function below; a consumer - checks each entry it was generated with against it once, before any - other call (producer entries it doesn't know are fine). */ -EDGE_CASES_API const edge_cases_contract_entry* edge_cases_edge_cases_contract(size_t* out_len); -#define EDGE_CASES_EDGE_CASES_CONTRACT_LEN 47 -#define EDGE_CASES_EDGE_CASES_CONTRACT { \ - {0x056a86511785ba2bull, 0x5cba3f16d57c6cdbull}, /* edge_cases.make_widget */ \ - {0x07bfa9ecb2b720d2ull, 0x3813b3f57c64bf35ull}, /* edge_cases.EdgeErrors */ \ - {0x0aaa11867e37f991ull, 0xf090dd52146ebdf0ull}, /* edge_cases.Widget.refresh */ \ - {0x0f9701493898ae4cull, 0x6944f47a275c96e8ull}, /* edge_cases.Inner */ \ - {0x1226b1adac179f7dull, 0x0e95a00ab0bef637ull}, /* edge_cases.async_keywords */ \ - {0x16d36bef1541136cull, 0x5ea7021ec324298full}, /* edge_cases.find_widget */ \ - {0x1b7324c55e980febull, 0xabf87fe372358f42ull}, /* edge_cases.Widget.old_name */ \ - {0x1cbd731467c2bab8ull, 0x4915b9989c2c9074ull}, /* edge_cases.Vault */ \ - {0x2c2a6de67d894b0aull, 0x50a4496b1e7191caull}, /* edge_cases.Vault.open */ \ - {0x322b4cfcd2d6861cull, 0xca7527a379ae8465ull}, /* edge_cases.Keywords */ \ - {0x3f8e23a5f32906f4ull, 0x8d4c908d796f130dull}, /* edge_cases.Leaf */ \ - {0x42ab8a685457b97cull, 0xe5ce30500cadb32dull}, /* edge_cases.Vault.handle */ \ - {0x4a68540e2836d3d0ull, 0xa20e44c71e420ed2ull}, /* edge_cases.mode_stream */ \ - {0x4e030580ac0b111dull, 0xb68b7a18d910e175ull}, /* edge_cases.legacy_value */ \ - {0x4f5e4e5291864ef3ull, 0x55a1a88b21a08f8bull}, /* edge_cases.Legacy */ \ - {0x4f84b06171b4213dull, 0xbaedcfdebb6a2820ull}, /* edge_cases.scalar_stream */ \ - {0x5067a5ff2d9ef910ull, 0xbfc79b18fc4714fcull}, /* edge_cases.Widget */ \ - {0x59aff159112fbf5bull, 0x18e4def57a8cfa49ull}, /* edge_cases.optional_params */ \ - {0x5daf65b98fa77c48ull, 0xc817a8cb202e59c2ull}, /* edge_cases.keywords_roundtrip */ \ - {0x5de8a5c9726c2c5aull, 0x7c50b5b65f7a896dull}, /* edge_cases.register_batch */ \ - {0x618a620309994c3cull, 0x826ad0b1d208d723ull}, /* edge_cases.async_bytes */ \ - {0x648285cdba0a7b8eull, 0xcf6e80e783a4d8beull}, /* edge_cases.WidgetHolder */ \ - {0x649c78079824b10dull, 0x5b12dd26e0634b69ull}, /* edge_cases.async_widget */ \ - {0x7da7a7357d122a37ull, 0xf544e025ffaa3d20ull}, /* edge_cases.async_void */ \ - {0x7e4015bc3a472e2dull, 0xfe22291d90b82e4aull}, /* edge_cases.widget_stream */ \ - {0x805276e5d81fdba7ull, 0xd9b64e360bd03b2bull}, /* edge_cases.Outer */ \ - {0x80e1183326542df4ull, 0xe8a57e12cc6ed276ull}, /* edge_cases.select */ \ - {0x923ce5e9bcd769a1ull, 0x0b973f5dc07dda8bull}, /* edge_cases.Vault.to_string */ \ - {0x98ca8c01f5239404ull, 0xfe11c93eb6b303efull}, /* edge_cases.Widget.rename */ \ - {0xa13e22e98ea278d4ull, 0xf1fae215d7387800ull}, /* edge_cases.widgets_everywhere */ \ - {0xa84a8c9bf2a881e1ull, 0xbe1dc840bf221996ull}, /* edge_cases.async_optional */ \ - {0xad958c478fbf205dull, 0xe386b0716d24f19bull}, /* edge_cases.Retired */ \ - {0xb2fc3964ef56ab39ull, 0x73c325c8bc351236ull}, /* edge_cases.async_list */ \ - {0xb3403af521cf0006ull, 0x6abc30d48206a3baull}, /* edge_cases.Widget.close */ \ - {0xb794304a22a406b8ull, 0xf6fb98711c516105ull}, /* edge_cases.async_struct */ \ - {0xba3936106a49c47dull, 0xb7025f2fadd6288eull}, /* edge_cases.Vault.peek */ \ - {0xc155b0c3cfb164f4ull, 0xc777ae57d92ebc47ull}, /* edge_cases.noop */ \ - {0xcdc18607777019ddull, 0xb81a31e5c20aa7a8ull}, /* edge_cases.inspect_widget */ \ - {0xd164a82aeb64b8ccull, 0xded02c68005c4737ull}, /* edge_cases.nested_optional */ \ - {0xd978ccad2468038full, 0x30c39c65d6235153ull}, /* edge_cases.Mode */ \ - {0xdbe9d947431c9124ull, 0x39537fb848fdf980ull}, /* edge_cases.BatchListener */ \ - {0xeae8b7862c08bb07ull, 0xfe12eeaa830452eaull}, /* edge_cases.Keyword */ \ - {0xf1e155a05ae262b8ull, 0xb575eb818a423d42ull}, /* edge_cases.async_enum */ \ - {0xf22e210bdbce7343ull, 0x2a5e0f801bc8c768ull}, /* edge_cases.Widget.dispose */ \ - {0xf3c7cf675563ee6cull, 0x0ecffeef399facc8ull}, /* edge_cases.Vault.probe */ \ - {0xfd34bc67acac379eull, 0x576caa14f3c0901aull}, /* edge_cases.Widget.fetch */ \ - {0xff2e9d2bff941786ull, 0x7e96764ef1f5fbaaull}, /* edge_cases.string_stream */ \ -} -/* The id of the first expected entry the loaded library lacks or - declares differently, or 0 when these bindings match it. */ -static inline uint64_t edge_cases_edge_cases_contract_check(void) { - static const edge_cases_contract_entry expected[] = EDGE_CASES_EDGE_CASES_CONTRACT; - size_t len = 0; - const edge_cases_contract_entry* table = edge_cases_edge_cases_contract(&len); - for (size_t i = 0; i < EDGE_CASES_EDGE_CASES_CONTRACT_LEN; i++) { - size_t lo = 0, hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) lo = mid + 1; else hi = mid; - } - if (lo == len || table[lo].id != expected[i].id || table[lo].hash != expected[i].hash) { - return expected[i].id; - } - } - return 0; -} - -/* - * Strings, bytes, and value buffers cross as (ptr, len) pairs. A - * parameter named "v" expands to a view borrowed for the call: - * const uint8_t* v_ptr, size_t v_len - * Strings are UTF-8 without a NUL terminator; NULL with length 0 is - * the empty value. A returned string, bytes, or buffer is - * `const uint8_t*` with a trailing `size_t* out_len`, owned by the - * caller and released with edge_cases_free_bytes. - * Records, rich enums, lists, maps, and optionals are serialized - * in the little-endian value buffer format; when generated, - * edge_cases_buffer.h declares a C struct and codec for each. - */ - -/** Deprecated enum with non-contiguous values and keyword-like variants */ -typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; -typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; -/** Error codes reported by throwing functions in the `edge_cases` module tree. */ -typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; -typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; -typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; -typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; -typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; -typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; -typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; -/** - * Callback interface carrying composite payloads and objects - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable, with `size` set to `sizeof` the vtable and `flags` to 0 (the producer rejects a smaller vtable). The producer may call any entry from any thread until it calls `free(ctx)` exactly once, also from any thread. A method reports failure with `error_set(out_err, ...)`; a string, bytes, or buffer return is a run the consumer allocates with `alloc` and writes to `out_ptr` and `out_len`, which the producer adopts. - */ -typedef struct edge_cases_edge_cases_BatchListener_vtable { - uint32_t size; - uint32_t flags; - void (*free)(void* ctx); - /** Composite payloads cross as value buffers */ - void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); - /** Objects cross as strong references the consumer must release */ - void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); - void (*on_tick)(void* ctx, edge_cases_error* out_err); - /** Scalar return travels back through the vtable */ - bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); - /** Enum return travels back as its integer discriminant */ - edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); -} edge_cases_edge_cases_BatchListener_vtable; -/** - * Callback interface whose name and methods are keywords - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable, with `size` set to `sizeof` the vtable and `flags` to 0 (the producer rejects a smaller vtable). The producer may call any entry from any thread until it calls `free(ctx)` exactly once, also from any thread. A method reports failure with `error_set(out_err, ...)`; a string, bytes, or buffer return is a run the consumer allocates with `alloc` and writes to `out_ptr` and `out_len`, which the producer adopts. - */ -typedef struct edge_cases_edge_cases_Keyword_vtable { - uint32_t size; - uint32_t flags; - void (*free)(void* ctx); - void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); - int32_t (*import)(void* ctx, edge_cases_error* out_err); -} edge_cases_edge_cases_Keyword_vtable; -typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); -typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); -typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); -typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); - -// Module: edge_cases -/** Interface with no constructor; instances come from make_widget */ -/** Void method that can fail */ -EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); -/** Async method with a struct result */ -EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); -EDGE_CASES_DEPRECATED("Use rename instead") -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); -/** Spelled like the release method of several wrappers */ -EDGE_CASES_API bool edge_cases_edge_cases_Widget_close(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); -/** Spelled like the release method of the Dart and .NET wrappers */ -EDGE_CASES_API void edge_cases_edge_cases_Widget_dispose(const edge_cases_edge_cases_Widget* self, bool force, edge_cases_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); - -/** Deprecated interface with a throwing constructor */ -/** Fails when the vault is busy */ -EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); -/** Throwing static */ -EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); -/** Static spelled like an inherited object member */ -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Vault_to_string(size_t* out_len, edge_cases_error* out_err); -EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); -/** Spelled like the handle accessor of several wrappers */ -EDGE_CASES_API int64_t edge_cases_edge_cases_Vault_handle(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); - -/** No parameters and no result */ -EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); -/** Function named after a keyword whose parameters are all keywords */ -EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); -/** Optional list of optional structs, the deepest legal nesting */ -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); -/** Two callback interface parameters on one function */ -EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); -/** Objects in every buffered position */ -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); -/** Iterator of objects */ -EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); -EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); -EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); -/** Async returning an object */ -EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); -/** Factory for an interface with no constructor */ -EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); -/** Optional interface return */ -EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); -/** Interface parameter, including an optional one */ -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); -/** Iterator of strings */ -EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); -EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); -EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); -/** Iterator of scalars */ -EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); -EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); -EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); -/** Iterator of enums */ -EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); -EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); -EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); -/** Async with no result */ -EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); -EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); -EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); -EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); -EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); -EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); -EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); -EDGE_CASES_DEPRECATED("Use Mode-based APIs") -EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // EDGE_CASES_H - -// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_buffer_h.snap index 390ef0a9..bbea9746 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_buffer_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_buffer_h.snap @@ -26,6 +26,8 @@ extern "C" { * followed by the value, lists as a `uint32_t` count followed by the * elements, maps as a `uint32_t` count followed by alternating keys and * values, and objects as a `uint64_t` token holding one strong reference. + * Every count and length is a `uint32_t`, so a writer fails on a string, + * bytes, list, or map past UINT32_MAX elements. * ------------------------------------------------------------------------- */ /* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. @@ -335,9 +337,64 @@ static inline uint8_t* kitchen_sink_reader_get_run(kitchen_sink_reader* r, size_ return copy; } +/* Whether the `n` bytes at `s` are well-formed UTF-8: no stray or missing + continuation bytes, overlong forms, surrogates, or code points past + U+10FFFF. */ +static inline bool kitchen_sink_reader_utf8(const uint8_t* s, size_t n) { + size_t i = 0; + while (i < n) { + uint8_t c = s[i]; + if (c < 0x80) { + i++; + continue; + } + size_t len; + uint32_t cp; + uint32_t min; + if ((c & 0xE0) == 0xC0) { + len = 2; + cp = c & 0x1F; + min = 0x80; + } else if ((c & 0xF0) == 0xE0) { + len = 3; + cp = c & 0x0F; + min = 0x800; + } else if ((c & 0xF8) == 0xF0) { + len = 4; + cp = c & 0x07; + min = 0x10000; + } else { + return false; + } + if (n - i < len) { + return false; + } + for (size_t k = 1; k < len; k++) { + uint8_t cc = s[i + k]; + if ((cc & 0xC0) != 0x80) { + return false; + } + cp = (cp << 6) | (uint32_t)(cc & 0x3F); + } + if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + return false; + } + i += len; + } + return true; +} + +/* A string must be well-formed UTF-8: anything else fails the reader (the + decoder then returns false, the buffer analog of runtime code -3). */ static inline kitchen_sink_str kitchen_sink_reader_get_string(kitchen_sink_reader* r) { kitchen_sink_str v; v.ptr = (const char*)kitchen_sink_reader_get_run(r, &v.len); + if (v.ptr != NULL && !kitchen_sink_reader_utf8((const uint8_t*)v.ptr, v.len)) { + free((void*)v.ptr); + v.ptr = NULL; + v.len = 0; + r->failed = true; + } return v; } @@ -367,10 +424,11 @@ static inline void* kitchen_sink_reader_get_object(kitchen_sink_reader* r) { typedef struct kitchen_sink_shared_Token kitchen_sink_shared_Token; typedef struct kitchen_sink_kitchen_Item kitchen_sink_kitchen_Item; +typedef struct kitchen_sink_kitchen_PantryError_OutOfStock_payload kitchen_sink_kitchen_PantryError_OutOfStock_payload; typedef struct kitchen_sink_list_string kitchen_sink_list_string; typedef struct kitchen_sink_map_string_string kitchen_sink_map_string_string; -typedef struct kitchen_sink_list_kitchen_Gadget kitchen_sink_list_kitchen_Gadget; -typedef struct kitchen_sink_list_kitchen_Item kitchen_sink_list_kitchen_Item; +typedef struct kitchen_sink_list_Gadget kitchen_sink_list_Gadget; +typedef struct kitchen_sink_list_Item kitchen_sink_list_Item; typedef struct kitchen_sink_map_string_i32 kitchen_sink_map_string_i32; /** `[string]` */ @@ -387,13 +445,13 @@ struct kitchen_sink_map_string_string { }; /** `[Gadget]` */ -struct kitchen_sink_list_kitchen_Gadget { +struct kitchen_sink_list_Gadget { kitchen_sink_kitchen_Gadget** items; size_t len; }; /** `[Item]` */ -struct kitchen_sink_list_kitchen_Item { +struct kitchen_sink_list_Item { kitchen_sink_kitchen_Item* items; size_t len; }; @@ -435,6 +493,15 @@ struct kitchen_sink_kitchen_Item { kitchen_sink_kitchen_Gadget* gadget; }; +/** + * The fields of a `kitchen_sink_kitchen_PantryError_OutOfStock` error + * (`OutOfStockError` in the language bindings), which it carries in + * `payload_ptr`/`payload_len`. + */ +struct kitchen_sink_kitchen_PantryError_OutOfStock_payload { + kitchen_sink_str item; +}; + /* --------------------------------------------------------------------------- * Codecs. Each value type T above, and each optional shape, has four * functions: @@ -461,6 +528,10 @@ static inline void kitchen_sink_kitchen_Item_write(kitchen_sink_writer* w, const static inline void kitchen_sink_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item* out); static inline bool kitchen_sink_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item* out); static inline void kitchen_sink_kitchen_Item_free(kitchen_sink_kitchen_Item* v); +static inline void kitchen_sink_kitchen_PantryError_OutOfStock_payload_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_PantryError_OutOfStock_payload* v); +static inline void kitchen_sink_kitchen_PantryError_OutOfStock_payload_read(kitchen_sink_reader* r, kitchen_sink_kitchen_PantryError_OutOfStock_payload* out); +static inline bool kitchen_sink_kitchen_PantryError_OutOfStock_payload_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_PantryError_OutOfStock_payload* out); +static inline void kitchen_sink_kitchen_PantryError_OutOfStock_payload_free(kitchen_sink_kitchen_PantryError_OutOfStock_payload* v); static inline void kitchen_sink_list_string_write(kitchen_sink_writer* w, const kitchen_sink_list_string* v); static inline void kitchen_sink_list_string_read(kitchen_sink_reader* r, kitchen_sink_list_string* out); static inline bool kitchen_sink_list_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_string* out); @@ -473,26 +544,30 @@ static inline void kitchen_sink_opt_i64_write(kitchen_sink_writer* w, const int6 static inline void kitchen_sink_opt_i64_read(kitchen_sink_reader* r, int64_t** out); static inline bool kitchen_sink_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out); static inline void kitchen_sink_opt_i64_free(int64_t** v); -static inline void kitchen_sink_opt_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Gadget* v); -static inline void kitchen_sink_opt_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Gadget** out); -static inline bool kitchen_sink_opt_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Gadget** out); -static inline void kitchen_sink_opt_kitchen_Gadget_free(kitchen_sink_kitchen_Gadget** v); -static inline void kitchen_sink_list_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Gadget* v); -static inline void kitchen_sink_list_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Gadget* out); -static inline bool kitchen_sink_list_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Gadget* out); -static inline void kitchen_sink_list_kitchen_Gadget_free(kitchen_sink_list_kitchen_Gadget* v); -static inline void kitchen_sink_opt_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v); -static inline void kitchen_sink_opt_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item** out); -static inline bool kitchen_sink_opt_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item** out); -static inline void kitchen_sink_opt_kitchen_Item_free(kitchen_sink_kitchen_Item** v); -static inline void kitchen_sink_list_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Item* v); -static inline void kitchen_sink_list_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Item* out); -static inline bool kitchen_sink_list_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Item* out); -static inline void kitchen_sink_list_kitchen_Item_free(kitchen_sink_list_kitchen_Item* v); +static inline void kitchen_sink_opt_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Gadget* v); +static inline void kitchen_sink_opt_Gadget_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Gadget** out); +static inline bool kitchen_sink_opt_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Gadget** out); +static inline void kitchen_sink_opt_Gadget_free(kitchen_sink_kitchen_Gadget** v); +static inline void kitchen_sink_list_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_list_Gadget* v); +static inline void kitchen_sink_list_Gadget_read(kitchen_sink_reader* r, kitchen_sink_list_Gadget* out); +static inline bool kitchen_sink_list_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_Gadget* out); +static inline void kitchen_sink_list_Gadget_free(kitchen_sink_list_Gadget* v); +static inline void kitchen_sink_opt_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v); +static inline void kitchen_sink_opt_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item** out); +static inline bool kitchen_sink_opt_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item** out); +static inline void kitchen_sink_opt_Item_free(kitchen_sink_kitchen_Item** v); +static inline void kitchen_sink_list_Item_write(kitchen_sink_writer* w, const kitchen_sink_list_Item* v); +static inline void kitchen_sink_list_Item_read(kitchen_sink_reader* r, kitchen_sink_list_Item* out); +static inline bool kitchen_sink_list_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_Item* out); +static inline void kitchen_sink_list_Item_free(kitchen_sink_list_Item* v); static inline void kitchen_sink_map_string_i32_write(kitchen_sink_writer* w, const kitchen_sink_map_string_i32* v); static inline void kitchen_sink_map_string_i32_read(kitchen_sink_reader* r, kitchen_sink_map_string_i32* out); static inline bool kitchen_sink_map_string_i32_decode(const uint8_t* ptr, size_t len, kitchen_sink_map_string_i32* out); static inline void kitchen_sink_map_string_i32_free(kitchen_sink_map_string_i32* v); +static inline void kitchen_sink_opt_string_write(kitchen_sink_writer* w, const kitchen_sink_str* v); +static inline void kitchen_sink_opt_string_read(kitchen_sink_reader* r, kitchen_sink_str** out); +static inline bool kitchen_sink_opt_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_str** out); +static inline void kitchen_sink_opt_string_free(kitchen_sink_str** v); static inline void kitchen_sink_shared_Token_write(kitchen_sink_writer* w, const kitchen_sink_shared_Token* v) { kitchen_sink_writer_put_i64(w, v->id); @@ -533,7 +608,7 @@ static inline void kitchen_sink_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_opt_i64_write(w, v->parent); kitchen_sink_shared_Token_write(w, &v->token); kitchen_sink_writer_put_i32(w, (int32_t)v->priority); - kitchen_sink_opt_kitchen_Gadget_write(w, v->gadget); + kitchen_sink_opt_Gadget_write(w, v->gadget); } static inline void kitchen_sink_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item* out) { @@ -550,7 +625,7 @@ static inline void kitchen_sink_kitchen_Item_read(kitchen_sink_reader* r, kitche kitchen_sink_opt_i64_read(r, &out->parent); kitchen_sink_shared_Token_read(r, &out->token); out->priority = (kitchen_sink_kitchen_Priority)kitchen_sink_reader_get_i32(r); - kitchen_sink_opt_kitchen_Gadget_read(r, &out->gadget); + kitchen_sink_opt_Gadget_read(r, &out->gadget); } static inline bool kitchen_sink_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item* out) { @@ -570,7 +645,31 @@ static inline void kitchen_sink_kitchen_Item_free(kitchen_sink_kitchen_Item* v) kitchen_sink_map_string_string_free(&v->attrs); kitchen_sink_opt_i64_free(&v->parent); kitchen_sink_shared_Token_free(&v->token); - kitchen_sink_opt_kitchen_Gadget_free(&v->gadget); + kitchen_sink_opt_Gadget_free(&v->gadget); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_kitchen_PantryError_OutOfStock_payload_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_PantryError_OutOfStock_payload* v) { + kitchen_sink_writer_put_string(w, v->item); +} + +static inline void kitchen_sink_kitchen_PantryError_OutOfStock_payload_read(kitchen_sink_reader* r, kitchen_sink_kitchen_PantryError_OutOfStock_payload* out) { + memset(out, 0, sizeof *out); + out->item = kitchen_sink_reader_get_string(r); +} + +static inline bool kitchen_sink_kitchen_PantryError_OutOfStock_payload_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_PantryError_OutOfStock_payload* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_kitchen_PantryError_OutOfStock_payload_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_kitchen_PantryError_OutOfStock_payload_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_kitchen_PantryError_OutOfStock_payload_free(kitchen_sink_kitchen_PantryError_OutOfStock_payload* v) { + kitchen_sink_str_free(&v->item); memset(v, 0, sizeof *v); } @@ -698,43 +797,43 @@ static inline void kitchen_sink_opt_i64_free(int64_t** v) { *v = NULL; } -static inline void kitchen_sink_opt_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Gadget* v) { +static inline void kitchen_sink_opt_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Gadget* v) { kitchen_sink_writer_put_bool(w, v != NULL); if (v != NULL) { kitchen_sink_writer_put_object(w, kitchen_sink_kitchen_Gadget_clone(v)); } } -static inline void kitchen_sink_opt_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Gadget** out) { +static inline void kitchen_sink_opt_Gadget_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Gadget** out) { *out = NULL; if (kitchen_sink_reader_get_bool(r)) { *out = (kitchen_sink_kitchen_Gadget*)kitchen_sink_reader_get_object(r); } } -static inline bool kitchen_sink_opt_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Gadget** out) { +static inline bool kitchen_sink_opt_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Gadget** out) { kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); - kitchen_sink_opt_kitchen_Gadget_read(&r, out); + kitchen_sink_opt_Gadget_read(&r, out); if (!kitchen_sink_reader_finish(&r)) { - kitchen_sink_opt_kitchen_Gadget_free(out); + kitchen_sink_opt_Gadget_free(out); return false; } return true; } -static inline void kitchen_sink_opt_kitchen_Gadget_free(kitchen_sink_kitchen_Gadget** v) { +static inline void kitchen_sink_opt_Gadget_free(kitchen_sink_kitchen_Gadget** v) { kitchen_sink_kitchen_Gadget_destroy(*v); *v = NULL; } -static inline void kitchen_sink_list_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Gadget* v) { +static inline void kitchen_sink_list_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_list_Gadget* v) { kitchen_sink_writer_put_len(w, v->len); for (size_t i = 0; i < v->len; i++) { kitchen_sink_writer_put_object(w, kitchen_sink_kitchen_Gadget_clone(v->items[i])); } } -static inline void kitchen_sink_list_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Gadget* out) { +static inline void kitchen_sink_list_Gadget_read(kitchen_sink_reader* r, kitchen_sink_list_Gadget* out) { memset(out, 0, sizeof *out); size_t n = kitchen_sink_reader_get_u32(r); out->items = (kitchen_sink_kitchen_Gadget**)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_kitchen_Gadget*), 8); @@ -747,17 +846,17 @@ static inline void kitchen_sink_list_kitchen_Gadget_read(kitchen_sink_reader* r, } } -static inline bool kitchen_sink_list_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Gadget* out) { +static inline bool kitchen_sink_list_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_Gadget* out) { kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); - kitchen_sink_list_kitchen_Gadget_read(&r, out); + kitchen_sink_list_Gadget_read(&r, out); if (!kitchen_sink_reader_finish(&r)) { - kitchen_sink_list_kitchen_Gadget_free(out); + kitchen_sink_list_Gadget_free(out); return false; } return true; } -static inline void kitchen_sink_list_kitchen_Gadget_free(kitchen_sink_list_kitchen_Gadget* v) { +static inline void kitchen_sink_list_Gadget_free(kitchen_sink_list_Gadget* v) { for (size_t i = 0; i < v->len; i++) { kitchen_sink_kitchen_Gadget_destroy(v->items[i]); } @@ -765,14 +864,14 @@ static inline void kitchen_sink_list_kitchen_Gadget_free(kitchen_sink_list_kitch memset(v, 0, sizeof *v); } -static inline void kitchen_sink_opt_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v) { +static inline void kitchen_sink_opt_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v) { kitchen_sink_writer_put_bool(w, v != NULL); if (v != NULL) { kitchen_sink_kitchen_Item_write(w, &(*v)); } } -static inline void kitchen_sink_opt_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item** out) { +static inline void kitchen_sink_opt_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item** out) { *out = NULL; if (!kitchen_sink_reader_get_bool(r)) { return; @@ -786,17 +885,17 @@ static inline void kitchen_sink_opt_kitchen_Item_read(kitchen_sink_reader* r, ki *out = value; } -static inline bool kitchen_sink_opt_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item** out) { +static inline bool kitchen_sink_opt_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item** out) { kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); - kitchen_sink_opt_kitchen_Item_read(&r, out); + kitchen_sink_opt_Item_read(&r, out); if (!kitchen_sink_reader_finish(&r)) { - kitchen_sink_opt_kitchen_Item_free(out); + kitchen_sink_opt_Item_free(out); return false; } return true; } -static inline void kitchen_sink_opt_kitchen_Item_free(kitchen_sink_kitchen_Item** v) { +static inline void kitchen_sink_opt_Item_free(kitchen_sink_kitchen_Item** v) { if (*v == NULL) { return; } @@ -805,14 +904,14 @@ static inline void kitchen_sink_opt_kitchen_Item_free(kitchen_sink_kitchen_Item* *v = NULL; } -static inline void kitchen_sink_list_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Item* v) { +static inline void kitchen_sink_list_Item_write(kitchen_sink_writer* w, const kitchen_sink_list_Item* v) { kitchen_sink_writer_put_len(w, v->len); for (size_t i = 0; i < v->len; i++) { kitchen_sink_kitchen_Item_write(w, &v->items[i]); } } -static inline void kitchen_sink_list_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Item* out) { +static inline void kitchen_sink_list_Item_read(kitchen_sink_reader* r, kitchen_sink_list_Item* out) { memset(out, 0, sizeof *out); size_t n = kitchen_sink_reader_get_u32(r); out->items = (kitchen_sink_kitchen_Item*)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_kitchen_Item), 59); @@ -825,17 +924,17 @@ static inline void kitchen_sink_list_kitchen_Item_read(kitchen_sink_reader* r, k } } -static inline bool kitchen_sink_list_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Item* out) { +static inline bool kitchen_sink_list_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_Item* out) { kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); - kitchen_sink_list_kitchen_Item_read(&r, out); + kitchen_sink_list_Item_read(&r, out); if (!kitchen_sink_reader_finish(&r)) { - kitchen_sink_list_kitchen_Item_free(out); + kitchen_sink_list_Item_free(out); return false; } return true; } -static inline void kitchen_sink_list_kitchen_Item_free(kitchen_sink_list_kitchen_Item* v) { +static inline void kitchen_sink_list_Item_free(kitchen_sink_list_Item* v) { for (size_t i = 0; i < v->len; i++) { kitchen_sink_kitchen_Item_free(&v->items[i]); } @@ -889,6 +988,46 @@ static inline void kitchen_sink_map_string_i32_free(kitchen_sink_map_string_i32* memset(v, 0, sizeof *v); } +static inline void kitchen_sink_opt_string_write(kitchen_sink_writer* w, const kitchen_sink_str* v) { + kitchen_sink_writer_put_bool(w, v != NULL); + if (v != NULL) { + kitchen_sink_writer_put_string(w, (*v)); + } +} + +static inline void kitchen_sink_opt_string_read(kitchen_sink_reader* r, kitchen_sink_str** out) { + *out = NULL; + if (!kitchen_sink_reader_get_bool(r)) { + return; + } + kitchen_sink_str* value = (kitchen_sink_str*)calloc(1, sizeof(kitchen_sink_str)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = kitchen_sink_reader_get_string(r); + *out = value; +} + +static inline bool kitchen_sink_opt_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_str** out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_opt_string_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_opt_string_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_opt_string_free(kitchen_sink_str** v) { + if (*v == NULL) { + return; + } + kitchen_sink_str_free(&(**v)); + free(*v); + *v = NULL; +} + #ifdef __cplusplus } #endif diff --git a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap index ae1a6fe2..56a4609b 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap @@ -19,8 +19,6 @@ source: crates/weaveffi-cli/tests/snapshots.rs # else # define KITCHEN_SINK_API __declspec(dllimport) # endif -# elif defined(__EMSCRIPTEN__) -# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) # elif defined(__GNUC__) && (__GNUC__ >= 4) # define KITCHEN_SINK_API __attribute__((visibility("default"))) # else @@ -45,31 +43,35 @@ extern "C" { /* The C ABI revision this header was generated against. The producer exports kitchen_sink_abi_version() so a consumer can refuse to load a library built for a different revision. */ -#define KITCHEN_SINK_ABI_VERSION 4u +#define KITCHEN_SINK_ABI_VERSION 5u KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); -/* Error slot written by every fallible call. `message` is NUL-terminated - UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields - serialized in the value buffer format (null when the code declares no - fields). Both are released by kitchen_sink_error_clear. Positive codes are - the API's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation failed, -5 cancelled. */ +/* Error slot written by every fallible call. `code` is 0 on success, + positive for an error domain's code, and negative for a runtime + failure: -1 an untyped error (`throws: any`), -2 a producer panic, -3 a + marshalling failure, -4 a callback interface implementation failed, + -5 cancelled. The message is `message_len` bytes of UTF-8 at + `message_ptr`, NOT NUL-terminated. `payload_ptr`/`payload_len` hold + the code's fields in the value buffer format (NULL when it declares + none). The producer owns both runs; kitchen_sink_error_clear releases them. */ typedef struct kitchen_sink_error { int32_t code; - const char* message; + const uint8_t* message_ptr; + size_t message_len; const uint8_t* payload_ptr; size_t payload_len; } kitchen_sink_error; -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface implementations call this to report a failure without - allocating with a foreign allocator. */ -KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +/* Fill `err` with `code` and a producer-owned copy of the `message_len` + bytes at `message_ptr` (UTF-8), replacing what it held. Callback + implementations report failures this way, never by allocating the + message themselves. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const uint8_t* message_ptr, size_t message_len); /* Replace `err`'s payload with a producer-owned copy of `len` bytes: the - fields of a declared error code, which a callback method that throws - attaches after kitchen_sink_error_set. */ + fields of a domain code, which a callback method attaches after + kitchen_sink_error_set. */ KITCHEN_SINK_API void kitchen_sink_error_set_payload(kitchen_sink_error* err, const uint8_t* ptr, size_t len); +/* Release `err`'s message and payload and reset it to success. */ KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); /* Async completions receive a heap-boxed error the consumer owns; @@ -77,12 +79,13 @@ KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); NULL is a no-op. */ KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); -/* Allocate a zero-filled run of `len` bytes (NULL for 0). A callback - method returns a string, bytes, or buffer as such a run, which the - producer adopts; any other run is released with kitchen_sink_free_bytes. */ +/* Allocate a zero-filled, 8-aligned run of `len` bytes (NULL for 0). A + callback method returns a string, bytes, buffer, or typed array as + such a run, which the producer adopts. */ KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(size_t len); -/* Release a string, bytes, or value buffer the producer returned, or a - run from kitchen_sink_alloc, with its exact length. NULL or 0 is a no-op. */ +/* Release a run the producer returned (a string, bytes, value buffer, + or typed array: `len` is its size in bytes, `count * sizeof(T)` for + a typed array) or one from kitchen_sink_alloc. NULL or 0 is a no-op. */ KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); /* Cancellation for `cancellable` async functions. A token starts with @@ -95,6 +98,12 @@ KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); +/* A callback interface vtable's `flags` bit: the methods that return a + value (a non-void C return or any out slot) may only be called on + the thread that passed the vtable to the producer, which fails such + a call from any other thread with -4 without calling it. */ +#define KITCHEN_SINK_VTABLE_THREAD_AFFINE 1u + /* Live-allocation counters for leak checks in tests: 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. Kind -1 is 1 when the producer counts at all (its `leak-check` @@ -115,8 +124,8 @@ typedef struct kitchen_sink_contract_entry { KITCHEN_SINK_API const kitchen_sink_contract_entry* kitchen_sink_shared_contract(size_t* out_len); #define KITCHEN_SINK_SHARED_CONTRACT_LEN 2 #define KITCHEN_SINK_SHARED_CONTRACT { \ - {0x709e234caa962446ull, 0x3894f2ca452e9f98ull}, /* shared.ping */ \ - {0xcfb306fa5801ee5bull, 0x1cbd9e10983cac2cull}, /* shared.Token */ \ + {0x709e234caa962446ull, 0x3894f2ca452e9f98ull}, /* shared.ping: function ping() -> string */ \ + {0xcfb306fa5801ee5bull, 0x18e9fc204a13df6full}, /* shared.Token: record Token {i64, string} */ \ } /* The id of the first expected entry the loaded library lacks or declares differently, or 0 when these bindings match it. */ @@ -142,39 +151,69 @@ static inline uint64_t kitchen_sink_shared_contract_check(void) { checks each entry it was generated with against it once, before any other call (producer entries it doesn't know are fine). */ KITCHEN_SINK_API const kitchen_sink_contract_entry* kitchen_sink_kitchen_contract(size_t* out_len); -#define KITCHEN_SINK_KITCHEN_CONTRACT_LEN 31 +#define KITCHEN_SINK_KITCHEN_CONTRACT_LEN 61 #define KITCHEN_SINK_KITCHEN_CONTRACT { \ - {0x01d4f09bbddc986full, 0x29eb88fecc66544dull}, /* kitchen.Gadget.new */ \ - {0x10472d8fe748c2eeull, 0xb265832b6a759fc4ull}, /* kitchen.maybe_gadget */ \ - {0x11368eec07a74078ull, 0xfa049f78b77e146aull}, /* kitchen.gadgets */ \ - {0x137f08a29cde6021ull, 0xda8de7aeb2dbb8a3ull}, /* kitchen.stream_gadgets */ \ - {0x2111b54980826dd7ull, 0x452783f3e6ca6e1dull}, /* kitchen.do_cancellable */ \ - {0x29dddbc60bf8d7dbull, 0x452a3e573d57f6dbull}, /* kitchen.Priority */ \ - {0x2b227003b74a0d16ull, 0x12332957470e9782ull}, /* kitchen.maybe_subscribe */ \ - {0x2dd66cb0064f0494ull, 0x60197363fefd92ddull}, /* kitchen.Item */ \ - {0x33c265bbf079b0e9ull, 0xc3dcd0927e6c1debull}, /* kitchen.u32_id */ \ - {0x33e6381d01660dfeull, 0x077c63c31fd7f90aull}, /* kitchen.KitchenErrors */ \ - {0x3f5fc99d73504b76ull, 0x2df88d710f1ea10eull}, /* kitchen.Gadget.poke */ \ - {0x4c5764cf109528d1ull, 0x2a65529af4338a58ull}, /* kitchen.maybe_item */ \ - {0x4de17dfb93fd494dull, 0xba0ad0dd52ed74afull}, /* kitchen.i32_id */ \ - {0x504e95b7f7e65570ull, 0x01b3e8e0c9a10e23ull}, /* kitchen.stream_items */ \ - {0x68cdb9a4bd3e3dd5ull, 0xc678b9164348d497ull}, /* kitchen.bool_id */ \ - {0x69e7cd703d16eb85ull, 0xf3b28dd22ae9e689ull}, /* kitchen.do_async */ \ - {0x70865079ce41be82ull, 0xbe3b69464672af46ull}, /* kitchen.Gadget.describe */ \ - {0x71c5a1116cb7c49aull, 0xcba1837108a89b84ull}, /* kitchen.echo_bytes */ \ - {0x735ee1e0049ee91aull, 0xca74320bc53eddabull}, /* kitchen.echo_string */ \ - {0x76d7fc58f4e41d84ull, 0x7373a0e47d30e690ull}, /* kitchen.i64_id */ \ - {0x79abfe1aa960fd6aull, 0xbca7a9e5eb26f3a6ull}, /* kitchen.nested.hello */ \ - {0x7d3dd7d5cf6749e3ull, 0xa192f5a7298adb39ull}, /* kitchen.new_op */ \ - {0x89a5e664bc25c67bull, 0x5764a95e81ecfd31ull}, /* kitchen.f64_id */ \ - {0x8cfdbdcecac5f7f1ull, 0x07e272f09c41fffcull}, /* kitchen.Gadget.version */ \ - {0x974b9f3e823c1738ull, 0xb81b91e975695f27ull}, /* kitchen.cross_module_token */ \ - {0xaca0655fac15d230ull, 0x11c4d2b28abe07bbull}, /* kitchen.list_items */ \ - {0xc883b4afae20543eull, 0xc40560e5411ff392ull}, /* kitchen.legacy_op */ \ - {0xf2f7753e6e1dffc2ull, 0xc9047a95bdf49e93ull}, /* kitchen.get_attrs */ \ - {0xfae25d3391922eadull, 0x1c7f95739f3210b8ull}, /* kitchen.subscribe */ \ - {0xfd0059b05cb2fcfbull, 0x248bf8404a5823b4ull}, /* kitchen.Gadget */ \ - {0xfed9c551fb9fd2d6ull, 0x59aec980ef3f3291ull}, /* kitchen.ReadyListener */ \ + {0x01d4f09bbddc986full, 0xffcf2bd3b04aa002ull}, /* kitchen.Gadget.new: constructor new(i64) -> Gadget */ \ + {0x02bbd5f31c953704ull, 0x2cfa679ea1373e09ull}, /* kitchen.Sampler.samples: callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError */ \ + {0x10472d8fe748c2eeull, 0xcc570a2649bbf7dcull}, /* kitchen.maybe_gadget: function maybe_gadget(Gadget?) -> Gadget? */ \ + {0x11368eec07a74078ull, 0xfa049f78b77e146aull}, /* kitchen.gadgets: function gadgets() -> [Gadget] */ \ + {0x137f08a29cde6021ull, 0xda8de7aeb2dbb8a3ull}, /* kitchen.stream_gadgets: function stream_gadgets() -> iter */ \ + {0x184bfc4ab0e3fe45ull, 0xb64587477022696bull}, /* kitchen.ids_u64: function ids_u64([u64]) -> [u64] */ \ + {0x2111b54980826dd7ull, 0x627bf01ab365b209ull}, /* kitchen.do_cancellable: function do_cancellable(string) -> string async cancellable */ \ + {0x21c70f9371374d74ull, 0x25d5d88a30fe0a79ull}, /* kitchen.ReadyListener.favorite: callback_method #4 favorite() -> Gadget */ \ + {0x23de5bd597372f46ull, 0xcf1bc6bb37872798ull}, /* kitchen.stream_chunks: function stream_chunks() -> iter<[i32]> */ \ + {0x249f1ddaec6a6cbeull, 0x0710d6c9a4de93daull}, /* kitchen.take_from_pantry: function take_from_pantry(string) -> i32 throws PantryError */ \ + {0x29dddbc60bf8d7dbull, 0x452a3e573d57f6dbull}, /* kitchen.Priority: enum Priority {Low = 0, Normal = 1, High = 2} */ \ + {0x2b227003b74a0d16ull, 0x5dae036b8a6aed30ull}, /* kitchen.maybe_subscribe: function maybe_subscribe(ReadyListener?) -> bool */ \ + {0x2dd66cb0064f0494ull, 0x74b131724dd0d867ull}, /* kitchen.Item: record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} */ \ + {0x33c265bbf079b0e9ull, 0x97c32b764693f2cbull}, /* kitchen.u32_id: function u32_id(u32) -> u32 */ \ + {0x33e6381d01660dfeull, 0x05e35a563089bf7dull}, /* kitchen.KitchenErrors: errors KitchenErrors */ \ + {0x3d969ae76db6cb23ull, 0x65a325cfc772296bull}, /* kitchen.ReadyListener.latest: callback_method #3 latest() -> Item? */ \ + {0x3f5fc99d73504b76ull, 0x417e4a2acd3210b3ull}, /* kitchen.Gadget.poke: method poke(i32) -> i32 throws KitchenErrors */ \ + {0x4082f0085db28781ull, 0x18848e701962912eull}, /* kitchen.Sampler: callback Sampler */ \ + {0x4340a6b1e7ec8c50ull, 0xa59d0f32a66dc7a2ull}, /* kitchen.Sampler.describe: callback_method #2 describe() -> string throws any */ \ + {0x4c5764cf109528d1ull, 0xb374accf14676d90ull}, /* kitchen.maybe_item: function maybe_item(i64) -> Item? throws KitchenErrors */ \ + {0x4de17dfb93fd494dull, 0x78be3203bcd10727ull}, /* kitchen.i32_id: function i32_id(i32) -> i32 */ \ + {0x504e95b7f7e65570ull, 0x01b3e8e0c9a10e23ull}, /* kitchen.stream_items: function stream_items() -> iter */ \ + {0x5dafabb7775fcb2dull, 0x6c4b78c7d3f858c1ull}, /* kitchen.PantryError.Spoiled: code Spoiled = 2 */ \ + {0x5e0a8f6be4b00600ull, 0x18ea1a42036c96a0ull}, /* kitchen.Sampler.threshold: callback_method #0 threshold(u32?) -> f64? */ \ + {0x5ec0af1d4ecc6fedull, 0x6540bdb74246420cull}, /* kitchen.async_samples: function async_samples(u32) -> [f32] async cancellable */ \ + {0x61a358b0571e0a4eull, 0x11a24c4b50d11996ull}, /* kitchen.async_maybe_count: function async_maybe_count() -> i64? async */ \ + {0x68cdb9a4bd3e3dd5ull, 0xcde022fe6bc3b1bfull}, /* kitchen.bool_id: function bool_id(bool) -> bool */ \ + {0x69e7cd703d16eb85ull, 0xb13cb8d39e702c79ull}, /* kitchen.do_async: function do_async(string) -> string async */ \ + {0x70865079ce41be82ull, 0xbe3b69464672af46ull}, /* kitchen.Gadget.describe: method describe() -> string */ \ + {0x71c5a1116cb7c49aull, 0x1506cfe6f548fac4ull}, /* kitchen.echo_bytes: function echo_bytes(bytes) -> bytes */ \ + {0x735ee1e0049ee91aull, 0xa713dbd1be707752ull}, /* kitchen.echo_string: function echo_string(string) -> string */ \ + {0x76d7fc58f4e41d84ull, 0x6bdb4ccea2841d5eull}, /* kitchen.i64_id: function i64_id(i64) -> i64 */ \ + {0x78d91fd617e162b8ull, 0xd88e2753503b36dfull}, /* kitchen.stream_maybe: function stream_maybe() -> iter */ \ + {0x79abfe1aa960fd6aull, 0xbca7a9e5eb26f3a6ull}, /* kitchen.nested.hello: function hello() -> string */ \ + {0x7b3865b536da998cull, 0x1c4e79d60a20e74full}, /* kitchen.mixed_slices: function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] */ \ + {0x7ba266dc26a58989ull, 0x0f86001cac0c21e8ull}, /* kitchen.KitchenErrors.InvalidInput: code InvalidInput = 2 */ \ + {0x7d3dd7d5cf6749e3ull, 0xa192f5a7298adb39ull}, /* kitchen.new_op: function new_op() -> i32 */ \ + {0x81ded69adee0abf2ull, 0x8c71141a931906f6ull}, /* kitchen.scale_i32: function scale_i32([i32], i32) -> [i32] */ \ + {0x81fa72769330fed6ull, 0x89681e305b55a4f4ull}, /* kitchen.ReadyListener.label: callback_method #2 label() -> string throws KitchenErrors */ \ + {0x89a5e664bc25c67bull, 0x357fd9b65ad879a9ull}, /* kitchen.f64_id: function f64_id(f64) -> f64 */ \ + {0x8cfdbdcecac5f7f1ull, 0x07e272f09c41fffcull}, /* kitchen.Gadget.version: static version() -> string */ \ + {0x8dc42397e47614b9ull, 0xcddf6fede09b2e7full}, /* kitchen.opt_priority: function opt_priority(Priority?) -> Priority? */ \ + {0x92f060ef68961fc1ull, 0x4af6ce5b4f9fa508ull}, /* kitchen.ReadyListener.on_item: callback_method #1 on_item(Item, Gadget) -> bool */ \ + {0x974b9f3e823c1738ull, 0xb81b91e975695f27ull}, /* kitchen.cross_module_token: function cross_module_token() -> Token */ \ + {0x9f0f001aa85f97d5ull, 0x55815247f895bd45ull}, /* kitchen.sum_f64: function sum_f64([f64]) -> f64 */ \ + {0xab2a29fd1ef5458bull, 0x471a34aa4c09ae2bull}, /* kitchen.KitchenErrors.NotFound: code NotFound = 1 */ \ + {0xaca0655fac15d230ull, 0x11c4d2b28abe07bbull}, /* kitchen.list_items: function list_items() -> [Item] */ \ + {0xb57c8e6ae3fb4383ull, 0x3f857851ab75366cull}, /* kitchen.PantryError: errors PantryError */ \ + {0xbdc795b195edd428ull, 0x36127462f8b0969full}, /* kitchen.opt_mixed: function opt_mixed(f64, f32?, i64?, string?) -> u16? */ \ + {0xbe4821ab3c340e71ull, 0xe1d32e38b766e623ull}, /* kitchen.ReadyListener.on_ready: callback_method #0 on_ready(i32, string) -> void */ \ + {0xc2ad53f2a6a36323ull, 0x071e29abee823f17ull}, /* kitchen.opt_i32_id: function opt_i32_id(i32?) -> i32? */ \ + {0xc7ab409554fd3d7bull, 0xfea12b6bc219fc46ull}, /* kitchen.parse_any: function parse_any(string) -> i64 throws any */ \ + {0xc82a7f2d85f99f4eull, 0xa9bcb8bacedb036cull}, /* kitchen.sample_with: function sample_with(Sampler, u32?) -> [i16] throws PantryError */ \ + {0xc883b4afae20543eull, 0xc40560e5411ff392ull}, /* kitchen.legacy_op: function legacy_op() -> i32 */ \ + {0xd180bf646bff59e2ull, 0xa4515a6d00d7c600ull}, /* kitchen.PantryError.OutOfStock: code OutOfStock = 1 {string} */ \ + {0xe6778df0f5c1b82bull, 0x38eaafdb350559e1ull}, /* kitchen.opt_bool_id: function opt_bool_id(bool?) -> bool? */ \ + {0xf2f7753e6e1dffc2ull, 0xc9047a95bdf49e93ull}, /* kitchen.get_attrs: function get_attrs() -> {string:i32} */ \ + {0xfae25d3391922eadull, 0xcc034cf506cae182ull}, /* kitchen.subscribe: function subscribe(ReadyListener) -> i32 */ \ + {0xfd0059b05cb2fcfbull, 0x248bf8404a5823b4ull}, /* kitchen.Gadget: interface Gadget */ \ + {0xfed9c551fb9fd2d6ull, 0xe1020573f084dc1dull}, /* kitchen.ReadyListener: callback ReadyListener */ \ + {0xff3a99b52597dee2ull, 0x14c4ecac97bdfc9bull}, /* kitchen.Gadget.readings: method readings(u32?) -> [f64] throws any */ \ } /* The id of the first expected entry the loaded library lacks or declares differently, or 0 when these bindings match it. */ @@ -196,36 +235,80 @@ static inline uint64_t kitchen_sink_kitchen_contract_check(void) { } /* - * Strings, bytes, and value buffers cross as (ptr, len) pairs. A - * parameter named "v" expands to a view borrowed for the call: - * const uint8_t* v_ptr, size_t v_len - * Strings are UTF-8 without a NUL terminator; NULL with length 0 is - * the empty value. A returned string, bytes, or buffer is - * `const uint8_t*` with a trailing `size_t* out_len`, owned by the - * caller and released with kitchen_sink_free_bytes. - * Records, rich enums, lists, maps, and optionals are serialized - * in the little-endian value buffer format; when generated, - * kitchen_sink_buffer.h declares a C struct and codec for each. + * How values cross. A parameter `v` becomes, by its type: + * integer, float, bool, C-style enum T v + * optional of one of those (T?) bool has_v, T v (v is 0 and ignored + * when has_v is false) + * [i8] [i16] [i32] [i64] [u16] [u32] const T* v_ptr, size_t v_len + * [u64] [f32] [f64] (typed arrays) (v_len counts elements; v_ptr is + * aligned for T, NULL when v_len is 0) + * string, bytes, and every other const uint8_t* v_ptr, size_t v_len + * record, enum, list, map, optional + * interface const Tag* v (borrowed; NULL for an + * absent optional) + * callback interface void* v_ctx, const Tag_vtable* v_vtable + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is the + * empty value. Everything in the (ptr, len) row past bytes is a value + * buffer in the little-endian format; when generated, kitchen_sink_buffer.h + * declares a C struct and codec for each such type. + * + * A return of type T is the C return, except: T? returns `bool` (present) + * with the value in a trailing `T* out_value`; a typed array returns `T*` + * with its element count in `size_t* out_len`, released with + * kitchen_sink_free_bytes((uint8_t*)ptr, len * sizeof(T)); a string, bytes, or + * buffer returns `const uint8_t*` with `size_t* out_len`, released with + * kitchen_sink_free_bytes(ptr, len); an interface returns an owned reference, + * released with its `_destroy`. Every producer run is 8-aligned. Every + * sync call ends with `kitchen_sink_error* out_err`. */ /** Task priority levels */ -typedef enum { +typedef int32_t kitchen_sink_kitchen_Priority; +enum { /** Low priority */ kitchen_sink_kitchen_Priority_Low = 0, /** Normal priority */ kitchen_sink_kitchen_Priority_Normal = 1, /** High priority */ kitchen_sink_kitchen_Priority_High = 2 -} kitchen_sink_kitchen_Priority; -/** Error codes reported by throwing functions in the `kitchen` module tree. */ -typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +}; +/** + * Error codes of the `KitchenErrors` domain of module `kitchen` + * (`KitchenError` in the language bindings), which a function declared + * `throws: KitchenErrors` reports in `kitchen_sink_error.code`. Domains are + * open: a newer library may report a positive code not listed here, which + * keeps its message. + */ +typedef int32_t kitchen_sink_kitchen_KitchenErrors; +enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 }; +/** + * Error codes of the `PantryError` domain of module `kitchen` (`PantryError` + * in the language bindings), which a function declared `throws: PantryError` + * reports in `kitchen_sink_error.code`. Domains are open: a newer library may + * report a positive code not listed here, which keeps its message. + */ +typedef int32_t kitchen_sink_kitchen_PantryError; +enum { kitchen_sink_kitchen_PantryError_OutOfStock = 1, kitchen_sink_kitchen_PantryError_Spoiled = 2 }; typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +typedef struct kitchen_sink_kitchen_StreamMaybeIterator kitchen_sink_kitchen_StreamMaybeIterator; +typedef struct kitchen_sink_kitchen_StreamChunksIterator kitchen_sink_kitchen_StreamChunksIterator; /** * Consumer-implemented observer of ready items * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable, with `size` set to `sizeof` the vtable and `flags` to 0 (the producer rejects a smaller vtable). The producer may call any entry from any thread until it calls `free(ctx)` exactly once, also from any thread. A method reports failure with `error_set(out_err, ...)`; a string, bytes, or buffer return is a run the consumer allocates with `alloc` and writes to `out_ptr` and `out_len`, which the producer adopts. + * A consumer-implemented callback interface. The consumer passes a context + * pointer and a pointer to a vtable that outlives it, with `size` set to + * `sizeof` the vtable (the producer rejects a smaller one) and `flags` to 0 or + * `KITCHEN_SINK_VTABLE_THREAD_AFFINE`. The producer may call any method from + * any thread (with `KITCHEN_SINK_VTABLE_THREAD_AFFINE`, a method that returns + * a value only from the thread that passed the vtable) until it calls + * `free(ctx)` exactly once, also from any thread. A method reports failure + * with `kitchen_sink_error_set(out_err, ...)`. It returns a string, bytes, + * buffer, or typed array as a run it allocates with `kitchen_sink_alloc` and + * stores in `*out_ptr` and `*out_len` (an element count for a typed array), + * which the producer adopts, and an optional scalar as a `bool` C return + * (present) with the value in `*out_value`. */ typedef struct kitchen_sink_kitchen_ReadyListener_vtable { uint32_t size; @@ -238,7 +321,8 @@ typedef struct kitchen_sink_kitchen_ReadyListener_vtable { /** * A display label, failing with a kitchen error * - * May fail with a `kitchen_sink_kitchen_KitchenErrors` code (its fields attached with `kitchen_sink_error_set_payload`). + * May fail with a `kitchen_sink_kitchen_KitchenErrors` code (attaching its + * fields with `kitchen_sink_error_set_payload` when it declares any). */ void (*label)(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); /** The most recent item the listener kept, if any */ @@ -246,8 +330,46 @@ typedef struct kitchen_sink_kitchen_ReadyListener_vtable { /** The listener's favorite gadget */ kitchen_sink_kitchen_Gadget* (*favorite)(void* ctx, kitchen_sink_error* out_err); } kitchen_sink_kitchen_ReadyListener_vtable; +/** + * Consumer-implemented source of scalar samples + * + * A consumer-implemented callback interface. The consumer passes a context + * pointer and a pointer to a vtable that outlives it, with `size` set to + * `sizeof` the vtable (the producer rejects a smaller one) and `flags` to 0 or + * `KITCHEN_SINK_VTABLE_THREAD_AFFINE`. The producer may call any method from + * any thread (with `KITCHEN_SINK_VTABLE_THREAD_AFFINE`, a method that returns + * a value only from the thread that passed the vtable) until it calls + * `free(ctx)` exactly once, also from any thread. A method reports failure + * with `kitchen_sink_error_set(out_err, ...)`. It returns a string, bytes, + * buffer, or typed array as a run it allocates with `kitchen_sink_alloc` and + * stores in `*out_ptr` and `*out_len` (an element count for a typed array), + * which the producer adopts, and an optional scalar as a `bool` C return + * (present) with the value in `*out_value`. + */ +typedef struct kitchen_sink_kitchen_Sampler_vtable { + uint32_t size; + uint32_t flags; + void (*free)(void* ctx); + /** The cutoff for a level, if the sampler has one */ + bool (*threshold)(void* ctx, bool has_level, uint32_t level, double* out_value, kitchen_sink_error* out_err); + /** + * Up to `limit` samples, failing when the pantry is empty + * + * May fail with a `kitchen_sink_kitchen_PantryError` code (attaching its + * fields with `kitchen_sink_error_set_payload` when it declares any). + */ + void (*samples)(void* ctx, bool has_limit, uint32_t limit, const double* weights_ptr, size_t weights_len, int16_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); + /** + * A free-form description, failing with an untyped error + * + * May fail with code -1 and a message. + */ + void (*describe)(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); +} kitchen_sink_kitchen_Sampler_vtable; typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_async_maybe_count_callback)(void* context, kitchen_sink_error* err, bool has_result, int64_t result); +typedef void (*kitchen_sink_kitchen_async_samples_callback)(void* context, kitchen_sink_error* err, const float* result_ptr, size_t result_len); // Module: shared /** Trivial cross-module helper */ @@ -261,8 +383,19 @@ KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(in KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); /** Render the gadget as a human-readable string */ KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); -/** Poke the gadget a number of times, failing on a negative count */ +/** + * Poke the gadget a number of times, failing on a negative count + * + * Fails with a `kitchen_sink_kitchen_KitchenErrors` code (and its fields as a + * payload when it declares any). + */ KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** + * The gadget's sensor readings, optionally limited + * + * Fails with code -1 and a message. + */ +KITCHEN_SINK_API double* kitchen_sink_kitchen_Gadget_readings(const kitchen_sink_kitchen_Gadget* self, bool has_limit, uint32_t limit, size_t* out_len, kitchen_sink_error* out_err); /** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); /** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ @@ -288,7 +421,12 @@ KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, ki KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); -/** Optional struct return */ +/** + * Optional struct return + * + * Fails with a `kitchen_sink_kitchen_KitchenErrors` code (and its fields as a + * payload when it declares any). + */ KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); /** List of structs */ KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); @@ -305,10 +443,56 @@ KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, si /** Cancellable async operation */ KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); /** Legacy operation kept for compatibility */ -KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_DEPRECATED("Use `kitchen_sink_kitchen_new_op` instead") KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); -/** Replacement for legacy_op */ +/** Replacement for `kitchen_sink_kitchen_legacy_op` */ KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); +/** Optional scalar in both directions */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_opt_i32_id(bool has_x, int32_t x, int32_t* out_value, kitchen_sink_error* out_err); +KITCHEN_SINK_API bool kitchen_sink_kitchen_opt_bool_id(bool has_x, bool x, bool* out_value, kitchen_sink_error* out_err); +/** Optional C-style enum in both directions */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_opt_priority(bool has_p, kitchen_sink_kitchen_Priority p, kitchen_sink_kitchen_Priority* out_value, kitchen_sink_error* out_err); +/** Optional scalars next to ordinary parameters */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_opt_mixed(double base, bool has_scale, float scale, bool has_offset, int64_t offset, const uint8_t* label_ptr, size_t label_len, uint16_t* out_value, kitchen_sink_error* out_err); +/** A typed-array parameter */ +KITCHEN_SINK_API double kitchen_sink_kitchen_sum_f64(const double* xs_ptr, size_t xs_len, kitchen_sink_error* out_err); +/** Typed arrays in both directions */ +KITCHEN_SINK_API int32_t* kitchen_sink_kitchen_scale_i32(const int32_t* xs_ptr, size_t xs_len, int32_t factor, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint64_t* kitchen_sink_kitchen_ids_u64(const uint64_t* ids_ptr, size_t ids_len, size_t* out_len, kitchen_sink_error* out_err); +/** Every slice element type */ +KITCHEN_SINK_API uint32_t* kitchen_sink_kitchen_mixed_slices(const int8_t* a_ptr, size_t a_len, const int16_t* b_ptr, size_t b_len, const uint16_t* c_ptr, size_t c_len, const uint32_t* d_ptr, size_t d_len, const int64_t* e_ptr, size_t e_len, const float* f_ptr, size_t f_len, size_t* out_len, kitchen_sink_error* out_err); +/** Async returning an optional scalar */ +KITCHEN_SINK_API void kitchen_sink_kitchen_async_maybe_count(kitchen_sink_kitchen_async_maybe_count_callback callback, void* context); +/** Async returning a typed array */ +KITCHEN_SINK_API void kitchen_sink_kitchen_async_samples(uint32_t n, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_async_samples_callback callback, void* context); +/** Iterator of optional scalars */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamMaybeIterator* kitchen_sink_kitchen_stream_maybe(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamMaybeIterator_next(kitchen_sink_kitchen_StreamMaybeIterator* iter, bool* out_has_item, int32_t* out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamMaybeIterator_destroy(kitchen_sink_kitchen_StreamMaybeIterator* iter); +/** Iterator of typed arrays */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamChunksIterator* kitchen_sink_kitchen_stream_chunks(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamChunksIterator_next(kitchen_sink_kitchen_StreamChunksIterator* iter, int32_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamChunksIterator_destroy(kitchen_sink_kitchen_StreamChunksIterator* iter); +/** + * Drive a consumer-implemented sampler + * + * Fails with a `kitchen_sink_kitchen_PantryError` code (and its fields as a + * payload when it declares any). + */ +KITCHEN_SINK_API int16_t* kitchen_sink_kitchen_sample_with(void* sampler_ctx, const kitchen_sink_kitchen_Sampler_vtable* sampler_vtable, bool has_level, uint32_t level, size_t* out_len, kitchen_sink_error* out_err); +/** + * Fails with the second domain + * + * Fails with a `kitchen_sink_kitchen_PantryError` code (and its fields as a + * payload when it declares any). + */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_take_from_pantry(const uint8_t* item_ptr, size_t item_len, kitchen_sink_error* out_err); +/** + * Fails with an untyped error + * + * Fails with code -1 and a message. + */ +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_parse_any(const uint8_t* text_ptr, size_t text_len, kitchen_sink_error* out_err); // Module: kitchen_nested /** Trivial nested-module function */ diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_buffer_h.snap deleted file mode 100644 index 320c6526..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_buffer_h.snap +++ /dev/null @@ -1,505 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef NESTED_MODULES_BUFFER_H -#define NESTED_MODULES_BUFFER_H - -#include "nested_modules.h" - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* --------------------------------------------------------------------------- - * Value buffer runtime. - * - * The wire format is little-endian and packed: fixed-width scalars at their - * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length - * followed by the raw bytes (no NUL terminator), optionals as a presence byte - * followed by the value, lists as a `uint32_t` count followed by the - * elements, maps as a `uint32_t` count followed by alternating keys and - * values, and objects as a `uint64_t` token holding one strong reference. - * ------------------------------------------------------------------------- */ - -/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. - A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be - passed to C string functions when the text has no interior NUL. */ -typedef struct nested_modules_str { - const char* ptr; - size_t len; -} nested_modules_str; - -/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ -typedef struct nested_modules_bytes { - const uint8_t* ptr; - size_t len; -} nested_modules_bytes; - -/* Borrow a NUL-terminated C string (NULL is the empty string). */ -static inline nested_modules_str nested_modules_str_of(const char* s) { - nested_modules_str v; - v.ptr = s; - v.len = s ? strlen(s) : 0; - return v; -} - -/* Borrow `len` bytes at `ptr`. */ -static inline nested_modules_bytes nested_modules_bytes_of(const uint8_t* ptr, size_t len) { - nested_modules_bytes v; - v.ptr = ptr; - v.len = len; - return v; -} - -/* Release a decoded string. Never call this on a borrowed view. */ -static inline void nested_modules_str_free(nested_modules_str* s) { - free((void*)s->ptr); - s->ptr = NULL; - s->len = 0; -} - -/* Release decoded bytes. Never call this on a borrowed view. */ -static inline void nested_modules_bytes_free(nested_modules_bytes* b) { - free((void*)b->ptr); - b->ptr = NULL; - b->len = 0; -} - -/* A growable output buffer. Zero-initialize it, write values, pass - `ptr`/`len` to the call, then release it with nested_modules_writer_free. An - allocation failure sets `failed` and turns later writes into no-ops. */ -typedef struct nested_modules_writer { - uint8_t* ptr; - size_t len; - size_t cap; - bool failed; -} nested_modules_writer; - -static inline void nested_modules_writer_free(nested_modules_writer* w) { - free(w->ptr); - w->ptr = NULL; - w->len = 0; - w->cap = 0; - w->failed = false; -} - -static inline void nested_modules_writer_put_raw(nested_modules_writer* w, const void* data, size_t n) { - if (w->failed || n == 0) { - return; - } - if (n > w->cap - w->len) { - size_t cap = w->cap ? w->cap : 64; - while (cap - w->len < n) { - if (cap > SIZE_MAX / 2) { - w->failed = true; - return; - } - cap *= 2; - } - uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); - if (grown == NULL) { - w->failed = true; - return; - } - w->ptr = grown; - w->cap = cap; - } - memcpy(w->ptr + w->len, data, n); - w->len += n; -} - -static inline void nested_modules_writer_put_u64(nested_modules_writer* w, uint64_t v) { - uint8_t b[8]; - for (int i = 0; i < 8; i++) { - b[i] = (uint8_t)(v >> (8 * i)); - } - nested_modules_writer_put_raw(w, b, 8); -} - -static inline void nested_modules_writer_put_u32(nested_modules_writer* w, uint32_t v) { - uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; - nested_modules_writer_put_raw(w, b, 4); -} - -static inline void nested_modules_writer_put_u16(nested_modules_writer* w, uint16_t v) { - uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; - nested_modules_writer_put_raw(w, b, 2); -} - -static inline void nested_modules_writer_put_u8(nested_modules_writer* w, uint8_t v) { - nested_modules_writer_put_raw(w, &v, 1); -} - -static inline void nested_modules_writer_put_i8(nested_modules_writer* w, int8_t v) { - nested_modules_writer_put_u8(w, (uint8_t)v); -} - -static inline void nested_modules_writer_put_i16(nested_modules_writer* w, int16_t v) { - nested_modules_writer_put_u16(w, (uint16_t)v); -} - -static inline void nested_modules_writer_put_i32(nested_modules_writer* w, int32_t v) { - nested_modules_writer_put_u32(w, (uint32_t)v); -} - -static inline void nested_modules_writer_put_i64(nested_modules_writer* w, int64_t v) { - nested_modules_writer_put_u64(w, (uint64_t)v); -} - -static inline void nested_modules_writer_put_bool(nested_modules_writer* w, bool v) { - nested_modules_writer_put_u8(w, v ? 1 : 0); -} - -static inline void nested_modules_writer_put_f32(nested_modules_writer* w, float v) { - uint32_t bits; - memcpy(&bits, &v, 4); - nested_modules_writer_put_u32(w, bits); -} - -static inline void nested_modules_writer_put_f64(nested_modules_writer* w, double v) { - uint64_t bits; - memcpy(&bits, &v, 8); - nested_modules_writer_put_u64(w, bits); -} - -static inline void nested_modules_writer_put_len(nested_modules_writer* w, size_t n) { - if (n > UINT32_MAX) { - w->failed = true; - return; - } - nested_modules_writer_put_u32(w, (uint32_t)n); -} - -static inline void nested_modules_writer_put_string(nested_modules_writer* w, nested_modules_str v) { - nested_modules_writer_put_len(w, v.len); - nested_modules_writer_put_raw(w, v.ptr, v.len); -} - -static inline void nested_modules_writer_put_bytes(nested_modules_writer* w, nested_modules_bytes v) { - nested_modules_writer_put_len(w, v.len); - nested_modules_writer_put_raw(w, v.ptr, v.len); -} - -/* Write an object token. The token carries one strong reference, so pass - a reference the buffer may own (the generated codecs pass a fresh - `_clone`). */ -static inline void nested_modules_writer_put_object(nested_modules_writer* w, const void* owned) { - nested_modules_writer_put_u64(w, (uint64_t)(uintptr_t)owned); -} - -/* A cursor over an input buffer. A malformed buffer sets `failed`; every - later read then returns a zero value, so a decoder can always finish and - release what it built. */ -typedef struct nested_modules_reader { - const uint8_t* ptr; - size_t len; - size_t pos; - bool failed; -} nested_modules_reader; - -static inline nested_modules_reader nested_modules_reader_of(const uint8_t* ptr, size_t len) { - nested_modules_reader r; - r.ptr = ptr; - r.len = ptr ? len : 0; - r.pos = 0; - r.failed = false; - return r; -} - -/* `true` when every byte was consumed and nothing was malformed. */ -static inline bool nested_modules_reader_finish(const nested_modules_reader* r) { - return !r->failed && r->pos == r->len; -} - -static inline const uint8_t* nested_modules_reader_take(nested_modules_reader* r, size_t n) { - if (r->failed || n > r->len - r->pos) { - r->failed = true; - return NULL; - } - const uint8_t* at = r->ptr + r->pos; - r->pos += n; - return at; -} - -/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is - zero). `min_wire` is the smallest encoding of one element; a count the - remaining bytes can't hold fails the reader before anything is - allocated. */ -static inline void* nested_modules_reader_alloc(nested_modules_reader* r, size_t n, size_t size, size_t min_wire) { - if (r->failed || n == 0) { - return NULL; - } - if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { - r->failed = true; - return NULL; - } - void* p = calloc(n, size); - if (p == NULL) { - r->failed = true; - } - return p; -} - -static inline uint64_t nested_modules_reader_get_u64(nested_modules_reader* r) { - const uint8_t* b = nested_modules_reader_take(r, 8); - uint64_t v = 0; - if (b != NULL) { - for (int i = 0; i < 8; i++) { - v |= (uint64_t)b[i] << (8 * i); - } - } - return v; -} - -static inline uint32_t nested_modules_reader_get_u32(nested_modules_reader* r) { - const uint8_t* b = nested_modules_reader_take(r, 4); - if (b == NULL) { - return 0; - } - return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); -} - -static inline uint16_t nested_modules_reader_get_u16(nested_modules_reader* r) { - const uint8_t* b = nested_modules_reader_take(r, 2); - return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; -} - -static inline uint8_t nested_modules_reader_get_u8(nested_modules_reader* r) { - const uint8_t* b = nested_modules_reader_take(r, 1); - return b ? b[0] : (uint8_t)0; -} - -static inline int8_t nested_modules_reader_get_i8(nested_modules_reader* r) { - return (int8_t)nested_modules_reader_get_u8(r); -} - -static inline int16_t nested_modules_reader_get_i16(nested_modules_reader* r) { - return (int16_t)nested_modules_reader_get_u16(r); -} - -static inline int32_t nested_modules_reader_get_i32(nested_modules_reader* r) { - return (int32_t)nested_modules_reader_get_u32(r); -} - -static inline int64_t nested_modules_reader_get_i64(nested_modules_reader* r) { - return (int64_t)nested_modules_reader_get_u64(r); -} - -static inline bool nested_modules_reader_get_bool(nested_modules_reader* r) { - uint8_t v = nested_modules_reader_get_u8(r); - if (v > 1) { - r->failed = true; - return false; - } - return v == 1; -} - -static inline float nested_modules_reader_get_f32(nested_modules_reader* r) { - uint32_t bits = nested_modules_reader_get_u32(r); - float v; - memcpy(&v, &bits, 4); - return v; -} - -static inline double nested_modules_reader_get_f64(nested_modules_reader* r) { - uint64_t bits = nested_modules_reader_get_u64(r); - double v; - memcpy(&v, &bits, 8); - return v; -} - -/* A `uint32_t` length followed by that many bytes, copied into a fresh - NUL-terminated allocation. */ -static inline uint8_t* nested_modules_reader_get_run(nested_modules_reader* r, size_t* out_len) { - size_t n = nested_modules_reader_get_u32(r); - const uint8_t* at = nested_modules_reader_take(r, n); - *out_len = 0; - if (at == NULL) { - return NULL; - } - uint8_t* copy = (uint8_t*)malloc(n + 1); - if (copy == NULL) { - r->failed = true; - return NULL; - } - memcpy(copy, at, n); - copy[n] = 0; - *out_len = n; - return copy; -} - -static inline nested_modules_str nested_modules_reader_get_string(nested_modules_reader* r) { - nested_modules_str v; - v.ptr = (const char*)nested_modules_reader_get_run(r, &v.len); - return v; -} - -static inline nested_modules_bytes nested_modules_reader_get_bytes(nested_modules_reader* r) { - nested_modules_bytes v; - v.ptr = nested_modules_reader_get_run(r, &v.len); - return v; -} - -/* Read an object token and adopt the strong reference it carries. A zero - token in a required position fails the reader. */ -static inline void* nested_modules_reader_get_object(nested_modules_reader* r) { - uint64_t token = nested_modules_reader_get_u64(r); - if (token == 0) { - r->failed = true; - } - return (void*)(uintptr_t)token; -} - -/* --------------------------------------------------------------------------- - * Value types. A record is a struct of its fields; a rich enum is a `tag` - * plus a union `as` with one struct per payload-carrying variant; a list is - * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an - * optional is a pointer that is NULL when absent; an object is its pointer; - * a string or byte string is a view (`ptr` + `len`). - * ------------------------------------------------------------------------- */ - -typedef struct nested_modules_graphics_Canvas nested_modules_graphics_Canvas; -typedef struct nested_modules_graphics_shapes_Shape nested_modules_graphics_shapes_Shape; -typedef struct nested_modules_graphics_shapes_geometry_Detail nested_modules_graphics_shapes_geometry_Detail; - -/** A struct two levels deep. */ -struct nested_modules_graphics_shapes_geometry_Detail { - double area; -}; - -/** A struct one level deep that references both an ancestor enum and a deeper struct. */ -struct nested_modules_graphics_shapes_Shape { - int32_t sides; - /** Reference up to the parent module's enum */ - nested_modules_graphics_Unit unit; - /** Reference down into a deeper module */ - nested_modules_graphics_shapes_geometry_Detail detail; -}; - -/** Top-level struct that references a struct defined one level deeper. */ -struct nested_modules_graphics_Canvas { - int32_t width; - /** Reference down into a nested module */ - nested_modules_graphics_shapes_Shape root; -}; - -/* --------------------------------------------------------------------------- - * Codecs. Each value type T above, and each optional shape, has four - * functions: - * - * T_write(w, v) appends the value at `v` to writer `w` - * T_read(r, out) decodes one value from reader `r` - * T_decode(ptr, len, out) decodes a whole buffer; on a malformed - * buffer it frees what it built and - * returns false - * T_free(v) releases a decoded value - * - * An optional's T_write takes the nullable pointer itself. Writing an - * object stores a fresh `_clone` reference for the reader to adopt; - * decoding adopts each object reference, and T_free destroys it. T_free - * releases only what T_read or T_decode allocated; never call it on a - * value that borrows caller memory. - * ------------------------------------------------------------------------- */ - -static inline void nested_modules_graphics_Canvas_write(nested_modules_writer* w, const nested_modules_graphics_Canvas* v); -static inline void nested_modules_graphics_Canvas_read(nested_modules_reader* r, nested_modules_graphics_Canvas* out); -static inline bool nested_modules_graphics_Canvas_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_Canvas* out); -static inline void nested_modules_graphics_Canvas_free(nested_modules_graphics_Canvas* v); -static inline void nested_modules_graphics_shapes_Shape_write(nested_modules_writer* w, const nested_modules_graphics_shapes_Shape* v); -static inline void nested_modules_graphics_shapes_Shape_read(nested_modules_reader* r, nested_modules_graphics_shapes_Shape* out); -static inline bool nested_modules_graphics_shapes_Shape_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_Shape* out); -static inline void nested_modules_graphics_shapes_Shape_free(nested_modules_graphics_shapes_Shape* v); -static inline void nested_modules_graphics_shapes_geometry_Detail_write(nested_modules_writer* w, const nested_modules_graphics_shapes_geometry_Detail* v); -static inline void nested_modules_graphics_shapes_geometry_Detail_read(nested_modules_reader* r, nested_modules_graphics_shapes_geometry_Detail* out); -static inline bool nested_modules_graphics_shapes_geometry_Detail_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_geometry_Detail* out); -static inline void nested_modules_graphics_shapes_geometry_Detail_free(nested_modules_graphics_shapes_geometry_Detail* v); - -static inline void nested_modules_graphics_Canvas_write(nested_modules_writer* w, const nested_modules_graphics_Canvas* v) { - nested_modules_writer_put_i32(w, v->width); - nested_modules_graphics_shapes_Shape_write(w, &v->root); -} - -static inline void nested_modules_graphics_Canvas_read(nested_modules_reader* r, nested_modules_graphics_Canvas* out) { - memset(out, 0, sizeof *out); - out->width = nested_modules_reader_get_i32(r); - nested_modules_graphics_shapes_Shape_read(r, &out->root); -} - -static inline bool nested_modules_graphics_Canvas_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_Canvas* out) { - nested_modules_reader r = nested_modules_reader_of(ptr, len); - nested_modules_graphics_Canvas_read(&r, out); - if (!nested_modules_reader_finish(&r)) { - nested_modules_graphics_Canvas_free(out); - return false; - } - return true; -} - -static inline void nested_modules_graphics_Canvas_free(nested_modules_graphics_Canvas* v) { - memset(v, 0, sizeof *v); -} - -static inline void nested_modules_graphics_shapes_Shape_write(nested_modules_writer* w, const nested_modules_graphics_shapes_Shape* v) { - nested_modules_writer_put_i32(w, v->sides); - nested_modules_writer_put_i32(w, (int32_t)v->unit); - nested_modules_graphics_shapes_geometry_Detail_write(w, &v->detail); -} - -static inline void nested_modules_graphics_shapes_Shape_read(nested_modules_reader* r, nested_modules_graphics_shapes_Shape* out) { - memset(out, 0, sizeof *out); - out->sides = nested_modules_reader_get_i32(r); - out->unit = (nested_modules_graphics_Unit)nested_modules_reader_get_i32(r); - nested_modules_graphics_shapes_geometry_Detail_read(r, &out->detail); -} - -static inline bool nested_modules_graphics_shapes_Shape_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_Shape* out) { - nested_modules_reader r = nested_modules_reader_of(ptr, len); - nested_modules_graphics_shapes_Shape_read(&r, out); - if (!nested_modules_reader_finish(&r)) { - nested_modules_graphics_shapes_Shape_free(out); - return false; - } - return true; -} - -static inline void nested_modules_graphics_shapes_Shape_free(nested_modules_graphics_shapes_Shape* v) { - memset(v, 0, sizeof *v); -} - -static inline void nested_modules_graphics_shapes_geometry_Detail_write(nested_modules_writer* w, const nested_modules_graphics_shapes_geometry_Detail* v) { - nested_modules_writer_put_f64(w, v->area); -} - -static inline void nested_modules_graphics_shapes_geometry_Detail_read(nested_modules_reader* r, nested_modules_graphics_shapes_geometry_Detail* out) { - memset(out, 0, sizeof *out); - out->area = nested_modules_reader_get_f64(r); -} - -static inline bool nested_modules_graphics_shapes_geometry_Detail_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_geometry_Detail* out) { - nested_modules_reader r = nested_modules_reader_of(ptr, len); - nested_modules_graphics_shapes_geometry_Detail_read(&r, out); - if (!nested_modules_reader_finish(&r)) { - nested_modules_graphics_shapes_geometry_Detail_free(out); - return false; - } - return true; -} - -static inline void nested_modules_graphics_shapes_geometry_Detail_free(nested_modules_graphics_shapes_geometry_Detail* v) { - memset(v, 0, sizeof *v); -} - -#ifdef __cplusplus -} -#endif - -#endif // NESTED_MODULES_BUFFER_H - -// END nested_modules_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_h.snap deleted file mode 100644 index 49955709..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_h.snap +++ /dev/null @@ -1,186 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef NESTED_MODULES_H -#define NESTED_MODULES_H - -#include -#include -#include - -#ifndef NESTED_MODULES_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef NESTED_MODULES_BUILD -# define NESTED_MODULES_API __declspec(dllexport) -# else -# define NESTED_MODULES_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define NESTED_MODULES_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define NESTED_MODULES_API __attribute__((visibility("default"))) -# else -# define NESTED_MODULES_API -# endif -#endif - -#ifndef NESTED_MODULES_DEPRECATED -# if defined(_MSC_VER) -# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define NESTED_MODULES_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* The C ABI revision this header was generated against. The producer - exports nested_modules_abi_version() so a consumer can refuse to load a - library built for a different revision. */ -#define NESTED_MODULES_ABI_VERSION 4u -NESTED_MODULES_API uint32_t nested_modules_abi_version(void); - -/* Error slot written by every fallible call. `message` is NUL-terminated - UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields - serialized in the value buffer format (null when the code declares no - fields). Both are released by nested_modules_error_clear. Positive codes are - the API's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation failed, -5 cancelled. */ -typedef struct nested_modules_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} nested_modules_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface implementations call this to report a failure without - allocating with a foreign allocator. */ -NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); -/* Replace `err`'s payload with a producer-owned copy of `len` bytes: the - fields of a declared error code, which a callback method that throws - attaches after nested_modules_error_set. */ -NESTED_MODULES_API void nested_modules_error_set_payload(nested_modules_error* err, const uint8_t* ptr, size_t len); -NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); - -/* Async completions receive a heap-boxed error the consumer owns; - nested_modules_error_free releases the message, the payload, and the box. - NULL is a no-op. */ -NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); - -/* Allocate a zero-filled run of `len` bytes (NULL for 0). A callback - method returns a string, bytes, or buffer as such a run, which the - producer adopts; any other run is released with nested_modules_free_bytes. */ -NESTED_MODULES_API uint8_t* nested_modules_alloc(size_t len); -/* Release a string, bytes, or value buffer the producer returned, or a - run from nested_modules_alloc, with its exact length. NULL or 0 is a no-op. */ -NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); - -/* Cancellation for `cancellable` async functions. A token starts with - one reference owned by the consumer; the producer takes its own for - each call it is passed to, so the consumer may cancel and destroy its - reference at any time. A cancelled call completes with code -5. */ -typedef struct nested_modules_cancel_token nested_modules_cancel_token; -NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); -NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); -NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); -NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); - -/* Live-allocation counters for leak checks in tests: 0 objects, - 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. - Kind -1 is 1 when the producer counts at all (its `leak-check` - feature) and 0 when every counter reads 0 regardless. */ -NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); - -/* One declaration's fingerprint in a module's contract table: FNV-1a 64 - of its dotted path and of its canonical signature. */ -typedef struct nested_modules_contract_entry { - uint64_t id; - uint64_t hash; -} nested_modules_contract_entry; - -/* The contract of module `graphics`: one entry per declaration, sorted by - id. The producer's table comes from the function below; a consumer - checks each entry it was generated with against it once, before any - other call (producer entries it doesn't know are fine). */ -NESTED_MODULES_API const nested_modules_contract_entry* nested_modules_graphics_contract(size_t* out_len); -#define NESTED_MODULES_GRAPHICS_CONTRACT_LEN 9 -#define NESTED_MODULES_GRAPHICS_CONTRACT { \ - {0x2c29eef9327a6bd4ull, 0x12983d5271651389ull}, /* graphics.Unit */ \ - {0x304088bbc28f0015ull, 0x97e6e809913be6b6ull}, /* graphics.new_canvas */ \ - {0x49f8837532df9ac6ull, 0x32e5b27e81ee0957ull}, /* graphics.Canvas */ \ - {0x55717fa027336b47ull, 0xb2bd3a6bdcce48c8ull}, /* graphics.shapes.describe */ \ - {0x7626b91b8f1f53cdull, 0x867bbf7cf0f41c74ull}, /* graphics.shapes.geometry.compute */ \ - {0xb3efcb50d9a61ab7ull, 0x3cb0207dfad2a149ull}, /* graphics.GraphicsError */ \ - {0xc62b16a9591b744bull, 0x7895610c8487eb1bull}, /* graphics.shapes.Shape */ \ - {0xe13001f4983ccd35ull, 0x16e7de070b4a4cfcull}, /* graphics.shapes.geometry.Detail */ \ - {0xf3f658bcfc5dba62ull, 0x2665ccb47356e746ull}, /* graphics.shapes.make_shape */ \ -} -/* The id of the first expected entry the loaded library lacks or - declares differently, or 0 when these bindings match it. */ -static inline uint64_t nested_modules_graphics_contract_check(void) { - static const nested_modules_contract_entry expected[] = NESTED_MODULES_GRAPHICS_CONTRACT; - size_t len = 0; - const nested_modules_contract_entry* table = nested_modules_graphics_contract(&len); - for (size_t i = 0; i < NESTED_MODULES_GRAPHICS_CONTRACT_LEN; i++) { - size_t lo = 0, hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) lo = mid + 1; else hi = mid; - } - if (lo == len || table[lo].id != expected[i].id || table[lo].hash != expected[i].hash) { - return expected[i].id; - } - } - return 0; -} - -/* - * Strings, bytes, and value buffers cross as (ptr, len) pairs. A - * parameter named "v" expands to a view borrowed for the call: - * const uint8_t* v_ptr, size_t v_len - * Strings are UTF-8 without a NUL terminator; NULL with length 0 is - * the empty value. A returned string, bytes, or buffer is - * `const uint8_t*` with a trailing `size_t* out_len`, owned by the - * caller and released with nested_modules_free_bytes. - * Records, rich enums, lists, maps, and optionals are serialized - * in the little-endian value buffer format; when generated, - * nested_modules_buffer.h declares a C struct and codec for each. - */ - -/** A measurement unit shared with nested submodules. */ -typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; -/** Error codes reported by throwing functions in the `graphics` module tree. */ -typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; - -// Module: graphics -/** Construct a Canvas. */ -NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); - -// Module: graphics_shapes -/** Construct a Shape. */ -NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); -/** Nested function returning a struct owned by an ancestor module. */ -NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); - -// Module: graphics_shapes_geometry -/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ -NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // NESTED_MODULES_H - -// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_buffer_h.snap deleted file mode 100644 index b9aca031..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_buffer_h.snap +++ /dev/null @@ -1,568 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef SHAPES_BUFFER_H -#define SHAPES_BUFFER_H - -#include "shapes.h" - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* --------------------------------------------------------------------------- - * Value buffer runtime. - * - * The wire format is little-endian and packed: fixed-width scalars at their - * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length - * followed by the raw bytes (no NUL terminator), optionals as a presence byte - * followed by the value, lists as a `uint32_t` count followed by the - * elements, maps as a `uint32_t` count followed by alternating keys and - * values, and objects as a `uint64_t` token holding one strong reference. - * ------------------------------------------------------------------------- */ - -/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. - A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be - passed to C string functions when the text has no interior NUL. */ -typedef struct shapes_str { - const char* ptr; - size_t len; -} shapes_str; - -/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ -typedef struct shapes_bytes { - const uint8_t* ptr; - size_t len; -} shapes_bytes; - -/* Borrow a NUL-terminated C string (NULL is the empty string). */ -static inline shapes_str shapes_str_of(const char* s) { - shapes_str v; - v.ptr = s; - v.len = s ? strlen(s) : 0; - return v; -} - -/* Borrow `len` bytes at `ptr`. */ -static inline shapes_bytes shapes_bytes_of(const uint8_t* ptr, size_t len) { - shapes_bytes v; - v.ptr = ptr; - v.len = len; - return v; -} - -/* Release a decoded string. Never call this on a borrowed view. */ -static inline void shapes_str_free(shapes_str* s) { - free((void*)s->ptr); - s->ptr = NULL; - s->len = 0; -} - -/* Release decoded bytes. Never call this on a borrowed view. */ -static inline void shapes_bytes_free(shapes_bytes* b) { - free((void*)b->ptr); - b->ptr = NULL; - b->len = 0; -} - -/* A growable output buffer. Zero-initialize it, write values, pass - `ptr`/`len` to the call, then release it with shapes_writer_free. An - allocation failure sets `failed` and turns later writes into no-ops. */ -typedef struct shapes_writer { - uint8_t* ptr; - size_t len; - size_t cap; - bool failed; -} shapes_writer; - -static inline void shapes_writer_free(shapes_writer* w) { - free(w->ptr); - w->ptr = NULL; - w->len = 0; - w->cap = 0; - w->failed = false; -} - -static inline void shapes_writer_put_raw(shapes_writer* w, const void* data, size_t n) { - if (w->failed || n == 0) { - return; - } - if (n > w->cap - w->len) { - size_t cap = w->cap ? w->cap : 64; - while (cap - w->len < n) { - if (cap > SIZE_MAX / 2) { - w->failed = true; - return; - } - cap *= 2; - } - uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); - if (grown == NULL) { - w->failed = true; - return; - } - w->ptr = grown; - w->cap = cap; - } - memcpy(w->ptr + w->len, data, n); - w->len += n; -} - -static inline void shapes_writer_put_u64(shapes_writer* w, uint64_t v) { - uint8_t b[8]; - for (int i = 0; i < 8; i++) { - b[i] = (uint8_t)(v >> (8 * i)); - } - shapes_writer_put_raw(w, b, 8); -} - -static inline void shapes_writer_put_u32(shapes_writer* w, uint32_t v) { - uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; - shapes_writer_put_raw(w, b, 4); -} - -static inline void shapes_writer_put_u16(shapes_writer* w, uint16_t v) { - uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; - shapes_writer_put_raw(w, b, 2); -} - -static inline void shapes_writer_put_u8(shapes_writer* w, uint8_t v) { - shapes_writer_put_raw(w, &v, 1); -} - -static inline void shapes_writer_put_i8(shapes_writer* w, int8_t v) { - shapes_writer_put_u8(w, (uint8_t)v); -} - -static inline void shapes_writer_put_i16(shapes_writer* w, int16_t v) { - shapes_writer_put_u16(w, (uint16_t)v); -} - -static inline void shapes_writer_put_i32(shapes_writer* w, int32_t v) { - shapes_writer_put_u32(w, (uint32_t)v); -} - -static inline void shapes_writer_put_i64(shapes_writer* w, int64_t v) { - shapes_writer_put_u64(w, (uint64_t)v); -} - -static inline void shapes_writer_put_bool(shapes_writer* w, bool v) { - shapes_writer_put_u8(w, v ? 1 : 0); -} - -static inline void shapes_writer_put_f32(shapes_writer* w, float v) { - uint32_t bits; - memcpy(&bits, &v, 4); - shapes_writer_put_u32(w, bits); -} - -static inline void shapes_writer_put_f64(shapes_writer* w, double v) { - uint64_t bits; - memcpy(&bits, &v, 8); - shapes_writer_put_u64(w, bits); -} - -static inline void shapes_writer_put_len(shapes_writer* w, size_t n) { - if (n > UINT32_MAX) { - w->failed = true; - return; - } - shapes_writer_put_u32(w, (uint32_t)n); -} - -static inline void shapes_writer_put_string(shapes_writer* w, shapes_str v) { - shapes_writer_put_len(w, v.len); - shapes_writer_put_raw(w, v.ptr, v.len); -} - -static inline void shapes_writer_put_bytes(shapes_writer* w, shapes_bytes v) { - shapes_writer_put_len(w, v.len); - shapes_writer_put_raw(w, v.ptr, v.len); -} - -/* Write an object token. The token carries one strong reference, so pass - a reference the buffer may own (the generated codecs pass a fresh - `_clone`). */ -static inline void shapes_writer_put_object(shapes_writer* w, const void* owned) { - shapes_writer_put_u64(w, (uint64_t)(uintptr_t)owned); -} - -/* A cursor over an input buffer. A malformed buffer sets `failed`; every - later read then returns a zero value, so a decoder can always finish and - release what it built. */ -typedef struct shapes_reader { - const uint8_t* ptr; - size_t len; - size_t pos; - bool failed; -} shapes_reader; - -static inline shapes_reader shapes_reader_of(const uint8_t* ptr, size_t len) { - shapes_reader r; - r.ptr = ptr; - r.len = ptr ? len : 0; - r.pos = 0; - r.failed = false; - return r; -} - -/* `true` when every byte was consumed and nothing was malformed. */ -static inline bool shapes_reader_finish(const shapes_reader* r) { - return !r->failed && r->pos == r->len; -} - -static inline const uint8_t* shapes_reader_take(shapes_reader* r, size_t n) { - if (r->failed || n > r->len - r->pos) { - r->failed = true; - return NULL; - } - const uint8_t* at = r->ptr + r->pos; - r->pos += n; - return at; -} - -/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is - zero). `min_wire` is the smallest encoding of one element; a count the - remaining bytes can't hold fails the reader before anything is - allocated. */ -static inline void* shapes_reader_alloc(shapes_reader* r, size_t n, size_t size, size_t min_wire) { - if (r->failed || n == 0) { - return NULL; - } - if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { - r->failed = true; - return NULL; - } - void* p = calloc(n, size); - if (p == NULL) { - r->failed = true; - } - return p; -} - -static inline uint64_t shapes_reader_get_u64(shapes_reader* r) { - const uint8_t* b = shapes_reader_take(r, 8); - uint64_t v = 0; - if (b != NULL) { - for (int i = 0; i < 8; i++) { - v |= (uint64_t)b[i] << (8 * i); - } - } - return v; -} - -static inline uint32_t shapes_reader_get_u32(shapes_reader* r) { - const uint8_t* b = shapes_reader_take(r, 4); - if (b == NULL) { - return 0; - } - return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); -} - -static inline uint16_t shapes_reader_get_u16(shapes_reader* r) { - const uint8_t* b = shapes_reader_take(r, 2); - return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; -} - -static inline uint8_t shapes_reader_get_u8(shapes_reader* r) { - const uint8_t* b = shapes_reader_take(r, 1); - return b ? b[0] : (uint8_t)0; -} - -static inline int8_t shapes_reader_get_i8(shapes_reader* r) { - return (int8_t)shapes_reader_get_u8(r); -} - -static inline int16_t shapes_reader_get_i16(shapes_reader* r) { - return (int16_t)shapes_reader_get_u16(r); -} - -static inline int32_t shapes_reader_get_i32(shapes_reader* r) { - return (int32_t)shapes_reader_get_u32(r); -} - -static inline int64_t shapes_reader_get_i64(shapes_reader* r) { - return (int64_t)shapes_reader_get_u64(r); -} - -static inline bool shapes_reader_get_bool(shapes_reader* r) { - uint8_t v = shapes_reader_get_u8(r); - if (v > 1) { - r->failed = true; - return false; - } - return v == 1; -} - -static inline float shapes_reader_get_f32(shapes_reader* r) { - uint32_t bits = shapes_reader_get_u32(r); - float v; - memcpy(&v, &bits, 4); - return v; -} - -static inline double shapes_reader_get_f64(shapes_reader* r) { - uint64_t bits = shapes_reader_get_u64(r); - double v; - memcpy(&v, &bits, 8); - return v; -} - -/* A `uint32_t` length followed by that many bytes, copied into a fresh - NUL-terminated allocation. */ -static inline uint8_t* shapes_reader_get_run(shapes_reader* r, size_t* out_len) { - size_t n = shapes_reader_get_u32(r); - const uint8_t* at = shapes_reader_take(r, n); - *out_len = 0; - if (at == NULL) { - return NULL; - } - uint8_t* copy = (uint8_t*)malloc(n + 1); - if (copy == NULL) { - r->failed = true; - return NULL; - } - memcpy(copy, at, n); - copy[n] = 0; - *out_len = n; - return copy; -} - -static inline shapes_str shapes_reader_get_string(shapes_reader* r) { - shapes_str v; - v.ptr = (const char*)shapes_reader_get_run(r, &v.len); - return v; -} - -static inline shapes_bytes shapes_reader_get_bytes(shapes_reader* r) { - shapes_bytes v; - v.ptr = shapes_reader_get_run(r, &v.len); - return v; -} - -/* Read an object token and adopt the strong reference it carries. A zero - token in a required position fails the reader. */ -static inline void* shapes_reader_get_object(shapes_reader* r) { - uint64_t token = shapes_reader_get_u64(r); - if (token == 0) { - r->failed = true; - } - return (void*)(uintptr_t)token; -} - -/* --------------------------------------------------------------------------- - * Value types. A record is a struct of its fields; a rich enum is a `tag` - * plus a union `as` with one struct per payload-carrying variant; a list is - * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an - * optional is a pointer that is NULL when absent; an object is its pointer; - * a string or byte string is a view (`ptr` + `len`). - * ------------------------------------------------------------------------- */ - -typedef struct shapes_shapes_Numerics shapes_shapes_Numerics; -typedef struct shapes_shapes_Shape shapes_shapes_Shape; - -/** One field per supported numeric primitive */ -struct shapes_shapes_Numerics { - int8_t a; - int16_t b; - int32_t c; - int64_t d; - uint8_t e; - uint16_t f; - uint32_t g; - uint64_t h; - float i; - double j; -}; - -/** An algebraic shape (sum type with associated data) */ -struct shapes_shapes_Shape { - shapes_shapes_Shape_Tag tag; - union { - /** A circle with a radius */ - struct { - /** Radius in points */ - double radius; - } Circle; - /** An axis-aligned rectangle */ - struct { - float width; - float height; - } Rectangle; - /** An integer-bounded shape (exercises narrow + wide ints) */ - struct { - int16_t min; - int16_t max; - uint64_t weight; - } Bounded; - /** A labeled shape with a small count */ - struct { - shapes_str label; - uint8_t count; - } Labeled; - } as; -}; - -/* --------------------------------------------------------------------------- - * Codecs. Each value type T above, and each optional shape, has four - * functions: - * - * T_write(w, v) appends the value at `v` to writer `w` - * T_read(r, out) decodes one value from reader `r` - * T_decode(ptr, len, out) decodes a whole buffer; on a malformed - * buffer it frees what it built and - * returns false - * T_free(v) releases a decoded value - * - * An optional's T_write takes the nullable pointer itself. Writing an - * object stores a fresh `_clone` reference for the reader to adopt; - * decoding adopts each object reference, and T_free destroys it. T_free - * releases only what T_read or T_decode allocated; never call it on a - * value that borrows caller memory. - * ------------------------------------------------------------------------- */ - -static inline void shapes_shapes_Numerics_write(shapes_writer* w, const shapes_shapes_Numerics* v); -static inline void shapes_shapes_Numerics_read(shapes_reader* r, shapes_shapes_Numerics* out); -static inline bool shapes_shapes_Numerics_decode(const uint8_t* ptr, size_t len, shapes_shapes_Numerics* out); -static inline void shapes_shapes_Numerics_free(shapes_shapes_Numerics* v); -static inline void shapes_shapes_Shape_write(shapes_writer* w, const shapes_shapes_Shape* v); -static inline void shapes_shapes_Shape_read(shapes_reader* r, shapes_shapes_Shape* out); -static inline bool shapes_shapes_Shape_decode(const uint8_t* ptr, size_t len, shapes_shapes_Shape* out); -static inline void shapes_shapes_Shape_free(shapes_shapes_Shape* v); - -static inline void shapes_shapes_Numerics_write(shapes_writer* w, const shapes_shapes_Numerics* v) { - shapes_writer_put_i8(w, v->a); - shapes_writer_put_i16(w, v->b); - shapes_writer_put_i32(w, v->c); - shapes_writer_put_i64(w, v->d); - shapes_writer_put_u8(w, v->e); - shapes_writer_put_u16(w, v->f); - shapes_writer_put_u32(w, v->g); - shapes_writer_put_u64(w, v->h); - shapes_writer_put_f32(w, v->i); - shapes_writer_put_f64(w, v->j); -} - -static inline void shapes_shapes_Numerics_read(shapes_reader* r, shapes_shapes_Numerics* out) { - memset(out, 0, sizeof *out); - out->a = shapes_reader_get_i8(r); - out->b = shapes_reader_get_i16(r); - out->c = shapes_reader_get_i32(r); - out->d = shapes_reader_get_i64(r); - out->e = shapes_reader_get_u8(r); - out->f = shapes_reader_get_u16(r); - out->g = shapes_reader_get_u32(r); - out->h = shapes_reader_get_u64(r); - out->i = shapes_reader_get_f32(r); - out->j = shapes_reader_get_f64(r); -} - -static inline bool shapes_shapes_Numerics_decode(const uint8_t* ptr, size_t len, shapes_shapes_Numerics* out) { - shapes_reader r = shapes_reader_of(ptr, len); - shapes_shapes_Numerics_read(&r, out); - if (!shapes_reader_finish(&r)) { - shapes_shapes_Numerics_free(out); - return false; - } - return true; -} - -static inline void shapes_shapes_Numerics_free(shapes_shapes_Numerics* v) { - memset(v, 0, sizeof *v); -} - -static inline void shapes_shapes_Shape_write(shapes_writer* w, const shapes_shapes_Shape* v) { - shapes_writer_put_i32(w, (int32_t)v->tag); - switch (v->tag) { - case shapes_shapes_Shape_Empty: - break; - case shapes_shapes_Shape_Circle: - shapes_writer_put_f64(w, v->as.Circle.radius); - break; - case shapes_shapes_Shape_Rectangle: - shapes_writer_put_f32(w, v->as.Rectangle.width); - shapes_writer_put_f32(w, v->as.Rectangle.height); - break; - case shapes_shapes_Shape_Bounded: - shapes_writer_put_i16(w, v->as.Bounded.min); - shapes_writer_put_i16(w, v->as.Bounded.max); - shapes_writer_put_u64(w, v->as.Bounded.weight); - break; - case shapes_shapes_Shape_Labeled: - shapes_writer_put_string(w, v->as.Labeled.label); - shapes_writer_put_u8(w, v->as.Labeled.count); - break; - default: - w->failed = true; - break; - } -} - -static inline void shapes_shapes_Shape_read(shapes_reader* r, shapes_shapes_Shape* out) { - memset(out, 0, sizeof *out); - int32_t tag = shapes_reader_get_i32(r); - switch (tag) { - case shapes_shapes_Shape_Empty: - out->tag = shapes_shapes_Shape_Empty; - break; - case shapes_shapes_Shape_Circle: - out->tag = shapes_shapes_Shape_Circle; - out->as.Circle.radius = shapes_reader_get_f64(r); - break; - case shapes_shapes_Shape_Rectangle: - out->tag = shapes_shapes_Shape_Rectangle; - out->as.Rectangle.width = shapes_reader_get_f32(r); - out->as.Rectangle.height = shapes_reader_get_f32(r); - break; - case shapes_shapes_Shape_Bounded: - out->tag = shapes_shapes_Shape_Bounded; - out->as.Bounded.min = shapes_reader_get_i16(r); - out->as.Bounded.max = shapes_reader_get_i16(r); - out->as.Bounded.weight = shapes_reader_get_u64(r); - break; - case shapes_shapes_Shape_Labeled: - out->tag = shapes_shapes_Shape_Labeled; - out->as.Labeled.label = shapes_reader_get_string(r); - out->as.Labeled.count = shapes_reader_get_u8(r); - break; - default: - r->failed = true; - break; - } -} - -static inline bool shapes_shapes_Shape_decode(const uint8_t* ptr, size_t len, shapes_shapes_Shape* out) { - shapes_reader r = shapes_reader_of(ptr, len); - shapes_shapes_Shape_read(&r, out); - if (!shapes_reader_finish(&r)) { - shapes_shapes_Shape_free(out); - return false; - } - return true; -} - -static inline void shapes_shapes_Shape_free(shapes_shapes_Shape* v) { - switch (v->tag) { - case shapes_shapes_Shape_Labeled: - shapes_str_free(&v->as.Labeled.label); - break; - default: - break; - } - memset(v, 0, sizeof *v); -} - -#ifdef __cplusplus -} -#endif - -#endif // SHAPES_BUFFER_H - -// END shapes_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_h.snap deleted file mode 100644 index 968d33c1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_h.snap +++ /dev/null @@ -1,191 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#ifndef SHAPES_H -#define SHAPES_H - -#include -#include -#include - -#ifndef SHAPES_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef SHAPES_BUILD -# define SHAPES_API __declspec(dllexport) -# else -# define SHAPES_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define SHAPES_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define SHAPES_API __attribute__((visibility("default"))) -# else -# define SHAPES_API -# endif -#endif - -#ifndef SHAPES_DEPRECATED -# if defined(_MSC_VER) -# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define SHAPES_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* The C ABI revision this header was generated against. The producer - exports shapes_abi_version() so a consumer can refuse to load a - library built for a different revision. */ -#define SHAPES_ABI_VERSION 4u -SHAPES_API uint32_t shapes_abi_version(void); - -/* Error slot written by every fallible call. `message` is NUL-terminated - UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields - serialized in the value buffer format (null when the code declares no - fields). Both are released by shapes_error_clear. Positive codes are - the API's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation failed, -5 cancelled. */ -typedef struct shapes_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} shapes_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface implementations call this to report a failure without - allocating with a foreign allocator. */ -SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); -/* Replace `err`'s payload with a producer-owned copy of `len` bytes: the - fields of a declared error code, which a callback method that throws - attaches after shapes_error_set. */ -SHAPES_API void shapes_error_set_payload(shapes_error* err, const uint8_t* ptr, size_t len); -SHAPES_API void shapes_error_clear(shapes_error* err); - -/* Async completions receive a heap-boxed error the consumer owns; - shapes_error_free releases the message, the payload, and the box. - NULL is a no-op. */ -SHAPES_API void shapes_error_free(shapes_error* err); - -/* Allocate a zero-filled run of `len` bytes (NULL for 0). A callback - method returns a string, bytes, or buffer as such a run, which the - producer adopts; any other run is released with shapes_free_bytes. */ -SHAPES_API uint8_t* shapes_alloc(size_t len); -/* Release a string, bytes, or value buffer the producer returned, or a - run from shapes_alloc, with its exact length. NULL or 0 is a no-op. */ -SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); - -/* Cancellation for `cancellable` async functions. A token starts with - one reference owned by the consumer; the producer takes its own for - each call it is passed to, so the consumer may cancel and destroy its - reference at any time. A cancelled call completes with code -5. */ -typedef struct shapes_cancel_token shapes_cancel_token; -SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); -SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); -SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); -SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); - -/* Live-allocation counters for leak checks in tests: 0 objects, - 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. - Kind -1 is 1 when the producer counts at all (its `leak-check` - feature) and 0 when every counter reads 0 regardless. */ -SHAPES_API uint64_t shapes_debug_live(int32_t kind); - -/* One declaration's fingerprint in a module's contract table: FNV-1a 64 - of its dotted path and of its canonical signature. */ -typedef struct shapes_contract_entry { - uint64_t id; - uint64_t hash; -} shapes_contract_entry; - -/* The contract of module `shapes`: one entry per declaration, sorted by - id. The producer's table comes from the function below; a consumer - checks each entry it was generated with against it once, before any - other call (producer entries it doesn't know are fine). */ -SHAPES_API const shapes_contract_entry* shapes_shapes_contract(size_t* out_len); -#define SHAPES_SHAPES_CONTRACT_LEN 7 -#define SHAPES_SHAPES_CONTRACT { \ - {0x0d7b3eec49243e7aull, 0x5f232311808e1b1full}, /* shapes.Channel */ \ - {0x183fa8198100860eull, 0xe40c6993cb31d359ull}, /* shapes.describe */ \ - {0x2d1395eb837b848eull, 0x42202a0c12f3a16eull}, /* shapes.sum_bytes */ \ - {0x7f9a5b87ba09b86bull, 0xc5b32fb0efbbaa17ull}, /* shapes.scale */ \ - {0xc8edd779240fda17ull, 0x68e1be78fddb20e4ull}, /* shapes.Numerics */ \ - {0xd3d3881b2fd18a62ull, 0xf24d0eb76a99d918ull}, /* shapes.make_numerics */ \ - {0xf010844afff5ac3cull, 0x50de28ae9b41ff15ull}, /* shapes.Shape */ \ -} -/* The id of the first expected entry the loaded library lacks or - declares differently, or 0 when these bindings match it. */ -static inline uint64_t shapes_shapes_contract_check(void) { - static const shapes_contract_entry expected[] = SHAPES_SHAPES_CONTRACT; - size_t len = 0; - const shapes_contract_entry* table = shapes_shapes_contract(&len); - for (size_t i = 0; i < SHAPES_SHAPES_CONTRACT_LEN; i++) { - size_t lo = 0, hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) lo = mid + 1; else hi = mid; - } - if (lo == len || table[lo].id != expected[i].id || table[lo].hash != expected[i].hash) { - return expected[i].id; - } - } - return 0; -} - -/* - * Strings, bytes, and value buffers cross as (ptr, len) pairs. A - * parameter named "v" expands to a view borrowed for the call: - * const uint8_t* v_ptr, size_t v_len - * Strings are UTF-8 without a NUL terminator; NULL with length 0 is - * the empty value. A returned string, bytes, or buffer is - * `const uint8_t*` with a trailing `size_t* out_len`, owned by the - * caller and released with shapes_free_bytes. - * Records, rich enums, lists, maps, and optionals are serialized - * in the little-endian value buffer format; when generated, - * shapes_buffer.h declares a C struct and codec for each. - */ - -/** An algebraic shape (sum type with associated data) */ -typedef enum { - /** The empty shape (a unit variant) */ - shapes_shapes_Shape_Empty = 0, - /** A circle with a radius */ - shapes_shapes_Shape_Circle = 1, - /** An axis-aligned rectangle */ - shapes_shapes_Shape_Rectangle = 2, - /** An integer-bounded shape (exercises narrow + wide ints) */ - shapes_shapes_Shape_Bounded = 3, - /** A labeled shape with a small count */ - shapes_shapes_Shape_Labeled = 4 -} shapes_shapes_Shape_Tag; -/** A plain C-style enum (no payloads) */ -typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; - -// Module: shapes -/** Render a shape to a string */ -SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); -/** Scale a shape by a factor, returning a new shape */ -SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); -/** Sum a list of bytes into a wide integer (numerics smoke) */ -SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); -/** Build a Numerics struct from every numeric primitive */ -SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // SHAPES_H - -// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_hpp.snap deleted file mode 100644 index 7e11b634..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_hpp.snap +++ /dev/null @@ -1,946 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#pragma once - -#include "docs_everywhere.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace docs_everywhere { - -/** - * An error a call that declares errors reports. - * - * A positive code is a declared code of the module's error domain, thrown as - * that domain's typed subclass. A negative code is a runtime failure: -1 a - * generic producer error, -2 a producer panic, -3 a marshalling failure, and - * -4 a callback-interface implementation that failed. A cancelled async call - * (-5) throws Cancelled. - */ -class Error : public std::runtime_error { - int32_t code_; - -public: - /** Builds an error carrying `code` and `message`. */ - Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} - - /** The numeric error code. */ - int32_t code() const noexcept { return code_; } -}; - -/** Thrown by an async call that was cancelled through its CancelToken (code -5). */ -class Cancelled : public Error { -public: - /** Builds a cancellation error carrying the producer's message. */ - explicit Cancelled(const std::string& message) : Error(-5, message) {} -}; - -/** - * Thrown by a call that declares no errors when it fails anyway: a producer - * panic (-2), a marshalling failure (-3), a callback-interface failure the - * producer let through (-4), or another runtime failure (-1). These are bugs, - * so no signature declares them; `what()` names the code and the producer's - * message. - */ -class InternalError : public std::runtime_error { - int32_t code_; - std::string message_; - -public: - /** Builds an internal error carrying `code` and the producer's `message`. */ - InternalError(int32_t code, const std::string& message) - : std::runtime_error("docs_everywhere: runtime error " + std::to_string(code) + ": " + message), - code_(code), - message_(message) {} - - /** The runtime error code (negative). */ - int32_t code() const noexcept { return code_; } - - /** The producer's message. */ - const std::string& message() const noexcept { return message_; } -}; - -/** - * Thrown on first use when the linked docs_everywhere library implements a - * different C ABI revision than this header, or lacks or changed a - * declaration this header was generated with. - */ -class LoadError : public std::runtime_error { -public: - /** Builds a load error describing the mismatch. */ - explicit LoadError(const std::string& message) : std::runtime_error(message) {} -}; - -/** - * Tag selecting the constructor that adopts a raw C pointer (one strong - * reference the wrapper then owns), in the style of std::adopt_lock. - */ -struct adopt_t { - explicit adopt_t() = default; -}; - -/** The adopt_t tag value: `Store s(adopt, raw)`. */ -inline constexpr adopt_t adopt{}; - -/** - * A cancellation token for cancellable async functions. - * - * Create one, pass it to any number of cancellable calls, and call cancel() - * from any thread. A cancelled call settles its future with Cancelled. Each - * call takes its own reference to the native token, so the token can be - * destroyed at any time; the destructor releases this wrapper's reference. - */ -class CancelToken { - docs_everywhere_cancel_token* handle_; - - struct none_t {}; - explicit CancelToken(none_t) noexcept : handle_(nullptr) {} - -public: - /** Creates a new, uncancelled token. */ - CancelToken() : handle_(docs_everywhere_cancel_token_create()) { - if (handle_ == nullptr) throw std::bad_alloc(); - } - - /** Releases this wrapper's reference to the native token. */ - ~CancelToken() { - if (handle_ != nullptr) docs_everywhere_cancel_token_destroy(handle_); - } - - CancelToken(const CancelToken&) = delete; - CancelToken& operator=(const CancelToken&) = delete; - - /** Transfers the native token; `other` becomes empty. */ - CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current token and takes over `other`'s. */ - CancelToken& operator=(CancelToken&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) docs_everywhere_cancel_token_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** The empty token a cancellable call uses when none is passed. */ - static const CancelToken& none() noexcept { - static const CancelToken token{none_t{}}; - return token; - } - - /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ - void cancel() const noexcept { - if (handle_ != nullptr) docs_everywhere_cancel_token_cancel(handle_); - } - - /** Whether cancel() has been called. */ - bool is_cancelled() const noexcept { - return handle_ != nullptr && docs_everywhere_cancel_token_is_cancelled(handle_); - } - - /** The native token, borrowed (null for an empty token). */ - docs_everywhere_cancel_token* handle() const noexcept { return handle_; } -}; - -namespace detail { - -/** The exception for a failed call that declares no errors: Cancelled for -5, else InternalError. */ -inline std::exception_ptr make_internal_error(int32_t code, const std::string& message) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - return std::make_exception_ptr(InternalError(code, message)); -} - -/** The message of a nonzero error slot. */ -inline std::string error_message(const docs_everywhere_error& err) { - return std::string(err.message != nullptr ? err.message : "unknown error"); -} - -/** Throws InternalError if `err` carries a nonzero code (a call that declares no errors). */ -inline void check(docs_everywhere_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_internal_error(err.code, error_message(err)); - docs_everywhere_error_clear(&err); - std::rethrow_exception(ex); -} - -/** Copies a producer-returned UTF-8 run into a string, then releases it. */ -inline std::string take_string(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::string(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { docs_everywhere_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::string(reinterpret_cast(ptr), len); -} - -/** Copies a producer-returned byte run into a vector, then releases it. */ -inline std::vector take_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::vector(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { docs_everywhere_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::vector(ptr, ptr + len); -} - -/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ -inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { - if (ptr == nullptr || len == 0) return std::string_view(); - return std::string_view(reinterpret_cast(ptr), len); -} - -/** Copies a borrowed byte run (null when empty) into a vector. */ -inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr || len == 0) return std::vector(); - return std::vector(ptr, ptr + len); -} - -/** The data pointer of a string argument, as the C ABI expects it. */ -inline const uint8_t* utf8(std::string_view s) noexcept { - return reinterpret_cast(s.data()); -} - -/** - * Hands `len` bytes from `data` to the producer through a callback method's - * out slots: copies them into a run from docs_everywhere_alloc, which the - * producer adopts and frees. - */ -inline void hand_over(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = nullptr; - if (len > 0) { - run = docs_everywhere_alloc(len); - if (run == nullptr) throw std::bad_alloc(); - std::memcpy(run, data, len); - } - *out_ptr = run; - *out_len = len; -} - -/** One declaration this header was generated with: its contract id and hash, and its dotted path. */ -struct ContractEntry { - /** FNV-1a 64 of the declaration's dotted path. */ - uint64_t id; - /** FNV-1a 64 of the declaration's canonical signature. */ - uint64_t hash; - /** The declaration's dotted path (`kv.Store.put`). */ - const char* path; -}; - -/** - * Why the contract table `contract` returns doesn't satisfy `expected`, - * naming the first declaration it lacks or declares differently, or an empty - * string when it has them all. Entries the producer has and `expected` - * doesn't are fine. - */ -template -std::string contract_mismatch(const docs_everywhere_contract_entry* (*contract)(size_t*), - const ContractEntry (&expected)[N]) { - size_t len = 0; - const docs_everywhere_contract_entry* table = contract(&len); - if (table == nullptr) len = 0; - for (const ContractEntry& want : expected) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < want.id) { - lo = mid + 1; - } else { - hi = mid; - } - } - if (lo == len || table[lo].id != want.id) { - return std::string(want.path) + " is missing from the library"; - } - if (table[lo].hash != want.hash) { - return std::string(want.path) + " changed since these bindings were generated"; - } - } - return std::string(); -} - -/** The declarations of module `docs` this header was generated with. */ -inline constexpr ContractEntry docs_contract[] = { - {0x00b2187fd75634ecull, 0x2263c6f787bf6081ull, "docs.publish_async"}, - {0x12fc242b9dc6444cull, 0x96405f484a642b31ull, "docs.Notebook.insert"}, - {0x2213822ed05cd5bdull, 0x26c5833392aa63ceull, "docs.Notebook.new"}, - {0x2bd762bcdaa207ffull, 0x1b0ebab2d04c49b0ull, "docs.SavedListener"}, - {0x2e086c45e9d617deull, 0x4d43bf32358c01ffull, "docs.Notebook.find"}, - {0x4817dc98197ad960ull, 0xf8017b5a7d58b2c7ull, "docs.Notebook.size"}, - {0x614de47452d5a55cull, 0xd9aae1472494ffefull, "docs.get_document"}, - {0x67c5cb9fc119bcd5ull, 0x08d5b25a1a22a7faull, "docs.Document"}, - {0x8d484bf2d78c9b70ull, 0xe9093cf96912194cull, "docs.Notebook.max_documents"}, - {0xaea61d6451691c67ull, 0xe4704128b3524b3dull, "docs.watch"}, - {0xd68d6ad1540cdec5ull, 0x01afeb40b0a8bfe1ull, "docs.Notebook"}, - {0xdb0601444a4760ccull, 0xe56c69629b444797ull, "docs.create_document"}, - {0xe12d6f4838347b3eull, 0xa9bab86646d84f51ull, "docs.Status"}, - {0xee2f8d25b3313b50ull, 0x9691232f35439a2bull, "docs.DocsErrors"}, -}; - -} // namespace detail - -/** - * Verifies that the linked docs_everywhere library implements C ABI revision - * 4 and every declaration this header was generated with, throwing - * LoadError naming the first mismatch. The check runs once; every free - * function, constructor, and static member calls this before its first - * native call, so calling it at startup is optional. - */ -inline void check_library() { - static const std::string failure = []() -> std::string { - uint32_t abi = docs_everywhere_abi_version(); - if (abi != DOCS_EVERYWHERE_ABI_VERSION) { - return "docs_everywhere: C ABI revision mismatch (header " + std::to_string(DOCS_EVERYWHERE_ABI_VERSION) + - ", library " + std::to_string(abi) + ")"; - } - if (std::string why = detail::contract_mismatch(docs_everywhere_docs_contract, detail::docs_contract); !why.empty()) { - return "docs_everywhere: " + why; - } - return std::string(); - }(); - if (!failure.empty()) throw LoadError(failure); -} - -namespace detail { - -/** - * Serializes values into the value-buffer wire format: little-endian, packed - * with no alignment, lengths and element counts as u32. - */ -class BufferWriter { - std::vector buf_; - - template - void append_le(T v) { - for (size_t i = 0; i < sizeof(T); ++i) { - buf_.push_back(static_cast(v >> (8 * i))); - } - } - -public: - /** Pointer to the encoded bytes. */ - const uint8_t* data() const noexcept { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const noexcept { return buf_.size(); } - - void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } - void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } - void write_u8(uint8_t v) { buf_.push_back(v); } - void write_i16(int16_t v) { append_le(static_cast(v)); } - void write_u16(uint16_t v) { append_le(v); } - void write_i32(int32_t v) { append_le(static_cast(v)); } - void write_u32(uint32_t v) { append_le(v); } - void write_i64(int64_t v) { append_le(static_cast(v)); } - void write_u64(uint64_t v) { append_le(v); } - - void write_f32(float v) { - uint32_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - void write_f64(double v) { - uint64_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - /** Writes a byte length or an element count as a u32. */ - void write_len(size_t n) { - if (static_cast(n) > UINT32_MAX) throw std::length_error("value buffer length exceeds u32"); - append_le(static_cast(n)); - } - - void write_string(std::string_view v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - void write_bytes(const std::vector& v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - /** Writes an optional's presence flag: 0 absent, 1 present. */ - void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } - - /** - * Writes an object token: a fresh strong reference from the wrapper's - * `_clone`, which the reader adopts while `v` keeps its own. - */ - template - void write_object(const T& v) { - write_u64(static_cast(reinterpret_cast(v.clone_handle()))); - } -}; - -/** - * Decodes values from the value-buffer wire format. A malformed buffer is a - * producer bug (both sides are generated from one API), so every decode - * failure throws InternalError with the marshalling code -3. Object tokens - * adopted before a failure live in RAII wrappers that release them on - * unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - void require(size_t n, const char* what) const { - if (len_ - pos_ < n) fail(what); - } - - template - T read_le(const char* what) { - require(sizeof(T), what); - uint64_t v = 0; - for (size_t i = 0; i < sizeof(T); ++i) { - v |= static_cast(data_[pos_ + i]) << (8 * i); - } - pos_ += sizeof(T); - return static_cast(v); - } - -public: - /** Throws the marshalling failure (-3) for a malformed buffer. */ - [[noreturn]] static void fail(const char* what) { - throw InternalError(-3, std::string("malformed value buffer: ") + what); - } - - /** Reads from the `len` bytes at `data` (null when empty). */ - BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const noexcept { return len_ - pos_; } - - bool read_bool() { - uint8_t b = read_le("bool"); - if (b > 1) fail("bool byte out of range"); - return b != 0; - } - - int8_t read_i8() { return read_le("i8"); } - uint8_t read_u8() { return read_le("u8"); } - int16_t read_i16() { return read_le("i16"); } - uint16_t read_u16() { return read_le("u16"); } - int32_t read_i32() { return read_le("i32"); } - uint32_t read_u32() { return read_le("u32"); } - int64_t read_i64() { return read_le("i64"); } - uint64_t read_u64() { return read_le("u64"); } - - float read_f32() { - uint32_t bits = read_le("f32"); - float v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - double read_f64() { - uint64_t bits = read_le("f64"); - double v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - /** Reads a byte length, rejecting one larger than the bytes remaining. */ - size_t read_len() { - uint32_t n = read_le("length"); - if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); - return static_cast(n); - } - - /** - * Reads an element count. Elements may encode to zero bytes, so a count - * larger than the bytes remaining is legal; callers cap preallocation - * with reserve_hint(). - */ - size_t read_count() { return static_cast(read_le("count")); } - - /** A preallocation size for `n` elements that a malformed count can't inflate. */ - size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } - - std::string read_string() { - size_t n = read_len(); - std::string v(reinterpret_cast(data_) + pos_, n); - pos_ += n; - return v; - } - - std::vector read_bytes() { - size_t n = read_len(); - std::vector v(data_ + pos_, data_ + pos_ + n); - pos_ += n; - return v; - } - - bool read_option_flag() { - uint8_t b = read_le("option flag"); - if (b > 1) fail("option flag byte out of range"); - return b != 0; - } - - /** Reads an object token, adopting the strong reference it carries into a `T` wrapper. */ - template - T read_object() { - auto* raw = reinterpret_cast(static_cast(read_u64())); - if (raw == nullptr) fail("null object token"); - return T(adopt, raw); - } - - /** Rejects unconsumed bytes after decoding a complete value. */ - void expect_end() const { - if (pos_ != len_) fail("trailing bytes after value"); - } -}; - -/** Releases a producer-allocated buffer with docs_everywhere_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; - - BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} - BufferGuard(const BufferGuard&) = delete; - BufferGuard& operator=(const BufferGuard&) = delete; - - ~BufferGuard() { - if (ptr != nullptr) docs_everywhere_free_bytes(const_cast(ptr), len); - } -}; - -/** Decodes one complete `T` from a borrowed buffer with `read`, rejecting trailing bytes. */ -template -T decode(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferReader r(ptr, len); - T value = read(r); - r.expect_end(); - return value; -} - -/** Decodes one complete `T` from a producer-returned buffer with `read`, then releases the buffer. */ -template -T take(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferGuard guard(ptr, len); - return decode(ptr, len, read); -} - -} // namespace detail - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -enum class Status : int32_t { - /** Document is being edited */ - Draft = 0, - /** Document is publicly visible */ - Published = 1, - /** Document is hidden from listings */ - Archived = 2, -}; - -struct Document; -class Notebook; -class SavedListener; - -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -class Notebook { - docs_everywhere_docs_Notebook* raw_; - -public: - /** The C type this class wraps. */ - using raw_type = docs_everywhere_docs_Notebook; - - /** Adopts one strong reference to a producer object: `Notebook(adopt, raw)`. */ - explicit Notebook(adopt_t, docs_everywhere_docs_Notebook* h) noexcept : raw_(h) {} - - /** Releases this wrapper's reference; the object is dropped with its last one. */ - ~Notebook() { - if (raw_ != nullptr) docs_everywhere_docs_Notebook_destroy(raw_); - } - - /** Copies share the object: the copy takes a new strong reference. */ - Notebook(const Notebook& other) : raw_(docs_everywhere_docs_Notebook_clone(other.raw_)) {} - - /** Takes a new reference to `other`'s object and releases the current one. */ - Notebook& operator=(const Notebook& other) { - if (this != &other) { - docs_everywhere_docs_Notebook* h = docs_everywhere_docs_Notebook_clone(other.raw_); - if (raw_ != nullptr) docs_everywhere_docs_Notebook_destroy(raw_); - raw_ = h; - } - return *this; - } - - /** Transfers `other`'s reference; `other` becomes empty. */ - Notebook(Notebook&& other) noexcept : raw_(other.raw_) { other.raw_ = nullptr; } - - /** Releases the current reference and takes over `other`'s. */ - Notebook& operator=(Notebook&& other) noexcept { - if (this != &other) { - if (raw_ != nullptr) docs_everywhere_docs_Notebook_destroy(raw_); - raw_ = other.raw_; - other.raw_ = nullptr; - } - return *this; - } - - /** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */ - const docs_everywhere_docs_Notebook* handle() const noexcept { return raw_; } - - /** A new strong reference to the object, which the caller owns. */ - docs_everywhere_docs_Notebook* clone_handle() const { return docs_everywhere_docs_Notebook_clone(raw_); } - - /** Create an empty notebook */ - Notebook(); - - /** - * Add a document, returning its position in the notebook - * - * @param document The document to add - */ - int32_t insert(const Document& document) const; - - /** - * Look up a document by identifier, failing when none matches - * - * @param id Identifier of the document to find - * @throws DocsErrorsError when the call fails with one of the domain's codes. - */ - Document find(int64_t id) const; - - /** The number of documents currently held */ - int32_t size() const; - - /** The largest number of documents one notebook will hold */ - static int32_t max_documents(); - -}; - -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -struct Document { - /** Stable opaque identifier */ - int64_t id; - /** Human-readable title shown in lists */ - std::string title; - /** Current lifecycle status */ - Status status; - /** Free-form tags for grouping */ - std::vector tags; -}; - -namespace detail { - -inline void write_Document(BufferWriter& w, const Document& v); -inline Document read_Document(BufferReader& r); -inline void write_list_string(BufferWriter& w, const std::vector& v); -inline std::vector read_list_string(BufferReader& r); -inline void write_opt_Document(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Document(BufferReader& r); - -/** Writes one `Document` to a value buffer. */ -inline void write_Document(BufferWriter& w, const Document& v) { - w.write_i64(v.id); - w.write_string(v.title); - w.write_i32(static_cast(v.status)); - detail::write_list_string(w, v.tags); -} - -/** Reads one `Document` from a value buffer. */ -inline Document read_Document(BufferReader& r) { - return Document{ - r.read_i64(), - r.read_string(), - static_cast(r.read_i32()), - detail::read_list_string(r), - }; -} - -/** Writes one `[string]` to a value buffer. */ -inline void write_list_string(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_string(item); -} - -/** Reads one `[string]` from a value buffer. */ -inline std::vector read_list_string(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(r.read_string()); - return v; -} - -/** Writes one `Document?` to a value buffer. */ -inline void write_opt_Document(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) detail::write_Document(w, *v); -} - -/** Reads one `Document?` from a value buffer. */ -inline std::optional read_opt_Document(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(detail::read_Document(r)); -} - -} // namespace detail - -/** The errors the `docs` module's throwing calls report; catch a code's subclass or this. */ -class DocsErrorsError : public Error { -public: - /** Builds an error carrying a domain `code` and `message`. */ - DocsErrorsError(int32_t code, const std::string& message) : Error(code, message) {} -}; - -/** - * Returned when no document matches the requested id - * - * Code 1. - */ -class NotFoundError : public DocsErrorsError { -public: - /** Builds the error with the producer's `message` and the code's fields. */ - explicit NotFoundError(const std::string& message) : DocsErrorsError(1, message) {} -}; - -/** - * Raised when attempting to move into a forbidden state - * - * Code 2. - */ -class InvalidStatusError : public DocsErrorsError { -public: - /** Builds the error with the producer's `message` and the code's fields. */ - explicit InvalidStatusError(const std::string& message) : DocsErrorsError(2, message) {} -}; - -namespace detail { - -/** The exception for a failed `DocsErrorsError` call: its code's subclass, Cancelled, or the root Error. */ -inline std::exception_ptr make_docs_error(int32_t code, const std::string& message, const uint8_t* payload_ptr, size_t payload_len) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - if (code < 0) return std::make_exception_ptr(Error(code, message)); - (void)payload_ptr; - (void)payload_len; - switch (code) { - case 1: return std::make_exception_ptr(NotFoundError(message)); - case 2: return std::make_exception_ptr(InvalidStatusError(message)); - default: return std::make_exception_ptr(DocsErrorsError(code, message)); - } -} - -/** Throws the typed `DocsErrorsError` exception if `err` carries a nonzero code. */ -inline void check_docs(docs_everywhere_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_docs_error(err.code, error_message(err), err.payload_ptr, err.payload_len); - docs_everywhere_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - * - * - * Subclass it and pass a `std::shared_ptr` where the API takes a `SavedListener`. The producer may call the methods from any thread, so they must be thread-safe, until it releases the implementation (also from any thread). An exception a method throws reaches the producer as a callback failure (code -4). - */ -class SavedListener { -public: - virtual ~SavedListener() = default; - - /** Fires after a document has been persisted successfully */ - virtual void on_saved(int64_t doc_id, int32_t revision) = 0; -}; - -namespace detail { - -/** Trampolines adapting a `SavedListener` implementation to `docs_everywhere_docs_SavedListener_vtable`. */ -struct SavedListener_trampolines { - static void on_saved(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err) { - try { - SavedListener& impl = **static_cast*>(ctx); - impl.on_saved(doc_id, revision); - } catch (const std::exception& e) { - docs_everywhere_error_set(out_err, -4, e.what()); - } catch (...) { - docs_everywhere_error_set(out_err, -4, "SavedListener::on_saved threw a non-standard exception"); - } - } - - /** Deletes the implementation box once the producer releases it. */ - static void free_ctx(void* ctx) { - delete static_cast*>(ctx); - } -}; - -/** The vtable every `SavedListener` implementation is passed with. */ -inline const docs_everywhere_docs_SavedListener_vtable& SavedListener_vtable() { - static const docs_everywhere_docs_SavedListener_vtable vtable = { - static_cast(sizeof(docs_everywhere_docs_SavedListener_vtable)), - 0, - &SavedListener_trampolines::free_ctx, - &SavedListener_trampolines::on_saved, - }; - return vtable; -} - -} // namespace detail - -inline Notebook::Notebook() : raw_(nullptr) { - check_library(); - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_Notebook_new(&err); - detail::check(err); - raw_ = result; -} - -inline int32_t Notebook::insert(const Document& document) const { - detail::BufferWriter document_buf; - detail::write_Document(document_buf, document); - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_Notebook_insert(raw_, document_buf.data(), document_buf.size(), &err); - detail::check(err); - return result; -} - -inline Document Notebook::find(int64_t id) const { - size_t out_len = 0; - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_Notebook_find(raw_, id, &out_len, &err); - detail::check_docs(err); - return detail::take(result, out_len, &detail::read_Document); -} - -inline int32_t Notebook::size() const { - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_Notebook_size(raw_, &err); - detail::check(err); - return result; -} - -inline int32_t Notebook::max_documents() { - check_library(); - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_Notebook_max_documents(&err); - detail::check(err); - return result; -} - -namespace docs { - -/** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * - * - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - */ -inline Document create_document(std::string_view title, const std::vector& tags) { - check_library(); - detail::BufferWriter tags_buf; - detail::write_list_string(tags_buf, tags); - size_t out_len = 0; - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_create_document(detail::utf8(title), title.size(), tags_buf.data(), tags_buf.size(), &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Document); -} - -/** - * Look up a document by identifier - * - * @param id Identifier returned from create_document - */ -inline std::optional get_document(int64_t id) { - check_library(); - size_t out_len = 0; - docs_everywhere_error err{}; - auto result = docs_everywhere_docs_get_document(id, &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_opt_Document); -} - -/** - * Publish a document in the background - * - * @param id Identifier of the document to publish - * @return A future settled from a producer thread; get() rethrows the call's error. - */ -inline std::future publish_async(int64_t id) { - check_library(); - auto promise = std::make_unique>(); - auto future = promise->get_future(); - docs_everywhere_docs_publish_async(id, [](void* context, docs_everywhere_error* err, bool result) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(result); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - docs_everywhere_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** - * Register a listener for saved notifications - * - * @param listener Receiver of saved notifications - */ -inline void watch(std::shared_ptr listener) { - check_library(); - if (!listener) throw std::invalid_argument("listener: null callback interface"); - auto listener_ctx = std::make_unique>(std::move(listener)); - docs_everywhere_error err{}; - docs_everywhere_docs_watch(static_cast(listener_ctx.release()), &detail::SavedListener_vtable(), &err); - detail::check(err); -} - -} // namespace docs - -} // namespace docs_everywhere - -// END docs_everywhere.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_hpp.snap deleted file mode 100644 index defce99c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_hpp.snap +++ /dev/null @@ -1,2505 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#pragma once - -#include "edge_cases.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace edge_cases { - -/** - * An error a call that declares errors reports. - * - * A positive code is a declared code of the module's error domain, thrown as - * that domain's typed subclass. A negative code is a runtime failure: -1 a - * generic producer error, -2 a producer panic, -3 a marshalling failure, and - * -4 a callback-interface implementation that failed. A cancelled async call - * (-5) throws Cancelled. - */ -class Error : public std::runtime_error { - int32_t code_; - -public: - /** Builds an error carrying `code` and `message`. */ - Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} - - /** The numeric error code. */ - int32_t code() const noexcept { return code_; } -}; - -/** Thrown by an async call that was cancelled through its CancelToken (code -5). */ -class Cancelled : public Error { -public: - /** Builds a cancellation error carrying the producer's message. */ - explicit Cancelled(const std::string& message) : Error(-5, message) {} -}; - -/** - * Thrown by a call that declares no errors when it fails anyway: a producer - * panic (-2), a marshalling failure (-3), a callback-interface failure the - * producer let through (-4), or another runtime failure (-1). These are bugs, - * so no signature declares them; `what()` names the code and the producer's - * message. - */ -class InternalError : public std::runtime_error { - int32_t code_; - std::string message_; - -public: - /** Builds an internal error carrying `code` and the producer's `message`. */ - InternalError(int32_t code, const std::string& message) - : std::runtime_error("edge_cases: runtime error " + std::to_string(code) + ": " + message), - code_(code), - message_(message) {} - - /** The runtime error code (negative). */ - int32_t code() const noexcept { return code_; } - - /** The producer's message. */ - const std::string& message() const noexcept { return message_; } -}; - -/** - * Thrown on first use when the linked edge_cases library implements a - * different C ABI revision than this header, or lacks or changed a - * declaration this header was generated with. - */ -class LoadError : public std::runtime_error { -public: - /** Builds a load error describing the mismatch. */ - explicit LoadError(const std::string& message) : std::runtime_error(message) {} -}; - -/** - * Tag selecting the constructor that adopts a raw C pointer (one strong - * reference the wrapper then owns), in the style of std::adopt_lock. - */ -struct adopt_t { - explicit adopt_t() = default; -}; - -/** The adopt_t tag value: `Store s(adopt, raw)`. */ -inline constexpr adopt_t adopt{}; - -/** - * A cancellation token for cancellable async functions. - * - * Create one, pass it to any number of cancellable calls, and call cancel() - * from any thread. A cancelled call settles its future with Cancelled. Each - * call takes its own reference to the native token, so the token can be - * destroyed at any time; the destructor releases this wrapper's reference. - */ -class CancelToken { - edge_cases_cancel_token* handle_; - - struct none_t {}; - explicit CancelToken(none_t) noexcept : handle_(nullptr) {} - -public: - /** Creates a new, uncancelled token. */ - CancelToken() : handle_(edge_cases_cancel_token_create()) { - if (handle_ == nullptr) throw std::bad_alloc(); - } - - /** Releases this wrapper's reference to the native token. */ - ~CancelToken() { - if (handle_ != nullptr) edge_cases_cancel_token_destroy(handle_); - } - - CancelToken(const CancelToken&) = delete; - CancelToken& operator=(const CancelToken&) = delete; - - /** Transfers the native token; `other` becomes empty. */ - CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current token and takes over `other`'s. */ - CancelToken& operator=(CancelToken&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) edge_cases_cancel_token_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** The empty token a cancellable call uses when none is passed. */ - static const CancelToken& none() noexcept { - static const CancelToken token{none_t{}}; - return token; - } - - /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ - void cancel() const noexcept { - if (handle_ != nullptr) edge_cases_cancel_token_cancel(handle_); - } - - /** Whether cancel() has been called. */ - bool is_cancelled() const noexcept { - return handle_ != nullptr && edge_cases_cancel_token_is_cancelled(handle_); - } - - /** The native token, borrowed (null for an empty token). */ - edge_cases_cancel_token* handle() const noexcept { return handle_; } -}; - -namespace detail { - -/** The exception for a failed call that declares no errors: Cancelled for -5, else InternalError. */ -inline std::exception_ptr make_internal_error(int32_t code, const std::string& message) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - return std::make_exception_ptr(InternalError(code, message)); -} - -/** The message of a nonzero error slot. */ -inline std::string error_message(const edge_cases_error& err) { - return std::string(err.message != nullptr ? err.message : "unknown error"); -} - -/** Throws InternalError if `err` carries a nonzero code (a call that declares no errors). */ -inline void check(edge_cases_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_internal_error(err.code, error_message(err)); - edge_cases_error_clear(&err); - std::rethrow_exception(ex); -} - -/** Copies a producer-returned UTF-8 run into a string, then releases it. */ -inline std::string take_string(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::string(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { edge_cases_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::string(reinterpret_cast(ptr), len); -} - -/** Copies a producer-returned byte run into a vector, then releases it. */ -inline std::vector take_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::vector(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { edge_cases_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::vector(ptr, ptr + len); -} - -/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ -inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { - if (ptr == nullptr || len == 0) return std::string_view(); - return std::string_view(reinterpret_cast(ptr), len); -} - -/** Copies a borrowed byte run (null when empty) into a vector. */ -inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr || len == 0) return std::vector(); - return std::vector(ptr, ptr + len); -} - -/** The data pointer of a string argument, as the C ABI expects it. */ -inline const uint8_t* utf8(std::string_view s) noexcept { - return reinterpret_cast(s.data()); -} - -/** - * Hands `len` bytes from `data` to the producer through a callback method's - * out slots: copies them into a run from edge_cases_alloc, which the - * producer adopts and frees. - */ -inline void hand_over(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = nullptr; - if (len > 0) { - run = edge_cases_alloc(len); - if (run == nullptr) throw std::bad_alloc(); - std::memcpy(run, data, len); - } - *out_ptr = run; - *out_len = len; -} - -/** One declaration this header was generated with: its contract id and hash, and its dotted path. */ -struct ContractEntry { - /** FNV-1a 64 of the declaration's dotted path. */ - uint64_t id; - /** FNV-1a 64 of the declaration's canonical signature. */ - uint64_t hash; - /** The declaration's dotted path (`kv.Store.put`). */ - const char* path; -}; - -/** - * Why the contract table `contract` returns doesn't satisfy `expected`, - * naming the first declaration it lacks or declares differently, or an empty - * string when it has them all. Entries the producer has and `expected` - * doesn't are fine. - */ -template -std::string contract_mismatch(const edge_cases_contract_entry* (*contract)(size_t*), - const ContractEntry (&expected)[N]) { - size_t len = 0; - const edge_cases_contract_entry* table = contract(&len); - if (table == nullptr) len = 0; - for (const ContractEntry& want : expected) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < want.id) { - lo = mid + 1; - } else { - hi = mid; - } - } - if (lo == len || table[lo].id != want.id) { - return std::string(want.path) + " is missing from the library"; - } - if (table[lo].hash != want.hash) { - return std::string(want.path) + " changed since these bindings were generated"; - } - } - return std::string(); -} - -/** The declarations of module `edge_cases` this header was generated with. */ -inline constexpr ContractEntry edge_cases_contract[] = { - {0x056a86511785ba2bull, 0x5cba3f16d57c6cdbull, "edge_cases.make_widget"}, - {0x07bfa9ecb2b720d2ull, 0x3813b3f57c64bf35ull, "edge_cases.EdgeErrors"}, - {0x0aaa11867e37f991ull, 0xf090dd52146ebdf0ull, "edge_cases.Widget.refresh"}, - {0x0f9701493898ae4cull, 0x6944f47a275c96e8ull, "edge_cases.Inner"}, - {0x1226b1adac179f7dull, 0x0e95a00ab0bef637ull, "edge_cases.async_keywords"}, - {0x16d36bef1541136cull, 0x5ea7021ec324298full, "edge_cases.find_widget"}, - {0x1b7324c55e980febull, 0xabf87fe372358f42ull, "edge_cases.Widget.old_name"}, - {0x1cbd731467c2bab8ull, 0x4915b9989c2c9074ull, "edge_cases.Vault"}, - {0x2c2a6de67d894b0aull, 0x50a4496b1e7191caull, "edge_cases.Vault.open"}, - {0x322b4cfcd2d6861cull, 0xca7527a379ae8465ull, "edge_cases.Keywords"}, - {0x3f8e23a5f32906f4ull, 0x8d4c908d796f130dull, "edge_cases.Leaf"}, - {0x42ab8a685457b97cull, 0xe5ce30500cadb32dull, "edge_cases.Vault.handle"}, - {0x4a68540e2836d3d0ull, 0xa20e44c71e420ed2ull, "edge_cases.mode_stream"}, - {0x4e030580ac0b111dull, 0xb68b7a18d910e175ull, "edge_cases.legacy_value"}, - {0x4f5e4e5291864ef3ull, 0x55a1a88b21a08f8bull, "edge_cases.Legacy"}, - {0x4f84b06171b4213dull, 0xbaedcfdebb6a2820ull, "edge_cases.scalar_stream"}, - {0x5067a5ff2d9ef910ull, 0xbfc79b18fc4714fcull, "edge_cases.Widget"}, - {0x59aff159112fbf5bull, 0x18e4def57a8cfa49ull, "edge_cases.optional_params"}, - {0x5daf65b98fa77c48ull, 0xc817a8cb202e59c2ull, "edge_cases.keywords_roundtrip"}, - {0x5de8a5c9726c2c5aull, 0x7c50b5b65f7a896dull, "edge_cases.register_batch"}, - {0x618a620309994c3cull, 0x826ad0b1d208d723ull, "edge_cases.async_bytes"}, - {0x648285cdba0a7b8eull, 0xcf6e80e783a4d8beull, "edge_cases.WidgetHolder"}, - {0x649c78079824b10dull, 0x5b12dd26e0634b69ull, "edge_cases.async_widget"}, - {0x7da7a7357d122a37ull, 0xf544e025ffaa3d20ull, "edge_cases.async_void"}, - {0x7e4015bc3a472e2dull, 0xfe22291d90b82e4aull, "edge_cases.widget_stream"}, - {0x805276e5d81fdba7ull, 0xd9b64e360bd03b2bull, "edge_cases.Outer"}, - {0x80e1183326542df4ull, 0xe8a57e12cc6ed276ull, "edge_cases.select"}, - {0x923ce5e9bcd769a1ull, 0x0b973f5dc07dda8bull, "edge_cases.Vault.to_string"}, - {0x98ca8c01f5239404ull, 0xfe11c93eb6b303efull, "edge_cases.Widget.rename"}, - {0xa13e22e98ea278d4ull, 0xf1fae215d7387800ull, "edge_cases.widgets_everywhere"}, - {0xa84a8c9bf2a881e1ull, 0xbe1dc840bf221996ull, "edge_cases.async_optional"}, - {0xad958c478fbf205dull, 0xe386b0716d24f19bull, "edge_cases.Retired"}, - {0xb2fc3964ef56ab39ull, 0x73c325c8bc351236ull, "edge_cases.async_list"}, - {0xb3403af521cf0006ull, 0x6abc30d48206a3baull, "edge_cases.Widget.close"}, - {0xb794304a22a406b8ull, 0xf6fb98711c516105ull, "edge_cases.async_struct"}, - {0xba3936106a49c47dull, 0xb7025f2fadd6288eull, "edge_cases.Vault.peek"}, - {0xc155b0c3cfb164f4ull, 0xc777ae57d92ebc47ull, "edge_cases.noop"}, - {0xcdc18607777019ddull, 0xb81a31e5c20aa7a8ull, "edge_cases.inspect_widget"}, - {0xd164a82aeb64b8ccull, 0xded02c68005c4737ull, "edge_cases.nested_optional"}, - {0xd978ccad2468038full, 0x30c39c65d6235153ull, "edge_cases.Mode"}, - {0xdbe9d947431c9124ull, 0x39537fb848fdf980ull, "edge_cases.BatchListener"}, - {0xeae8b7862c08bb07ull, 0xfe12eeaa830452eaull, "edge_cases.Keyword"}, - {0xf1e155a05ae262b8ull, 0xb575eb818a423d42ull, "edge_cases.async_enum"}, - {0xf22e210bdbce7343ull, 0x2a5e0f801bc8c768ull, "edge_cases.Widget.dispose"}, - {0xf3c7cf675563ee6cull, 0x0ecffeef399facc8ull, "edge_cases.Vault.probe"}, - {0xfd34bc67acac379eull, 0x576caa14f3c0901aull, "edge_cases.Widget.fetch"}, - {0xff2e9d2bff941786ull, 0x7e96764ef1f5fbaaull, "edge_cases.string_stream"}, -}; - -} // namespace detail - -/** - * Verifies that the linked edge_cases library implements C ABI revision - * 4 and every declaration this header was generated with, throwing - * LoadError naming the first mismatch. The check runs once; every free - * function, constructor, and static member calls this before its first - * native call, so calling it at startup is optional. - */ -inline void check_library() { - static const std::string failure = []() -> std::string { - uint32_t abi = edge_cases_abi_version(); - if (abi != EDGE_CASES_ABI_VERSION) { - return "edge_cases: C ABI revision mismatch (header " + std::to_string(EDGE_CASES_ABI_VERSION) + - ", library " + std::to_string(abi) + ")"; - } - if (std::string why = detail::contract_mismatch(edge_cases_edge_cases_contract, detail::edge_cases_contract); !why.empty()) { - return "edge_cases: " + why; - } - return std::string(); - }(); - if (!failure.empty()) throw LoadError(failure); -} - -namespace detail { - -/** - * Serializes values into the value-buffer wire format: little-endian, packed - * with no alignment, lengths and element counts as u32. - */ -class BufferWriter { - std::vector buf_; - - template - void append_le(T v) { - for (size_t i = 0; i < sizeof(T); ++i) { - buf_.push_back(static_cast(v >> (8 * i))); - } - } - -public: - /** Pointer to the encoded bytes. */ - const uint8_t* data() const noexcept { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const noexcept { return buf_.size(); } - - void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } - void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } - void write_u8(uint8_t v) { buf_.push_back(v); } - void write_i16(int16_t v) { append_le(static_cast(v)); } - void write_u16(uint16_t v) { append_le(v); } - void write_i32(int32_t v) { append_le(static_cast(v)); } - void write_u32(uint32_t v) { append_le(v); } - void write_i64(int64_t v) { append_le(static_cast(v)); } - void write_u64(uint64_t v) { append_le(v); } - - void write_f32(float v) { - uint32_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - void write_f64(double v) { - uint64_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - /** Writes a byte length or an element count as a u32. */ - void write_len(size_t n) { - if (static_cast(n) > UINT32_MAX) throw std::length_error("value buffer length exceeds u32"); - append_le(static_cast(n)); - } - - void write_string(std::string_view v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - void write_bytes(const std::vector& v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - /** Writes an optional's presence flag: 0 absent, 1 present. */ - void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } - - /** - * Writes an object token: a fresh strong reference from the wrapper's - * `_clone`, which the reader adopts while `v` keeps its own. - */ - template - void write_object(const T& v) { - write_u64(static_cast(reinterpret_cast(v.clone_handle()))); - } -}; - -/** - * Decodes values from the value-buffer wire format. A malformed buffer is a - * producer bug (both sides are generated from one API), so every decode - * failure throws InternalError with the marshalling code -3. Object tokens - * adopted before a failure live in RAII wrappers that release them on - * unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - void require(size_t n, const char* what) const { - if (len_ - pos_ < n) fail(what); - } - - template - T read_le(const char* what) { - require(sizeof(T), what); - uint64_t v = 0; - for (size_t i = 0; i < sizeof(T); ++i) { - v |= static_cast(data_[pos_ + i]) << (8 * i); - } - pos_ += sizeof(T); - return static_cast(v); - } - -public: - /** Throws the marshalling failure (-3) for a malformed buffer. */ - [[noreturn]] static void fail(const char* what) { - throw InternalError(-3, std::string("malformed value buffer: ") + what); - } - - /** Reads from the `len` bytes at `data` (null when empty). */ - BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const noexcept { return len_ - pos_; } - - bool read_bool() { - uint8_t b = read_le("bool"); - if (b > 1) fail("bool byte out of range"); - return b != 0; - } - - int8_t read_i8() { return read_le("i8"); } - uint8_t read_u8() { return read_le("u8"); } - int16_t read_i16() { return read_le("i16"); } - uint16_t read_u16() { return read_le("u16"); } - int32_t read_i32() { return read_le("i32"); } - uint32_t read_u32() { return read_le("u32"); } - int64_t read_i64() { return read_le("i64"); } - uint64_t read_u64() { return read_le("u64"); } - - float read_f32() { - uint32_t bits = read_le("f32"); - float v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - double read_f64() { - uint64_t bits = read_le("f64"); - double v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - /** Reads a byte length, rejecting one larger than the bytes remaining. */ - size_t read_len() { - uint32_t n = read_le("length"); - if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); - return static_cast(n); - } - - /** - * Reads an element count. Elements may encode to zero bytes, so a count - * larger than the bytes remaining is legal; callers cap preallocation - * with reserve_hint(). - */ - size_t read_count() { return static_cast(read_le("count")); } - - /** A preallocation size for `n` elements that a malformed count can't inflate. */ - size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } - - std::string read_string() { - size_t n = read_len(); - std::string v(reinterpret_cast(data_) + pos_, n); - pos_ += n; - return v; - } - - std::vector read_bytes() { - size_t n = read_len(); - std::vector v(data_ + pos_, data_ + pos_ + n); - pos_ += n; - return v; - } - - bool read_option_flag() { - uint8_t b = read_le("option flag"); - if (b > 1) fail("option flag byte out of range"); - return b != 0; - } - - /** Reads an object token, adopting the strong reference it carries into a `T` wrapper. */ - template - T read_object() { - auto* raw = reinterpret_cast(static_cast(read_u64())); - if (raw == nullptr) fail("null object token"); - return T(adopt, raw); - } - - /** Rejects unconsumed bytes after decoding a complete value. */ - void expect_end() const { - if (pos_ != len_) fail("trailing bytes after value"); - } -}; - -/** Releases a producer-allocated buffer with edge_cases_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; - - BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} - BufferGuard(const BufferGuard&) = delete; - BufferGuard& operator=(const BufferGuard&) = delete; - - ~BufferGuard() { - if (ptr != nullptr) edge_cases_free_bytes(const_cast(ptr), len); - } -}; - -/** Decodes one complete `T` from a borrowed buffer with `read`, rejecting trailing bytes. */ -template -T decode(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferReader r(ptr, len); - T value = read(r); - r.expect_end(); - return value; -} - -/** Decodes one complete `T` from a producer-returned buffer with `read`, then releases the buffer. */ -template -T take(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferGuard guard(ptr, len); - return decode(ptr, len, read); -} - -} // namespace detail - -/** - * Deprecated enum with non-contiguous values and keyword-like variants - * - * @deprecated Use Mode instead - */ -enum class Legacy : int32_t { - None = 0, - Default = 5, - Class = 10, -}; - -enum class Mode : int32_t { - Fast = 1, - Safe = 2, -}; - -struct Leaf; -struct Inner; -struct Outer; -struct Keywords; -struct Retired; -struct WidgetHolder; -class Widget; -class Vault; -class BatchListener; -class Keyword; - -/** Interface with no constructor; instances come from make_widget */ -class Widget { - edge_cases_edge_cases_Widget* raw_; - -public: - /** The C type this class wraps. */ - using raw_type = edge_cases_edge_cases_Widget; - - /** Adopts one strong reference to a producer object: `Widget(adopt, raw)`. */ - explicit Widget(adopt_t, edge_cases_edge_cases_Widget* h) noexcept : raw_(h) {} - - /** Releases this wrapper's reference; the object is dropped with its last one. */ - ~Widget() { - if (raw_ != nullptr) edge_cases_edge_cases_Widget_destroy(raw_); - } - - /** Copies share the object: the copy takes a new strong reference. */ - Widget(const Widget& other) : raw_(edge_cases_edge_cases_Widget_clone(other.raw_)) {} - - /** Takes a new reference to `other`'s object and releases the current one. */ - Widget& operator=(const Widget& other) { - if (this != &other) { - edge_cases_edge_cases_Widget* h = edge_cases_edge_cases_Widget_clone(other.raw_); - if (raw_ != nullptr) edge_cases_edge_cases_Widget_destroy(raw_); - raw_ = h; - } - return *this; - } - - /** Transfers `other`'s reference; `other` becomes empty. */ - Widget(Widget&& other) noexcept : raw_(other.raw_) { other.raw_ = nullptr; } - - /** Releases the current reference and takes over `other`'s. */ - Widget& operator=(Widget&& other) noexcept { - if (this != &other) { - if (raw_ != nullptr) edge_cases_edge_cases_Widget_destroy(raw_); - raw_ = other.raw_; - other.raw_ = nullptr; - } - return *this; - } - - /** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */ - const edge_cases_edge_cases_Widget* handle() const noexcept { return raw_; } - - /** A new strong reference to the object, which the caller owns. */ - edge_cases_edge_cases_Widget* clone_handle() const { return edge_cases_edge_cases_Widget_clone(raw_); } - - /** - * Void method that can fail - * - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ - void refresh() const; - - std::string rename(std::string_view class_, const std::optional& import) const; - - /** - * Async method with a struct result - * - * @return A future settled from a producer thread; get() rethrows the call's error. - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ - std::future fetch(int64_t id) const; - - [[deprecated("Use rename instead")]] - std::string old_name() const; - - /** Spelled like the release method of several wrappers */ - bool close() const; - - /** Spelled like the release method of the Dart and .NET wrappers */ - void dispose(bool force) const; - -}; - -/** - * Deprecated interface with a throwing constructor - * - * @deprecated Use Widget instead - */ -class Vault { - edge_cases_edge_cases_Vault* raw_; - -public: - /** The C type this class wraps. */ - using raw_type = edge_cases_edge_cases_Vault; - - /** Adopts one strong reference to a producer object: `Vault(adopt, raw)`. */ - explicit Vault(adopt_t, edge_cases_edge_cases_Vault* h) noexcept : raw_(h) {} - - /** Releases this wrapper's reference; the object is dropped with its last one. */ - ~Vault() { - if (raw_ != nullptr) edge_cases_edge_cases_Vault_destroy(raw_); - } - - /** Copies share the object: the copy takes a new strong reference. */ - Vault(const Vault& other) : raw_(edge_cases_edge_cases_Vault_clone(other.raw_)) {} - - /** Takes a new reference to `other`'s object and releases the current one. */ - Vault& operator=(const Vault& other) { - if (this != &other) { - edge_cases_edge_cases_Vault* h = edge_cases_edge_cases_Vault_clone(other.raw_); - if (raw_ != nullptr) edge_cases_edge_cases_Vault_destroy(raw_); - raw_ = h; - } - return *this; - } - - /** Transfers `other`'s reference; `other` becomes empty. */ - Vault(Vault&& other) noexcept : raw_(other.raw_) { other.raw_ = nullptr; } - - /** Releases the current reference and takes over `other`'s. */ - Vault& operator=(Vault&& other) noexcept { - if (this != &other) { - if (raw_ != nullptr) edge_cases_edge_cases_Vault_destroy(raw_); - raw_ = other.raw_; - other.raw_ = nullptr; - } - return *this; - } - - /** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */ - const edge_cases_edge_cases_Vault* handle() const noexcept { return raw_; } - - /** A new strong reference to the object, which the caller owns. */ - edge_cases_edge_cases_Vault* clone_handle() const { return edge_cases_edge_cases_Vault_clone(raw_); } - - /** - * Fails when the vault is busy - * - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ - static Vault open(std::string_view path); - - std::optional peek() const; - - /** Spelled like the handle accessor of several wrappers */ - int64_t handle_() const; - - /** - * Throwing static - * - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ - static bool probe(); - - /** Static spelled like an inherited object member */ - static std::string to_string(); - -}; - -struct Leaf { - float weight; -}; - -/** Middle layer of a three-deep struct nesting */ -struct Inner { - Leaf leaf; - std::vector leaves; -}; - -struct Outer { - Inner inner; - std::optional maybe_inner; - std::unordered_map by_name; - std::vector modes; - std::vector> matrix; - std::vector> sparse; - std::optional> blob; - std::optional note; - std::unordered_map by_id; - bool _hidden; - uint8_t v2; -}; - -/** Every field is a reserved word somewhere */ -struct Keywords { - int32_t class_; - int32_t def; - int32_t import; - int32_t from; - int32_t func; - int32_t range; - int32_t register_; - int32_t volatile_; - int32_t new_; - int32_t default_; - int32_t end; - int32_t in; - int32_t is; - int32_t int_; - int32_t var; - int32_t val; - int32_t self; - int32_t this_; - int32_t object; - int32_t string; - int32_t case_; - int32_t select; - int32_t switch_; - int32_t void_; - int32_t union_; - int32_t namespace_; - int32_t operator_; - int32_t template_; - int32_t package; - int32_t protocol; - int32_t guard; - int32_t defer; - int32_t inout; - int32_t internal; - int32_t when; - int32_t fun; - int32_t data; - int32_t late; - int32_t part; - int32_t out; - int32_t ref; - int32_t event; - int32_t base; - int32_t lambda; - int32_t yield; - int32_t nil; - int32_t null; - int32_t begin; - int32_t rescue; - int32_t module; - int32_t chan; - int32_t go; - int32_t let; - int32_t export_; - int32_t delete_; - int32_t typedef_; - int32_t sizeof_; - int32_t static_; - int32_t super; -}; - -/** - * Deprecated struct - * - * @deprecated Use Outer instead - */ -struct Retired { - int64_t id; -}; - -/** Record carrying objects in every buffered position */ -struct WidgetHolder { - Widget primary; - std::optional fallback; - std::vector extras; - std::unordered_map by_name; -}; - -namespace detail { - -inline void write_Leaf(BufferWriter& w, const Leaf& v); -inline Leaf read_Leaf(BufferReader& r); -inline void write_Inner(BufferWriter& w, const Inner& v); -inline Inner read_Inner(BufferReader& r); -inline void write_Outer(BufferWriter& w, const Outer& v); -inline Outer read_Outer(BufferReader& r); -inline void write_Keywords(BufferWriter& w, const Keywords& v); -inline Keywords read_Keywords(BufferReader& r); -inline void write_Retired(BufferWriter& w, const Retired& v); -inline Retired read_Retired(BufferReader& r); -inline void write_WidgetHolder(BufferWriter& w, const WidgetHolder& v); -inline WidgetHolder read_WidgetHolder(BufferReader& r); -inline void write_list_Leaf(BufferWriter& w, const std::vector& v); -inline std::vector read_list_Leaf(BufferReader& r); -inline void write_opt_Inner(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Inner(BufferReader& r); -inline void write_map_string_Inner(BufferWriter& w, const std::unordered_map& v); -inline std::unordered_map read_map_string_Inner(BufferReader& r); -inline void write_list_Mode(BufferWriter& w, const std::vector& v); -inline std::vector read_list_Mode(BufferReader& r); -inline void write_list_i32(BufferWriter& w, const std::vector& v); -inline std::vector read_list_i32(BufferReader& r); -inline void write_list_list_i32(BufferWriter& w, const std::vector>& v); -inline std::vector> read_list_list_i32(BufferReader& r); -inline void write_opt_i64(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_i64(BufferReader& r); -inline void write_list_opt_i64(BufferWriter& w, const std::vector>& v); -inline std::vector> read_list_opt_i64(BufferReader& r); -inline void write_opt_bytes(BufferWriter& w, const std::optional>& v); -inline std::optional> read_opt_bytes(BufferReader& r); -inline void write_opt_string(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_string(BufferReader& r); -inline void write_map_i64_string(BufferWriter& w, const std::unordered_map& v); -inline std::unordered_map read_map_i64_string(BufferReader& r); -inline void write_opt_Widget(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Widget(BufferReader& r); -inline void write_list_Widget(BufferWriter& w, const std::vector& v); -inline std::vector read_list_Widget(BufferReader& r); -inline void write_map_string_Widget(BufferWriter& w, const std::unordered_map& v); -inline std::unordered_map read_map_string_Widget(BufferReader& r); -inline void write_list_string(BufferWriter& w, const std::vector& v); -inline std::vector read_list_string(BufferReader& r); -inline void write_opt_i32(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_i32(BufferReader& r); -inline void write_opt_Outer(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Outer(BufferReader& r); -inline void write_list_opt_Outer(BufferWriter& w, const std::vector>& v); -inline std::vector> read_list_opt_Outer(BufferReader& r); -inline void write_opt_list_opt_Outer(BufferWriter& w, const std::optional>>& v); -inline std::optional>> read_opt_list_opt_Outer(BufferReader& r); -inline void write_opt_list_string(BufferWriter& w, const std::optional>& v); -inline std::optional> read_opt_list_string(BufferReader& r); -inline void write_opt_Mode(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Mode(BufferReader& r); -inline void write_list_opt_Widget(BufferWriter& w, const std::vector>& v); -inline std::vector> read_list_opt_Widget(BufferReader& r); -inline void write_list_Outer(BufferWriter& w, const std::vector& v); -inline std::vector read_list_Outer(BufferReader& r); - -/** Writes one `Leaf` to a value buffer. */ -inline void write_Leaf(BufferWriter& w, const Leaf& v) { - w.write_f32(v.weight); -} - -/** Reads one `Leaf` from a value buffer. */ -inline Leaf read_Leaf(BufferReader& r) { - return Leaf{ - r.read_f32(), - }; -} - -/** Writes one `Inner` to a value buffer. */ -inline void write_Inner(BufferWriter& w, const Inner& v) { - detail::write_Leaf(w, v.leaf); - detail::write_list_Leaf(w, v.leaves); -} - -/** Reads one `Inner` from a value buffer. */ -inline Inner read_Inner(BufferReader& r) { - return Inner{ - detail::read_Leaf(r), - detail::read_list_Leaf(r), - }; -} - -/** Writes one `Outer` to a value buffer. */ -inline void write_Outer(BufferWriter& w, const Outer& v) { - detail::write_Inner(w, v.inner); - detail::write_opt_Inner(w, v.maybe_inner); - detail::write_map_string_Inner(w, v.by_name); - detail::write_list_Mode(w, v.modes); - detail::write_list_list_i32(w, v.matrix); - detail::write_list_opt_i64(w, v.sparse); - detail::write_opt_bytes(w, v.blob); - detail::write_opt_string(w, v.note); - detail::write_map_i64_string(w, v.by_id); - w.write_bool(v._hidden); - w.write_u8(v.v2); -} - -/** Reads one `Outer` from a value buffer. */ -inline Outer read_Outer(BufferReader& r) { - return Outer{ - detail::read_Inner(r), - detail::read_opt_Inner(r), - detail::read_map_string_Inner(r), - detail::read_list_Mode(r), - detail::read_list_list_i32(r), - detail::read_list_opt_i64(r), - detail::read_opt_bytes(r), - detail::read_opt_string(r), - detail::read_map_i64_string(r), - r.read_bool(), - r.read_u8(), - }; -} - -/** Writes one `Keywords` to a value buffer. */ -inline void write_Keywords(BufferWriter& w, const Keywords& v) { - w.write_i32(v.class_); - w.write_i32(v.def); - w.write_i32(v.import); - w.write_i32(v.from); - w.write_i32(v.func); - w.write_i32(v.range); - w.write_i32(v.register_); - w.write_i32(v.volatile_); - w.write_i32(v.new_); - w.write_i32(v.default_); - w.write_i32(v.end); - w.write_i32(v.in); - w.write_i32(v.is); - w.write_i32(v.int_); - w.write_i32(v.var); - w.write_i32(v.val); - w.write_i32(v.self); - w.write_i32(v.this_); - w.write_i32(v.object); - w.write_i32(v.string); - w.write_i32(v.case_); - w.write_i32(v.select); - w.write_i32(v.switch_); - w.write_i32(v.void_); - w.write_i32(v.union_); - w.write_i32(v.namespace_); - w.write_i32(v.operator_); - w.write_i32(v.template_); - w.write_i32(v.package); - w.write_i32(v.protocol); - w.write_i32(v.guard); - w.write_i32(v.defer); - w.write_i32(v.inout); - w.write_i32(v.internal); - w.write_i32(v.when); - w.write_i32(v.fun); - w.write_i32(v.data); - w.write_i32(v.late); - w.write_i32(v.part); - w.write_i32(v.out); - w.write_i32(v.ref); - w.write_i32(v.event); - w.write_i32(v.base); - w.write_i32(v.lambda); - w.write_i32(v.yield); - w.write_i32(v.nil); - w.write_i32(v.null); - w.write_i32(v.begin); - w.write_i32(v.rescue); - w.write_i32(v.module); - w.write_i32(v.chan); - w.write_i32(v.go); - w.write_i32(v.let); - w.write_i32(v.export_); - w.write_i32(v.delete_); - w.write_i32(v.typedef_); - w.write_i32(v.sizeof_); - w.write_i32(v.static_); - w.write_i32(v.super); -} - -/** Reads one `Keywords` from a value buffer. */ -inline Keywords read_Keywords(BufferReader& r) { - return Keywords{ - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - r.read_i32(), - }; -} - -/** Writes one `Retired` to a value buffer. */ -inline void write_Retired(BufferWriter& w, const Retired& v) { - w.write_i64(v.id); -} - -/** Reads one `Retired` from a value buffer. */ -inline Retired read_Retired(BufferReader& r) { - return Retired{ - r.read_i64(), - }; -} - -/** Writes one `WidgetHolder` to a value buffer. */ -inline void write_WidgetHolder(BufferWriter& w, const WidgetHolder& v) { - w.write_object(v.primary); - detail::write_opt_Widget(w, v.fallback); - detail::write_list_Widget(w, v.extras); - detail::write_map_string_Widget(w, v.by_name); -} - -/** Reads one `WidgetHolder` from a value buffer. */ -inline WidgetHolder read_WidgetHolder(BufferReader& r) { - return WidgetHolder{ - r.read_object(), - detail::read_opt_Widget(r), - detail::read_list_Widget(r), - detail::read_map_string_Widget(r), - }; -} - -/** Writes one `[Leaf]` to a value buffer. */ -inline void write_list_Leaf(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_Leaf(w, item); -} - -/** Reads one `[Leaf]` from a value buffer. */ -inline std::vector read_list_Leaf(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_Leaf(r)); - return v; -} - -/** Writes one `Inner?` to a value buffer. */ -inline void write_opt_Inner(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) detail::write_Inner(w, *v); -} - -/** Reads one `Inner?` from a value buffer. */ -inline std::optional read_opt_Inner(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(detail::read_Inner(r)); -} - -/** Writes one `{string:Inner}` to a value buffer. */ -inline void write_map_string_Inner(BufferWriter& w, const std::unordered_map& v) { - w.write_len(v.size()); - for (const auto& entry : v) { - w.write_string(entry.first); - detail::write_Inner(w, entry.second); - } -} - -/** Reads one `{string:Inner}` from a value buffer. */ -inline std::unordered_map read_map_string_Inner(BufferReader& r) { - size_t n = r.read_count(); - std::unordered_map v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) { - std::string key = r.read_string(); - Inner value = detail::read_Inner(r); - if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); - } - return v; -} - -/** Writes one `[Mode]` to a value buffer. */ -inline void write_list_Mode(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_i32(static_cast(item)); -} - -/** Reads one `[Mode]` from a value buffer. */ -inline std::vector read_list_Mode(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(static_cast(r.read_i32())); - return v; -} - -/** Writes one `[i32]` to a value buffer. */ -inline void write_list_i32(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_i32(item); -} - -/** Reads one `[i32]` from a value buffer. */ -inline std::vector read_list_i32(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(r.read_i32()); - return v; -} - -/** Writes one `[[i32]]` to a value buffer. */ -inline void write_list_list_i32(BufferWriter& w, const std::vector>& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_list_i32(w, item); -} - -/** Reads one `[[i32]]` from a value buffer. */ -inline std::vector> read_list_list_i32(BufferReader& r) { - size_t n = r.read_count(); - std::vector> v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_list_i32(r)); - return v; -} - -/** Writes one `i64?` to a value buffer. */ -inline void write_opt_i64(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_i64(*v); -} - -/** Reads one `i64?` from a value buffer. */ -inline std::optional read_opt_i64(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(r.read_i64()); -} - -/** Writes one `[i64?]` to a value buffer. */ -inline void write_list_opt_i64(BufferWriter& w, const std::vector>& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_opt_i64(w, item); -} - -/** Reads one `[i64?]` from a value buffer. */ -inline std::vector> read_list_opt_i64(BufferReader& r) { - size_t n = r.read_count(); - std::vector> v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_opt_i64(r)); - return v; -} - -/** Writes one `bytes?` to a value buffer. */ -inline void write_opt_bytes(BufferWriter& w, const std::optional>& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_bytes(*v); -} - -/** Reads one `bytes?` from a value buffer. */ -inline std::optional> read_opt_bytes(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional>(r.read_bytes()); -} - -/** Writes one `string?` to a value buffer. */ -inline void write_opt_string(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_string(*v); -} - -/** Reads one `string?` from a value buffer. */ -inline std::optional read_opt_string(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(r.read_string()); -} - -/** Writes one `{i64:string}` to a value buffer. */ -inline void write_map_i64_string(BufferWriter& w, const std::unordered_map& v) { - w.write_len(v.size()); - for (const auto& entry : v) { - w.write_i64(entry.first); - w.write_string(entry.second); - } -} - -/** Reads one `{i64:string}` from a value buffer. */ -inline std::unordered_map read_map_i64_string(BufferReader& r) { - size_t n = r.read_count(); - std::unordered_map v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) { - int64_t key = r.read_i64(); - std::string value = r.read_string(); - if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); - } - return v; -} - -/** Writes one `Widget?` to a value buffer. */ -inline void write_opt_Widget(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_object(*v); -} - -/** Reads one `Widget?` from a value buffer. */ -inline std::optional read_opt_Widget(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(r.read_object()); -} - -/** Writes one `[Widget]` to a value buffer. */ -inline void write_list_Widget(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_object(item); -} - -/** Reads one `[Widget]` from a value buffer. */ -inline std::vector read_list_Widget(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(r.read_object()); - return v; -} - -/** Writes one `{string:Widget}` to a value buffer. */ -inline void write_map_string_Widget(BufferWriter& w, const std::unordered_map& v) { - w.write_len(v.size()); - for (const auto& entry : v) { - w.write_string(entry.first); - w.write_object(entry.second); - } -} - -/** Reads one `{string:Widget}` from a value buffer. */ -inline std::unordered_map read_map_string_Widget(BufferReader& r) { - size_t n = r.read_count(); - std::unordered_map v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) { - std::string key = r.read_string(); - Widget value = r.read_object(); - if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); - } - return v; -} - -/** Writes one `[string]` to a value buffer. */ -inline void write_list_string(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_string(item); -} - -/** Reads one `[string]` from a value buffer. */ -inline std::vector read_list_string(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(r.read_string()); - return v; -} - -/** Writes one `i32?` to a value buffer. */ -inline void write_opt_i32(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_i32(*v); -} - -/** Reads one `i32?` from a value buffer. */ -inline std::optional read_opt_i32(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(r.read_i32()); -} - -/** Writes one `Outer?` to a value buffer. */ -inline void write_opt_Outer(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) detail::write_Outer(w, *v); -} - -/** Reads one `Outer?` from a value buffer. */ -inline std::optional read_opt_Outer(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(detail::read_Outer(r)); -} - -/** Writes one `[Outer?]` to a value buffer. */ -inline void write_list_opt_Outer(BufferWriter& w, const std::vector>& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_opt_Outer(w, item); -} - -/** Reads one `[Outer?]` from a value buffer. */ -inline std::vector> read_list_opt_Outer(BufferReader& r) { - size_t n = r.read_count(); - std::vector> v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_opt_Outer(r)); - return v; -} - -/** Writes one `[Outer?]?` to a value buffer. */ -inline void write_opt_list_opt_Outer(BufferWriter& w, const std::optional>>& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) detail::write_list_opt_Outer(w, *v); -} - -/** Reads one `[Outer?]?` from a value buffer. */ -inline std::optional>> read_opt_list_opt_Outer(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional>>(detail::read_list_opt_Outer(r)); -} - -/** Writes one `[string]?` to a value buffer. */ -inline void write_opt_list_string(BufferWriter& w, const std::optional>& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) detail::write_list_string(w, *v); -} - -/** Reads one `[string]?` from a value buffer. */ -inline std::optional> read_opt_list_string(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional>(detail::read_list_string(r)); -} - -/** Writes one `Mode?` to a value buffer. */ -inline void write_opt_Mode(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_i32(static_cast(*v)); -} - -/** Reads one `Mode?` from a value buffer. */ -inline std::optional read_opt_Mode(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(static_cast(r.read_i32())); -} - -/** Writes one `[Widget?]` to a value buffer. */ -inline void write_list_opt_Widget(BufferWriter& w, const std::vector>& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_opt_Widget(w, item); -} - -/** Reads one `[Widget?]` from a value buffer. */ -inline std::vector> read_list_opt_Widget(BufferReader& r) { - size_t n = r.read_count(); - std::vector> v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_opt_Widget(r)); - return v; -} - -/** Writes one `[Outer]` to a value buffer. */ -inline void write_list_Outer(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_Outer(w, item); -} - -/** Reads one `[Outer]` from a value buffer. */ -inline std::vector read_list_Outer(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_Outer(r)); - return v; -} - -} // namespace detail - -/** The errors the `edge_cases` module's throwing calls report; catch a code's subclass or this. */ -class EdgeErrorsError : public Error { -public: - /** Builds an error carrying a domain `code` and `message`. */ - EdgeErrorsError(int32_t code, const std::string& message) : Error(code, message) {} -}; - -/** - * Resource is busy - * - * Code 7. - */ -class BusyError : public EdgeErrorsError { -public: - /** Builds the error with the producer's `message` and the code's fields. */ - explicit BusyError(const std::string& message) : EdgeErrorsError(7, message) {} -}; - -namespace detail { - -/** The exception for a failed `EdgeErrorsError` call: its code's subclass, Cancelled, or the root Error. */ -inline std::exception_ptr make_edge_cases_error(int32_t code, const std::string& message, const uint8_t* payload_ptr, size_t payload_len) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - if (code < 0) return std::make_exception_ptr(Error(code, message)); - (void)payload_ptr; - (void)payload_len; - switch (code) { - case 7: return std::make_exception_ptr(BusyError(message)); - default: return std::make_exception_ptr(EdgeErrorsError(code, message)); - } -} - -/** Throws the typed `EdgeErrorsError` exception if `err` carries a nonzero code. */ -inline void check_edge_cases(edge_cases_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_edge_cases_error(err.code, error_message(err), err.payload_ptr, err.payload_len); - edge_cases_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -/** - * Callback interface carrying composite payloads and objects - * - * Subclass it and pass a `std::shared_ptr` where the API takes a `BatchListener`. The producer may call the methods from any thread, so they must be thread-safe, until it releases the implementation (also from any thread). An exception a method throws reaches the producer as a callback failure (code -4). - */ -class BatchListener { -public: - virtual ~BatchListener() = default; - - /** Composite payloads cross as value buffers */ - virtual void on_batch(const std::vector& items, const std::vector& raw, const std::optional& first, Mode mode) = 0; - - /** Objects cross as strong references the consumer must release */ - virtual void on_widget(Widget widget, std::optional fallback, const std::vector& children) = 0; - - virtual void on_tick() = 0; - - /** Scalar return travels back through the vtable */ - virtual bool should_continue(int32_t class_) = 0; - - /** Enum return travels back as its integer discriminant */ - virtual Mode pick_mode() = 0; -}; - -namespace detail { - -/** Trampolines adapting a `BatchListener` implementation to `edge_cases_edge_cases_BatchListener_vtable`. */ -struct BatchListener_trampolines { - static void on_batch(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err) { - try { - BatchListener& impl = **static_cast*>(ctx); - std::vector items_arg = detail::decode(items_ptr, items_len, &detail::read_list_Outer); - std::optional first_arg = detail::decode(first_ptr, first_len, &detail::read_opt_Outer); - impl.on_batch(std::move(items_arg), detail::borrow_bytes(raw_ptr, raw_len), std::move(first_arg), static_cast(static_cast(mode))); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "BatchListener::on_batch threw a non-standard exception"); - } - } - - static void on_widget(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err) { - Widget widget_arg(adopt, widget); - std::optional fallback_arg; - if (fallback != nullptr) fallback_arg.emplace(adopt, fallback); - try { - BatchListener& impl = **static_cast*>(ctx); - std::vector children_arg = detail::decode(children_ptr, children_len, &detail::read_list_Widget); - impl.on_widget(std::move(widget_arg), std::move(fallback_arg), std::move(children_arg)); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "BatchListener::on_widget threw a non-standard exception"); - } - } - - static void on_tick(void* ctx, edge_cases_error* out_err) { - try { - BatchListener& impl = **static_cast*>(ctx); - impl.on_tick(); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "BatchListener::on_tick threw a non-standard exception"); - } - } - - static bool should_continue(void* ctx, int32_t class_, edge_cases_error* out_err) { - try { - BatchListener& impl = **static_cast*>(ctx); - return impl.should_continue(class_); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "BatchListener::should_continue threw a non-standard exception"); - } - return bool{}; - } - - static edge_cases_edge_cases_Mode pick_mode(void* ctx, edge_cases_error* out_err) { - try { - BatchListener& impl = **static_cast*>(ctx); - return static_cast(static_cast(impl.pick_mode())); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "BatchListener::pick_mode threw a non-standard exception"); - } - return edge_cases_edge_cases_Mode{}; - } - - /** Deletes the implementation box once the producer releases it. */ - static void free_ctx(void* ctx) { - delete static_cast*>(ctx); - } -}; - -/** The vtable every `BatchListener` implementation is passed with. */ -inline const edge_cases_edge_cases_BatchListener_vtable& BatchListener_vtable() { - static const edge_cases_edge_cases_BatchListener_vtable vtable = { - static_cast(sizeof(edge_cases_edge_cases_BatchListener_vtable)), - 0, - &BatchListener_trampolines::free_ctx, - &BatchListener_trampolines::on_batch, - &BatchListener_trampolines::on_widget, - &BatchListener_trampolines::on_tick, - &BatchListener_trampolines::should_continue, - &BatchListener_trampolines::pick_mode, - }; - return vtable; -} - -} // namespace detail - -/** - * Callback interface whose name and methods are keywords - * - * Subclass it and pass a `std::shared_ptr` where the API takes a `Keyword`. The producer may call the methods from any thread, so they must be thread-safe, until it releases the implementation (also from any thread). An exception a method throws reaches the producer as a callback failure (code -4). - */ -class Keyword { -public: - virtual ~Keyword() = default; - - virtual void delete_(int32_t self) = 0; - - virtual int32_t import() = 0; -}; - -namespace detail { - -/** Trampolines adapting a `Keyword` implementation to `edge_cases_edge_cases_Keyword_vtable`. */ -struct Keyword_trampolines { - static void delete_(void* ctx, int32_t self, edge_cases_error* out_err) { - try { - Keyword& impl = **static_cast*>(ctx); - impl.delete_(self); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "Keyword::delete_ threw a non-standard exception"); - } - } - - static int32_t import(void* ctx, edge_cases_error* out_err) { - try { - Keyword& impl = **static_cast*>(ctx); - return impl.import(); - } catch (const std::exception& e) { - edge_cases_error_set(out_err, -4, e.what()); - } catch (...) { - edge_cases_error_set(out_err, -4, "Keyword::import threw a non-standard exception"); - } - return int32_t{}; - } - - /** Deletes the implementation box once the producer releases it. */ - static void free_ctx(void* ctx) { - delete static_cast*>(ctx); - } -}; - -/** The vtable every `Keyword` implementation is passed with. */ -inline const edge_cases_edge_cases_Keyword_vtable& Keyword_vtable() { - static const edge_cases_edge_cases_Keyword_vtable vtable = { - static_cast(sizeof(edge_cases_edge_cases_Keyword_vtable)), - 0, - &Keyword_trampolines::free_ctx, - &Keyword_trampolines::delete_, - &Keyword_trampolines::import, - }; - return vtable; -} - -} // namespace detail - -inline void Widget::refresh() const { - edge_cases_error err{}; - edge_cases_edge_cases_Widget_refresh(raw_, &err); - detail::check_edge_cases(err); -} - -inline std::string Widget::rename(std::string_view class_, const std::optional& import) const { - detail::BufferWriter import_buf; - detail::write_opt_string(import_buf, import); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Widget_rename(raw_, detail::utf8(class_), class_.size(), import_buf.data(), import_buf.size(), &out_len, &err); - detail::check(err); - return detail::take_string(result, out_len); -} - -inline std::future Widget::fetch(int64_t id) const { - auto promise = std::make_unique>(); - auto future = promise->get_future(); - edge_cases_edge_cases_Widget_fetch(raw_, id, [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_edge_cases_error(err->code, message, err->payload_ptr, err->payload_len)); - } else { - p->set_value(detail::take(result_ptr, result_len, &detail::read_Outer)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -inline std::string Widget::old_name() const { - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Widget_old_name(raw_, &out_len, &err); - detail::check(err); - return detail::take_string(result, out_len); -} - -inline bool Widget::close() const { - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Widget_close(raw_, &err); - detail::check(err); - return result; -} - -inline void Widget::dispose(bool force) const { - edge_cases_error err{}; - edge_cases_edge_cases_Widget_dispose(raw_, force, &err); - detail::check(err); -} - -inline Vault Vault::open(std::string_view path) { - check_library(); - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Vault_open(detail::utf8(path), path.size(), &err); - detail::check_edge_cases(err); - return Vault(adopt, result); -} - -inline std::optional Vault::peek() const { - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Vault_peek(raw_, &err); - detail::check(err); - return result != nullptr ? std::optional(std::in_place, adopt, result) : std::nullopt; -} - -inline int64_t Vault::handle_() const { - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Vault_handle(raw_, &err); - detail::check(err); - return result; -} - -inline bool Vault::probe() { - check_library(); - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Vault_probe(&err); - detail::check_edge_cases(err); - return result; -} - -inline std::string Vault::to_string() { - check_library(); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_Vault_to_string(&out_len, &err); - detail::check(err); - return detail::take_string(result, out_len); -} - -namespace edge_cases { - -/** No parameters and no result */ -inline void noop() { - check_library(); - edge_cases_error err{}; - edge_cases_edge_cases_noop(&err); - detail::check(err); -} - -/** Function named after a keyword whose parameters are all keywords */ -inline int32_t select(int32_t class_, std::string_view from, const std::vector& func, const std::optional& range, bool type_) { - check_library(); - detail::BufferWriter func_buf; - detail::write_list_string(func_buf, func); - detail::BufferWriter range_buf; - detail::write_opt_i32(range_buf, range); - edge_cases_error err{}; - auto result = edge_cases_edge_cases_select(class_, detail::utf8(from), from.size(), func_buf.data(), func_buf.size(), range_buf.data(), range_buf.size(), type_, &err); - detail::check(err); - return result; -} - -/** Optional list of optional structs, the deepest legal nesting */ -inline std::optional>> nested_optional(const std::optional>>& items) { - check_library(); - detail::BufferWriter items_buf; - detail::write_opt_list_opt_Outer(items_buf, items); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_nested_optional(items_buf.data(), items_buf.size(), &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_opt_list_opt_Outer); -} - -inline std::optional optional_params(const std::optional& text, const std::optional& count, const std::optional& outer, const std::optional>& tags, const std::optional>& blob, const std::optional& mode) { - check_library(); - detail::BufferWriter text_buf; - detail::write_opt_string(text_buf, text); - detail::BufferWriter count_buf; - detail::write_opt_i32(count_buf, count); - detail::BufferWriter outer_buf; - detail::write_opt_Outer(outer_buf, outer); - detail::BufferWriter tags_buf; - detail::write_opt_list_string(tags_buf, tags); - detail::BufferWriter blob_buf; - detail::write_opt_bytes(blob_buf, blob); - detail::BufferWriter mode_buf; - detail::write_opt_Mode(mode_buf, mode); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_optional_params(text_buf.data(), text_buf.size(), count_buf.data(), count_buf.size(), outer_buf.data(), outer_buf.size(), tags_buf.data(), tags_buf.size(), blob_buf.data(), blob_buf.size(), mode_buf.data(), mode_buf.size(), &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_opt_Mode); -} - -/** Two callback interface parameters on one function */ -inline void register_batch(std::shared_ptr listener, std::shared_ptr keyword) { - check_library(); - if (!listener) throw std::invalid_argument("listener: null callback interface"); - auto listener_ctx = std::make_unique>(std::move(listener)); - if (!keyword) throw std::invalid_argument("keyword: null callback interface"); - auto keyword_ctx = std::make_unique>(std::move(keyword)); - edge_cases_error err{}; - edge_cases_edge_cases_register_batch(static_cast(listener_ctx.release()), &detail::BatchListener_vtable(), static_cast(keyword_ctx.release()), &detail::Keyword_vtable(), &err); - detail::check(err); -} - -/** Objects in every buffered position */ -inline WidgetHolder widgets_everywhere(const std::vector& list, const std::unordered_map& by_name, const std::vector>& sparse, const WidgetHolder& holder) { - check_library(); - detail::BufferWriter list_buf; - detail::write_list_Widget(list_buf, list); - detail::BufferWriter by_name_buf; - detail::write_map_string_Widget(by_name_buf, by_name); - detail::BufferWriter sparse_buf; - detail::write_list_opt_Widget(sparse_buf, sparse); - detail::BufferWriter holder_buf; - detail::write_WidgetHolder(holder_buf, holder); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_widgets_everywhere(list_buf.data(), list_buf.size(), by_name_buf.data(), by_name_buf.size(), sparse_buf.data(), sparse_buf.size(), holder_buf.data(), holder_buf.size(), &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_WidgetHolder); -} - -/** - * A lazy, move-only range over the `Widget` elements `widget_stream()` produces. - * - * Each step pulls one element from the producer, so results stream in constant memory. The range releases the producer iterator exactly once: when it's exhausted, or from the destructor when iteration stops early. - */ -class WidgetStreamIterator { - edge_cases_edge_cases_WidgetStreamIterator* handle_; - -public: - /** Adopts a producer iterator. */ - explicit WidgetStreamIterator(adopt_t, edge_cases_edge_cases_WidgetStreamIterator* h) noexcept : handle_(h) {} - - /** Releases the producer iterator if it's still held. */ - ~WidgetStreamIterator() { - if (handle_ != nullptr) edge_cases_edge_cases_WidgetStreamIterator_destroy(handle_); - } - - WidgetStreamIterator(const WidgetStreamIterator&) = delete; - WidgetStreamIterator& operator=(const WidgetStreamIterator&) = delete; - - /** Transfers `other`'s iterator; `other` becomes empty. */ - WidgetStreamIterator(WidgetStreamIterator&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current iterator and takes over `other`'s. */ - WidgetStreamIterator& operator=(WidgetStreamIterator&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) edge_cases_edge_cases_WidgetStreamIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element, or `std::nullopt` once exhausted (which releases the producer iterator). A producer error releases the iterator and throws InternalError. */ - std::optional next() { - if (handle_ == nullptr) return std::nullopt; - edge_cases_error err{}; - edge_cases_edge_cases_Widget* item{}; - int32_t has_item = edge_cases_edge_cases_WidgetStreamIterator_next(handle_, &item, &err); - if (err.code != 0 || has_item == 0) { - edge_cases_edge_cases_WidgetStreamIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - return std::nullopt; - } - return std::optional(std::in_place, Widget(adopt, item)); - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element. */ - class iterator { - WidgetStreamIterator* range_; - std::optional current_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = Widget; - using difference_type = std::ptrdiff_t; - using pointer = Widget*; - using reference = Widget&; - - /** Binds to `range` and pulls the first element. */ - explicit iterator(WidgetStreamIterator* range) : range_(range), current_(range->next()) {} - reference operator*() { return *current_; } - pointer operator->() { return &*current_; } - iterator& operator++() { current_ = range_->next(); return *this; } - void operator++(int) { current_ = range_->next(); } - bool operator==(sentinel) const { return !current_.has_value(); } - bool operator!=(sentinel) const { return current_.has_value(); } - }; - - /** Begins iteration by pulling the first element. */ - iterator begin() { return iterator(this); } - - /** The past-the-end sentinel. */ - sentinel end() const { return sentinel{}; } -}; - -/** - * Iterator of objects - * - * @return A lazy `WidgetStreamIterator` range that pulls one element per step and releases the producer iterator when exhausted or destroyed. - */ -inline WidgetStreamIterator widget_stream() { - check_library(); - edge_cases_error err{}; - edge_cases_edge_cases_WidgetStreamIterator* iter = edge_cases_edge_cases_widget_stream(&err); - detail::check(err); - return WidgetStreamIterator(adopt, iter); -} - -/** - * Async returning an object - * - * @return A future settled from a producer thread; get() rethrows the call's error. - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ -inline std::future async_widget() { - check_library(); - auto promise = std::make_unique>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_widget([](void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_edge_cases_error(err->code, message, err->payload_ptr, err->payload_len)); - } else { - p->set_value(Widget(adopt, result)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** Factory for an interface with no constructor */ -inline Widget make_widget(std::string_view label) { - check_library(); - edge_cases_error err{}; - auto result = edge_cases_edge_cases_make_widget(detail::utf8(label), label.size(), &err); - detail::check(err); - return Widget(adopt, result); -} - -/** - * Optional interface return - * - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ -inline std::optional find_widget(int64_t id) { - check_library(); - edge_cases_error err{}; - auto result = edge_cases_edge_cases_find_widget(id, &err); - detail::check_edge_cases(err); - return result != nullptr ? std::optional(std::in_place, adopt, result) : std::nullopt; -} - -/** Interface parameter, including an optional one */ -inline std::string inspect_widget(const Widget& widget, const std::optional& fallback) { - check_library(); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_inspect_widget(widget.handle(), fallback.has_value() ? fallback->handle() : nullptr, &out_len, &err); - detail::check(err); - return detail::take_string(result, out_len); -} - -/** - * A lazy, move-only range over the `std::string` elements `string_stream()` produces. - * - * Each step pulls one element from the producer, so results stream in constant memory. The range releases the producer iterator exactly once: when it's exhausted, or from the destructor when iteration stops early. - */ -class StringStreamIterator { - edge_cases_edge_cases_StringStreamIterator* handle_; - -public: - /** Adopts a producer iterator. */ - explicit StringStreamIterator(adopt_t, edge_cases_edge_cases_StringStreamIterator* h) noexcept : handle_(h) {} - - /** Releases the producer iterator if it's still held. */ - ~StringStreamIterator() { - if (handle_ != nullptr) edge_cases_edge_cases_StringStreamIterator_destroy(handle_); - } - - StringStreamIterator(const StringStreamIterator&) = delete; - StringStreamIterator& operator=(const StringStreamIterator&) = delete; - - /** Transfers `other`'s iterator; `other` becomes empty. */ - StringStreamIterator(StringStreamIterator&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current iterator and takes over `other`'s. */ - StringStreamIterator& operator=(StringStreamIterator&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) edge_cases_edge_cases_StringStreamIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element, or `std::nullopt` once exhausted (which releases the producer iterator). A producer error releases the iterator and throws InternalError. */ - std::optional next() { - if (handle_ == nullptr) return std::nullopt; - edge_cases_error err{}; - const uint8_t* item{}; - size_t item_len = 0; - int32_t has_item = edge_cases_edge_cases_StringStreamIterator_next(handle_, &item, &item_len, &err); - if (err.code != 0 || has_item == 0) { - edge_cases_edge_cases_StringStreamIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - return std::nullopt; - } - return std::optional(std::in_place, detail::take_string(item, item_len)); - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element. */ - class iterator { - StringStreamIterator* range_; - std::optional current_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = std::string; - using difference_type = std::ptrdiff_t; - using pointer = std::string*; - using reference = std::string&; - - /** Binds to `range` and pulls the first element. */ - explicit iterator(StringStreamIterator* range) : range_(range), current_(range->next()) {} - reference operator*() { return *current_; } - pointer operator->() { return &*current_; } - iterator& operator++() { current_ = range_->next(); return *this; } - void operator++(int) { current_ = range_->next(); } - bool operator==(sentinel) const { return !current_.has_value(); } - bool operator!=(sentinel) const { return current_.has_value(); } - }; - - /** Begins iteration by pulling the first element. */ - iterator begin() { return iterator(this); } - - /** The past-the-end sentinel. */ - sentinel end() const { return sentinel{}; } -}; - -/** - * Iterator of strings - * - * @return A lazy `StringStreamIterator` range that pulls one element per step and releases the producer iterator when exhausted or destroyed. - */ -inline StringStreamIterator string_stream() { - check_library(); - edge_cases_error err{}; - edge_cases_edge_cases_StringStreamIterator* iter = edge_cases_edge_cases_string_stream(&err); - detail::check(err); - return StringStreamIterator(adopt, iter); -} - -/** - * A lazy, move-only range over the `int32_t` elements `scalar_stream()` produces. - * - * Each step pulls one element from the producer, so results stream in constant memory. The range releases the producer iterator exactly once: when it's exhausted, or from the destructor when iteration stops early. - */ -class ScalarStreamIterator { - edge_cases_edge_cases_ScalarStreamIterator* handle_; - -public: - /** Adopts a producer iterator. */ - explicit ScalarStreamIterator(adopt_t, edge_cases_edge_cases_ScalarStreamIterator* h) noexcept : handle_(h) {} - - /** Releases the producer iterator if it's still held. */ - ~ScalarStreamIterator() { - if (handle_ != nullptr) edge_cases_edge_cases_ScalarStreamIterator_destroy(handle_); - } - - ScalarStreamIterator(const ScalarStreamIterator&) = delete; - ScalarStreamIterator& operator=(const ScalarStreamIterator&) = delete; - - /** Transfers `other`'s iterator; `other` becomes empty. */ - ScalarStreamIterator(ScalarStreamIterator&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current iterator and takes over `other`'s. */ - ScalarStreamIterator& operator=(ScalarStreamIterator&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) edge_cases_edge_cases_ScalarStreamIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element, or `std::nullopt` once exhausted (which releases the producer iterator). A producer error releases the iterator and throws InternalError. */ - std::optional next() { - if (handle_ == nullptr) return std::nullopt; - edge_cases_error err{}; - int32_t item{}; - int32_t has_item = edge_cases_edge_cases_ScalarStreamIterator_next(handle_, &item, &err); - if (err.code != 0 || has_item == 0) { - edge_cases_edge_cases_ScalarStreamIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - return std::nullopt; - } - return std::optional(std::in_place, item); - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element. */ - class iterator { - ScalarStreamIterator* range_; - std::optional current_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = int32_t; - using difference_type = std::ptrdiff_t; - using pointer = int32_t*; - using reference = int32_t&; - - /** Binds to `range` and pulls the first element. */ - explicit iterator(ScalarStreamIterator* range) : range_(range), current_(range->next()) {} - reference operator*() { return *current_; } - pointer operator->() { return &*current_; } - iterator& operator++() { current_ = range_->next(); return *this; } - void operator++(int) { current_ = range_->next(); } - bool operator==(sentinel) const { return !current_.has_value(); } - bool operator!=(sentinel) const { return current_.has_value(); } - }; - - /** Begins iteration by pulling the first element. */ - iterator begin() { return iterator(this); } - - /** The past-the-end sentinel. */ - sentinel end() const { return sentinel{}; } -}; - -/** - * Iterator of scalars - * - * @return A lazy `ScalarStreamIterator` range that pulls one element per step and releases the producer iterator when exhausted or destroyed. - */ -inline ScalarStreamIterator scalar_stream() { - check_library(); - edge_cases_error err{}; - edge_cases_edge_cases_ScalarStreamIterator* iter = edge_cases_edge_cases_scalar_stream(&err); - detail::check(err); - return ScalarStreamIterator(adopt, iter); -} - -/** - * A lazy, move-only range over the `Mode` elements `mode_stream()` produces. - * - * Each step pulls one element from the producer, so results stream in constant memory. The range releases the producer iterator exactly once: when it's exhausted, or from the destructor when iteration stops early. - */ -class ModeStreamIterator { - edge_cases_edge_cases_ModeStreamIterator* handle_; - -public: - /** Adopts a producer iterator. */ - explicit ModeStreamIterator(adopt_t, edge_cases_edge_cases_ModeStreamIterator* h) noexcept : handle_(h) {} - - /** Releases the producer iterator if it's still held. */ - ~ModeStreamIterator() { - if (handle_ != nullptr) edge_cases_edge_cases_ModeStreamIterator_destroy(handle_); - } - - ModeStreamIterator(const ModeStreamIterator&) = delete; - ModeStreamIterator& operator=(const ModeStreamIterator&) = delete; - - /** Transfers `other`'s iterator; `other` becomes empty. */ - ModeStreamIterator(ModeStreamIterator&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current iterator and takes over `other`'s. */ - ModeStreamIterator& operator=(ModeStreamIterator&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) edge_cases_edge_cases_ModeStreamIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element, or `std::nullopt` once exhausted (which releases the producer iterator). A producer error releases the iterator and throws InternalError. */ - std::optional next() { - if (handle_ == nullptr) return std::nullopt; - edge_cases_error err{}; - edge_cases_edge_cases_Mode item{}; - int32_t has_item = edge_cases_edge_cases_ModeStreamIterator_next(handle_, &item, &err); - if (err.code != 0 || has_item == 0) { - edge_cases_edge_cases_ModeStreamIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - return std::nullopt; - } - return std::optional(std::in_place, static_cast(item)); - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element. */ - class iterator { - ModeStreamIterator* range_; - std::optional current_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = Mode; - using difference_type = std::ptrdiff_t; - using pointer = Mode*; - using reference = Mode&; - - /** Binds to `range` and pulls the first element. */ - explicit iterator(ModeStreamIterator* range) : range_(range), current_(range->next()) {} - reference operator*() { return *current_; } - pointer operator->() { return &*current_; } - iterator& operator++() { current_ = range_->next(); return *this; } - void operator++(int) { current_ = range_->next(); } - bool operator==(sentinel) const { return !current_.has_value(); } - bool operator!=(sentinel) const { return current_.has_value(); } - }; - - /** Begins iteration by pulling the first element. */ - iterator begin() { return iterator(this); } - - /** The past-the-end sentinel. */ - sentinel end() const { return sentinel{}; } -}; - -/** - * Iterator of enums - * - * @return A lazy `ModeStreamIterator` range that pulls one element per step and releases the producer iterator when exhausted or destroyed. - */ -inline ModeStreamIterator mode_stream() { - check_library(); - edge_cases_error err{}; - edge_cases_edge_cases_ModeStreamIterator* iter = edge_cases_edge_cases_mode_stream(&err); - detail::check(err); - return ModeStreamIterator(adopt, iter); -} - -/** - * Async with no result - * - * @return A future settled from a producer thread; get() rethrows the call's error. - */ -inline std::future async_void() { - check_library(); - auto promise = std::make_unique>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_void([](void* context, edge_cases_error* err) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** @return A future settled from a producer thread; get() rethrows the call's error. */ -inline std::future> async_bytes(uint32_t size) { - check_library(); - auto promise = std::make_unique>>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_bytes(size, [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr>> p(static_cast>*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(detail::take_bytes(result_ptr, result_len)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** - * @return A future settled from a producer thread; get() rethrows the call's error. - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ -inline std::future async_struct() { - check_library(); - auto promise = std::make_unique>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_struct([](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_edge_cases_error(err->code, message, err->payload_ptr, err->payload_len)); - } else { - p->set_value(detail::take(result_ptr, result_len, &detail::read_Outer)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** - * @param cancel_token Cancels the call; the future then throws Cancelled. - * @return A future settled from a producer thread; get() rethrows the call's error. - * @throws EdgeErrorsError when the call fails with one of the domain's codes. - */ -inline std::future> async_list(const CancelToken& cancel_token = CancelToken::none()) { - check_library(); - auto promise = std::make_unique>>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_list(cancel_token.handle(), [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr>> p(static_cast>*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_edge_cases_error(err->code, message, err->payload_ptr, err->payload_len)); - } else { - p->set_value(detail::take(result_ptr, result_len, &detail::read_list_Outer)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** @return A future settled from a producer thread; get() rethrows the call's error. */ -inline std::future> async_optional() { - check_library(); - auto promise = std::make_unique>>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_optional([](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr>> p(static_cast>*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(detail::take(result_ptr, result_len, &detail::read_opt_i64)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** @return A future settled from a producer thread; get() rethrows the call's error. */ -inline std::future async_enum() { - check_library(); - auto promise = std::make_unique>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_enum([](void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(static_cast(result)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -/** @return A future settled from a producer thread; get() rethrows the call's error. */ -inline std::future async_keywords(std::string_view class_, int32_t import) { - check_library(); - auto promise = std::make_unique>(); - auto future = promise->get_future(); - edge_cases_edge_cases_async_keywords(detail::utf8(class_), class_.size(), import, [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(detail::take_string(result_ptr, result_len)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - edge_cases_error_free(err); - }, static_cast(promise.release())); - return future; -} - -inline Keywords keywords_roundtrip(const Keywords& value) { - check_library(); - detail::BufferWriter value_buf; - detail::write_Keywords(value_buf, value); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_keywords_roundtrip(value_buf.data(), value_buf.size(), &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Keywords); -} - -[[deprecated("Use Mode-based APIs")]] -inline Retired legacy_value(Legacy value) { - check_library(); - size_t out_len = 0; - edge_cases_error err{}; - auto result = edge_cases_edge_cases_legacy_value(static_cast(static_cast(value)), &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Retired); -} - -} // namespace edge_cases - -} // namespace edge_cases - -// END edge_cases.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap deleted file mode 100644 index fc660879..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -cmake_minimum_required(VERSION 3.14) -project(kitchen_sink_cpp VERSION 0.1.0 LANGUAGES CXX) - -# The producer library to link: a full path from the KITCHEN_SINK_LIBRARY cache or -# environment variable, or else the library name `kitchen_sink` (a CMake target -# of that name, or libkitchen_sink on the linker search path). -set(KITCHEN_SINK_LIBRARY "$ENV{KITCHEN_SINK_LIBRARY}" CACHE FILEPATH "Path to the kitchen_sink native library") - -add_library(kitchen_sink_cpp INTERFACE) -add_library(kitchen_sink::cpp ALIAS kitchen_sink_cpp) -target_include_directories(kitchen_sink_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_compile_features(kitchen_sink_cpp INTERFACE cxx_std_17) -if(KITCHEN_SINK_LIBRARY) - target_link_libraries(kitchen_sink_cpp INTERFACE "${KITCHEN_SINK_LIBRARY}") -else() - target_link_libraries(kitchen_sink_cpp INTERFACE kitchen_sink) -endif() - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap deleted file mode 100644 index 734d3c47..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap +++ /dev/null @@ -1,43 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink C++ bindings - -A header-only C++17 wrapper over the `kitchen_sink` C ABI. `kitchen_sink.hpp` -includes the C header beside it; every declaration lives in -`namespace kitchen_sink`. - -## Prerequisites - -- CMake 3.14 or later -- A C++17 compiler -- The `kitchen_sink` native library (`libkitchen_sink.so`, `libkitchen_sink.dylib`, or - `kitchen_sink.dll`) - -## Use with CMake - -```cmake -add_subdirectory(path/to/cpp) -add_executable(app main.cpp) -target_link_libraries(app PRIVATE kitchen_sink::cpp) -``` - -`kitchen_sink::cpp` adds this directory to the include path, requires -C++17, and links the native library: the file named by the `KITCHEN_SINK_LIBRARY` -CMake cache variable or environment variable when set, or else -`kitchen_sink` by name. - -```cpp -#include "kitchen_sink.hpp" -``` - -The first free function, constructor, or static member you call checks -that the linked library matches these headers and throws -`kitchen_sink::LoadError` if it doesn't; call `kitchen_sink::check_library()` -at startup to check eagerly. - - diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap index fdfc2800..ad491a55 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap @@ -21,6 +21,7 @@ source: crates/weaveffi-cli/tests/snapshots.rs #include #include #include +#include #include #include #include @@ -28,23 +29,26 @@ source: crates/weaveffi-cli/tests/snapshots.rs namespace kitchen_sink { /** - * An error a call that declares errors reports. + * The base of every exception this library throws. * - * A positive code is a declared code of the module's error domain, thrown as - * that domain's typed subclass. A negative code is a runtime failure: -1 a - * generic producer error, -2 a producer panic, -3 a marshalling failure, and - * -4 a callback-interface implementation that failed. A cancelled async call - * (-5) throws Cancelled. + * `code()` is the failure's error code. A positive code belongs to an error + * domain and is thrown as that domain's class (or the class of the code). + * A negative code is a runtime failure: -1 an untyped error (a call declared + * `throws: any`), -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that failed, and -5 a cancelled async + * call (thrown as Cancelled). Code 0 is a failure these bindings detected + * without calling the producer (LoadError). */ class Error : public std::runtime_error { - int32_t code_; - public: /** Builds an error carrying `code` and `message`. */ Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} /** The numeric error code. */ int32_t code() const noexcept { return code_; } + +private: + int32_t code_; }; /** Thrown by an async call that was cancelled through its CancelToken (code -5). */ @@ -55,39 +59,27 @@ public: }; /** - * Thrown by a call that declares no errors when it fails anyway: a producer - * panic (-2), a marshalling failure (-3), a callback-interface failure the - * producer let through (-4), or another runtime failure (-1). These are bugs, - * so no signature declares them; `what()` names the code and the producer's - * message. + * Thrown when a call that declares no errors fails anyway: a producer panic + * (-2), a marshalling failure (-3), a callback-interface failure the producer + * let through (-4), or another runtime failure. These are bugs, so no + * signature declares them. A value buffer these bindings can't decode is + * also an InternalError with code -3. */ -class InternalError : public std::runtime_error { - int32_t code_; - std::string message_; - +class InternalError : public Error { public: - /** Builds an internal error carrying `code` and the producer's `message`. */ - InternalError(int32_t code, const std::string& message) - : std::runtime_error("kitchen_sink: runtime error " + std::to_string(code) + ": " + message), - code_(code), - message_(message) {} - - /** The runtime error code (negative). */ - int32_t code() const noexcept { return code_; } - - /** The producer's message. */ - const std::string& message() const noexcept { return message_; } + /** Builds an internal error carrying the runtime `code` and the producer's `message`. */ + InternalError(int32_t code, const std::string& message) : Error(code, message) {} }; /** * Thrown on first use when the linked kitchen_sink library implements a * different C ABI revision than this header, or lacks or changed a - * declaration this header was generated with. + * declaration this header was generated with. Its code is 0. */ -class LoadError : public std::runtime_error { +class LoadError : public Error { public: /** Builds a load error describing the mismatch. */ - explicit LoadError(const std::string& message) : std::runtime_error(message) {} + explicit LoadError(const std::string& message) : Error(0, message) {} }; /** @@ -101,143 +93,499 @@ struct adopt_t { /** The adopt_t tag value: `Store s(adopt, raw)`. */ inline constexpr adopt_t adopt{}; -/** - * A cancellation token for cancellable async functions. - * - * Create one, pass it to any number of cancellable calls, and call cancel() - * from any thread. A cancelled call settles its future with Cancelled. Each - * call takes its own reference to the native token, so the token can be - * destroyed at any time; the destructor releases this wrapper's reference. - */ -class CancelToken { - kitchen_sink_cancel_token* handle_; +namespace detail { - struct none_t {}; - explicit CancelToken(none_t) noexcept : handle_(nullptr) {} +/** The C error slot every fallible call fills. */ +using error = kitchen_sink_error; -public: - /** Creates a new, uncancelled token. */ - CancelToken() : handle_(kitchen_sink_cancel_token_create()) { - if (handle_ == nullptr) throw std::bad_alloc(); - } +/** The message of a failed call's error slot (empty when the producer sent none). */ +inline std::string message(const error& err) { + if (err.message_ptr == nullptr || err.message_len == 0) return std::string(); + return std::string(reinterpret_cast(err.message_ptr), err.message_len); +} + +/** Fills a callback's error slot with `code` and a copy of `message`. */ +inline void set_error(error* out_err, int32_t code, std::string_view message) noexcept { + kitchen_sink_error_set(out_err, code, reinterpret_cast(message.data()), message.size()); +} - /** Releases this wrapper's reference to the native token. */ - ~CancelToken() { - if (handle_ != nullptr) kitchen_sink_cancel_token_destroy(handle_); +/** + * Reports the exception being handled through a callback's error slot as + * `code` with its `what()`. Call it only from inside a catch handler. + */ +inline void report_current(error* out_err, int32_t code) noexcept { + try { + throw; + } catch (const std::exception& e) { + set_error(out_err, code, e.what()); + } catch (...) { + set_error(out_err, code, "a callback implementation threw an exception not derived from std::exception"); } +} - CancelToken(const CancelToken&) = delete; - CancelToken& operator=(const CancelToken&) = delete; +/** + * How a call that throws `E` turns a failed error slot into an exception + * (`raise`), and how a callback method that may throw `E` reports the + * exception being handled to the producer (`report`). Specialized for + * InternalError (calls that declare no errors), Error (`throws: any`), and + * each error domain. + */ +template +struct Errors; - /** Transfers the native token; `other` becomes empty. */ - CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } +/** Throws Cancelled for -5 and `E` for every other code. */ +template +[[noreturn]] void raise_runtime(const error& err) { + if (err.code == -5) throw Cancelled(message(err)); + throw E(err.code, message(err)); +} - /** Releases the current token and takes over `other`'s. */ - CancelToken& operator=(CancelToken&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) kitchen_sink_cancel_token_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } +/** A call that declares no errors: any failure is an InternalError. A callback method reports -4. */ +template <> +struct Errors { + [[noreturn]] static void raise(const error& err) { raise_runtime(err); } + static void report(error* out_err) noexcept { report_current(out_err, -4); } +}; - /** The empty token a cancellable call uses when none is passed. */ - static const CancelToken& none() noexcept { - static const CancelToken token{none_t{}}; - return token; - } +/** A call declared `throws: any`: any failure is an Error. A callback method reports -1. */ +template <> +struct Errors { + [[noreturn]] static void raise(const error& err) { raise_runtime(err); } + static void report(error* out_err) noexcept { report_current(out_err, -1); } +}; - /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ - void cancel() const noexcept { - if (handle_ != nullptr) kitchen_sink_cancel_token_cancel(handle_); - } +/** Releases an error slot's message and payload on scope exit. */ +struct ErrorClear { + error* err; + ~ErrorClear() { kitchen_sink_error_clear(err); } +}; - /** Whether cancel() has been called. */ - bool is_cancelled() const noexcept { - return handle_ != nullptr && kitchen_sink_cancel_token_is_cancelled(handle_); - } +/** Throws the `E` exception for a failed call's error slot, which it then clears. */ +template +void check(error& err) { + if (err.code == 0) return; + ErrorClear clear{&err}; + Errors::raise(err); +} - /** The native token, borrowed (null for an empty token). */ - kitchen_sink_cancel_token* handle() const noexcept { return handle_; } +/** Releases an async completion's heap-boxed error. */ +struct ErrorFree { + void operator()(error* err) const noexcept { kitchen_sink_error_free(err); } }; -namespace detail { - -/** The exception for a failed call that declares no errors: Cancelled for -5, else InternalError. */ -inline std::exception_ptr make_internal_error(int32_t code, const std::string& message) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - return std::make_exception_ptr(InternalError(code, message)); +/** + * Settles the `std::promise` an async call passed as `context`, exactly + * once: with the `E` exception for a failure, else with `value()` (which + * takes ownership of the result slots). Releases the boxed error. Runs on + * a producer thread inside a C frame, so nothing unwinds out of it. + */ +template +void settle(void* context, error* err, F&& value) noexcept { + std::unique_ptr> promise(static_cast*>(context)); + std::unique_ptr owned(err); + try { + if (err != nullptr && err->code != 0) Errors::raise(*err); + if constexpr (std::is_void_v) { + value(); + promise->set_value(); + } else { + promise->set_value(value()); + } + } catch (...) { + promise->set_exception(std::current_exception()); + } } -/** The message of a nonzero error slot. */ -inline std::string error_message(const kitchen_sink_error& err) { - return std::string(err.message != nullptr ? err.message : "unknown error"); +/** + * Runs a callback method's `body` for a trampoline, reporting any exception + * through `out_err` as a method that throws `E` does. Returns the body's + * value, or a zero value after a failure. + */ +template +auto callback(error* out_err, F&& body) noexcept -> decltype(body()) { + try { + return body(); + } catch (...) { + Errors::report(out_err); + } + if constexpr (!std::is_void_v) return decltype(body()){}; } -/** Throws InternalError if `err` carries a nonzero code (a call that declares no errors). */ -inline void check(kitchen_sink_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_internal_error(err.code, error_message(err)); - kitchen_sink_error_clear(&err); - std::rethrow_exception(ex); +/** Releases a producer run (a string, bytes, value buffer, or typed array) on scope exit. */ +struct Run { + const void* ptr; + size_t len; + ~Run() { + if (ptr != nullptr) kitchen_sink_free_bytes(static_cast(const_cast(ptr)), len); + } +}; + +/** The byte size of `count` elements of `T`, failing with -3 when it overflows. */ +template +size_t byte_size(size_t count) { + if (count > SIZE_MAX / sizeof(T)) throw Error(-3, "array is too large"); + return count * sizeof(T); } /** Copies a producer-returned UTF-8 run into a string, then releases it. */ inline std::string take_string(const uint8_t* ptr, size_t len) { + Run run{ptr, len}; if (ptr == nullptr) return std::string(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { kitchen_sink_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; return std::string(reinterpret_cast(ptr), len); } /** Copies a producer-returned byte run into a vector, then releases it. */ inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + Run run{ptr, len}; if (ptr == nullptr) return std::vector(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { kitchen_sink_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; return std::vector(ptr, ptr + len); } -/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +/** Copies a producer-returned typed array of `len` elements into a vector, then releases it. */ +template +std::vector take_slice(const T* ptr, size_t len) { + Run run{ptr, len * sizeof(T)}; + if (ptr == nullptr) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run as a string_view (a zero-length run may be dangling). */ inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { - if (ptr == nullptr || len == 0) return std::string_view(); + if (len == 0) return std::string_view(); return std::string_view(reinterpret_cast(ptr), len); } -/** Copies a borrowed byte run (null when empty) into a vector. */ +/** Copies a borrowed byte run into a vector. */ inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr || len == 0) return std::vector(); + if (len == 0) return std::vector(); return std::vector(ptr, ptr + len); } +/** Copies a borrowed typed array of `len` elements into a vector. */ +template +std::vector borrow_slice(const T* ptr, size_t len) { + if (len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + /** The data pointer of a string argument, as the C ABI expects it. */ inline const uint8_t* utf8(std::string_view s) noexcept { return reinterpret_cast(s.data()); } +/** The C value of a scalar or C-style enum: an enum crosses as its `int32_t` discriminant. */ +template +constexpr auto to_c(T value) noexcept { + if constexpr (std::is_enum_v) { + return static_cast>(value); + } else { + return value; + } +} + +/** The C value slot of an optional scalar or enum argument: its value, or zero when absent. */ +template +constexpr auto value_or_zero(const std::optional& value) noexcept { + return to_c(value.has_value() ? *value : T{}); +} + +/** An optional scalar or enum from its C presence flag and value. */ +template +std::optional lift_optional(bool present, C value) noexcept { + if (!present) return std::nullopt; + return std::optional(static_cast(value)); +} + +/** A wrapper adopting a returned object reference, or none for null. */ +template +std::optional adopt_optional(typename T::raw_type* raw) noexcept { + if (raw == nullptr) return std::nullopt; + return std::optional(std::in_place, adopt, raw); +} + +/** The borrowed C pointer of an optional object argument (null for none). */ +template +const typename T::raw_type* handle_of(const std::optional& object) noexcept { + return object.has_value() ? object->handle() : nullptr; +} + +/** A new strong reference to an optional object a callback returns (null for none). */ +template +typename T::raw_type* clone_of(const std::optional& object) noexcept { + return object.has_value() ? object->clone_handle() : nullptr; +} + +/** Hands a callback's optional scalar or enum return to the producer: writes the value, returns presence. */ +template +bool give_optional(const std::optional& value, C* out_value) noexcept { + if (!value.has_value()) return false; + *out_value = static_cast(*value); + return true; +} + /** * Hands `len` bytes from `data` to the producer through a callback method's * out slots: copies them into a run from kitchen_sink_alloc, which the * producer adopts and frees. */ -inline void hand_over(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { +template +void give_run(const void* data, size_t len, T** out_ptr, size_t* out_len) { uint8_t* run = nullptr; if (len > 0) { run = kitchen_sink_alloc(len); if (run == nullptr) throw std::bad_alloc(); std::memcpy(run, data, len); } - *out_ptr = run; + *out_ptr = reinterpret_cast(run); *out_len = len; } +/** Hands a callback's string return to the producer. */ +inline void give(std::string_view value, uint8_t** out_ptr, size_t* out_len) { + give_run(value.data(), value.size(), out_ptr, out_len); +} + +/** Hands a callback's bytes return to the producer. */ +inline void give(const std::vector& value, uint8_t** out_ptr, size_t* out_len) { + give_run(value.data(), value.size(), out_ptr, out_len); +} + +/** Hands a callback's typed-array return to the producer; `out_len` receives the element count. */ +template +void give_slice(const std::vector& value, T** out_ptr, size_t* out_len) { + give_run(value.data(), byte_size(value.size()), out_ptr, out_len); + *out_len = value.size(); +} + +/** The implementation a callback interface's `ctx` boxes. */ +template +I& implementation(void* ctx) noexcept { + return **static_cast*>(ctx); +} + +/** Deletes a callback interface's `ctx` box once the producer releases it. */ +template +void release(void* ctx) noexcept { + delete static_cast*>(ctx); +} + +/** + * Boxes an implementation as a callback interface's `ctx` (null for an + * empty pointer). The caller releases the box into the call, after which + * the producer owns it and deletes it through the vtable's `free`. + */ +template +std::unique_ptr> lend(std::shared_ptr impl) { + if (!impl) return nullptr; + return std::make_unique>(std::move(impl)); +} + +/** Boxes a required implementation, failing with -3 (naming `param`) when it's empty. */ +template +std::unique_ptr> lend_required(std::shared_ptr impl, const char* param) { + if (!impl) throw Error(-3, std::string(param) + ": null callback interface"); + return lend(std::move(impl)); +} + +/** + * The trampolines and static vtable that adapt implementations of the + * callback interface `I` to its C vtable; specialized per callback + * interface. + */ +template +struct Callbacks; + +/** + * Owns one strong reference to a reference-counted producer object, the + * Rule-of-Zero core of every interface wrapper: copying takes a new + * reference through `Traits::clone`, moving transfers it (leaving null), and + * destruction releases it through `Traits::destroy`. + */ +template +class Handle { +public: + /** The C type of the object. */ + using raw_type = typename Traits::raw_type; + + /** An empty handle. */ + Handle() noexcept = default; + + /** Adopts one strong reference (or null). */ + explicit Handle(raw_type* raw) noexcept : raw_(raw) {} + + /** Takes a new strong reference to `other`'s object. */ + Handle(const Handle& other) noexcept : raw_(other.clone()) {} + + /** Transfers `other`'s reference; `other` becomes empty. */ + Handle(Handle&& other) noexcept : raw_(std::exchange(other.raw_, nullptr)) {} + + /** Copy-and-swap assignment from a copy or a moved-from handle. */ + Handle& operator=(Handle other) noexcept { + std::swap(raw_, other.raw_); + return *this; + } + + /** Releases the reference. */ + ~Handle() { + if (raw_ != nullptr) Traits::destroy(raw_); + } + + /** The object, borrowed (null when empty). */ + raw_type* get() const noexcept { return raw_; } + + /** A new strong reference to the object, which the caller owns (null when empty). */ + raw_type* clone() const noexcept { return raw_ != nullptr ? Traits::clone(raw_) : nullptr; } + + /** Releases the current reference and adopts `raw`. */ + void reset(raw_type* raw) noexcept { *this = Handle(raw); } + +private: + raw_type* raw_ = nullptr; +}; + +} // namespace detail + +/** + * A cancellation token for cancellable async functions. + * + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time. Move-only. + */ +class CancelToken { +public: + /** Creates a new, uncancelled token. */ + CancelToken() : handle_(kitchen_sink_cancel_token_create()) { + if (!handle_) throw std::bad_alloc(); + } + + /** The empty token a cancellable call uses when none is passed; it never cancels. */ + static const CancelToken& none() noexcept { + static const CancelToken token{nullptr}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_) kitchen_sink_cancel_token_cancel(handle_.get()); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { return handle_ && kitchen_sink_cancel_token_is_cancelled(handle_.get()); } + + /** The native token, borrowed (null for an empty token). */ + kitchen_sink_cancel_token* handle() const noexcept { return handle_.get(); } + +private: + struct Destroy { + void operator()(kitchen_sink_cancel_token* token) const noexcept { kitchen_sink_cancel_token_destroy(token); } + }; + + explicit CancelToken(std::nullptr_t) noexcept {} + + std::unique_ptr handle_; +}; + +/** + * A lazy, single-pass, move-only range over the elements a producer + * iterator yields: what every `iter` callable returns. + * + * Each step makes one producer call, so results stream in constant memory: + * + * for (const std::string& key : store.keys(std::nullopt)) { ... } + * + * The range releases the producer iterator exactly once: when it's + * exhausted, when a step fails (rethrowing the call's exception), on + * close(), or from the destructor when iteration stops early. Moving a range + * invalidates its iterators. + */ +template +class Range { +public: + /** The element type. */ + using value_type = T; + + /** Pulls one element into `item`; returns false once exhausted, throws on a producer error. */ + using Next = bool (*)(void* iter, std::optional& item); + + /** Releases the producer iterator. */ + using Destroy = void (*)(void* iter); + + /** Adopts a producer iterator, driven by `next` and released by `destroy`. */ + Range(adopt_t, void* iter, Next next, Destroy destroy) noexcept : iter_(iter, destroy), next_(next) {} + + /** Pulls the next element, or `std::nullopt` once the range is exhausted. */ + std::optional next() { + std::optional item; + if (!iter_) return item; + bool more = false; + try { + more = next_(iter_.get(), item); + } catch (...) { + iter_.reset(); + throw; + } + if (!more) iter_.reset(); + return item; + } + + /** Releases the producer iterator now; the range is then exhausted. */ + void close() noexcept { iter_.reset(); } + + /** Marks the end of the range. */ + struct sentinel {}; + + /** A single-pass input iterator; each increment pulls one element. */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; + + iterator() noexcept = default; + + reference operator*() const { return *range_->current_; } + pointer operator->() const { return &*range_->current_; } + + iterator& operator++() { + range_->current_ = range_->next(); + return *this; + } + + void operator++(int) { ++*this; } + + friend bool operator==(const iterator& it, sentinel) noexcept { return it.done(); } + friend bool operator==(sentinel end, const iterator& it) noexcept { return it == end; } + friend bool operator!=(const iterator& it, sentinel end) noexcept { return !(it == end); } + friend bool operator!=(sentinel end, const iterator& it) noexcept { return !(it == end); } + + private: + friend class Range; + explicit iterator(Range* range) noexcept : range_(range) {} + bool done() const noexcept { return !range_->current_.has_value(); } + Range* range_ = nullptr; + }; + + /** Begins iteration by pulling the first element. */ + iterator begin() { + current_ = next(); + return iterator(this); + } + + /** The end sentinel. */ + sentinel end() const noexcept { return sentinel{}; } + +private: + std::unique_ptr iter_; + Next next_; + std::optional current_; +}; + +namespace detail { + /** One declaration this header was generated with: its contract id and hash, and its dotted path. */ struct ContractEntry { /** FNV-1a 64 of the declaration's dotted path. */ @@ -283,50 +631,80 @@ std::string contract_mismatch(const kitchen_sink_contract_entry* (*contract)(siz /** The declarations of module `shared` this header was generated with. */ inline constexpr ContractEntry shared_contract[] = { - {0x709e234caa962446ull, 0x3894f2ca452e9f98ull, "shared.ping"}, - {0xcfb306fa5801ee5bull, 0x1cbd9e10983cac2cull, "shared.Token"}, + {0x709e234caa962446ull, 0x3894f2ca452e9f98ull, "shared.ping"}, // function ping() -> string + {0xcfb306fa5801ee5bull, 0x18e9fc204a13df6full, "shared.Token"}, // record Token {i64, string} }; /** The declarations of module `kitchen` this header was generated with. */ inline constexpr ContractEntry kitchen_contract[] = { - {0x01d4f09bbddc986full, 0x29eb88fecc66544dull, "kitchen.Gadget.new"}, - {0x10472d8fe748c2eeull, 0xb265832b6a759fc4ull, "kitchen.maybe_gadget"}, - {0x11368eec07a74078ull, 0xfa049f78b77e146aull, "kitchen.gadgets"}, - {0x137f08a29cde6021ull, 0xda8de7aeb2dbb8a3ull, "kitchen.stream_gadgets"}, - {0x2111b54980826dd7ull, 0x452783f3e6ca6e1dull, "kitchen.do_cancellable"}, - {0x29dddbc60bf8d7dbull, 0x452a3e573d57f6dbull, "kitchen.Priority"}, - {0x2b227003b74a0d16ull, 0x12332957470e9782ull, "kitchen.maybe_subscribe"}, - {0x2dd66cb0064f0494ull, 0x60197363fefd92ddull, "kitchen.Item"}, - {0x33c265bbf079b0e9ull, 0xc3dcd0927e6c1debull, "kitchen.u32_id"}, - {0x33e6381d01660dfeull, 0x077c63c31fd7f90aull, "kitchen.KitchenErrors"}, - {0x3f5fc99d73504b76ull, 0x2df88d710f1ea10eull, "kitchen.Gadget.poke"}, - {0x4c5764cf109528d1ull, 0x2a65529af4338a58ull, "kitchen.maybe_item"}, - {0x4de17dfb93fd494dull, 0xba0ad0dd52ed74afull, "kitchen.i32_id"}, - {0x504e95b7f7e65570ull, 0x01b3e8e0c9a10e23ull, "kitchen.stream_items"}, - {0x68cdb9a4bd3e3dd5ull, 0xc678b9164348d497ull, "kitchen.bool_id"}, - {0x69e7cd703d16eb85ull, 0xf3b28dd22ae9e689ull, "kitchen.do_async"}, - {0x70865079ce41be82ull, 0xbe3b69464672af46ull, "kitchen.Gadget.describe"}, - {0x71c5a1116cb7c49aull, 0xcba1837108a89b84ull, "kitchen.echo_bytes"}, - {0x735ee1e0049ee91aull, 0xca74320bc53eddabull, "kitchen.echo_string"}, - {0x76d7fc58f4e41d84ull, 0x7373a0e47d30e690ull, "kitchen.i64_id"}, - {0x79abfe1aa960fd6aull, 0xbca7a9e5eb26f3a6ull, "kitchen.nested.hello"}, - {0x7d3dd7d5cf6749e3ull, 0xa192f5a7298adb39ull, "kitchen.new_op"}, - {0x89a5e664bc25c67bull, 0x5764a95e81ecfd31ull, "kitchen.f64_id"}, - {0x8cfdbdcecac5f7f1ull, 0x07e272f09c41fffcull, "kitchen.Gadget.version"}, - {0x974b9f3e823c1738ull, 0xb81b91e975695f27ull, "kitchen.cross_module_token"}, - {0xaca0655fac15d230ull, 0x11c4d2b28abe07bbull, "kitchen.list_items"}, - {0xc883b4afae20543eull, 0xc40560e5411ff392ull, "kitchen.legacy_op"}, - {0xf2f7753e6e1dffc2ull, 0xc9047a95bdf49e93ull, "kitchen.get_attrs"}, - {0xfae25d3391922eadull, 0x1c7f95739f3210b8ull, "kitchen.subscribe"}, - {0xfd0059b05cb2fcfbull, 0x248bf8404a5823b4ull, "kitchen.Gadget"}, - {0xfed9c551fb9fd2d6ull, 0x59aec980ef3f3291ull, "kitchen.ReadyListener"}, + {0x01d4f09bbddc986full, 0xffcf2bd3b04aa002ull, "kitchen.Gadget.new"}, // constructor new(i64) -> Gadget + {0x02bbd5f31c953704ull, 0x2cfa679ea1373e09ull, "kitchen.Sampler.samples"}, // callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + {0x10472d8fe748c2eeull, 0xcc570a2649bbf7dcull, "kitchen.maybe_gadget"}, // function maybe_gadget(Gadget?) -> Gadget? + {0x11368eec07a74078ull, 0xfa049f78b77e146aull, "kitchen.gadgets"}, // function gadgets() -> [Gadget] + {0x137f08a29cde6021ull, 0xda8de7aeb2dbb8a3ull, "kitchen.stream_gadgets"}, // function stream_gadgets() -> iter + {0x184bfc4ab0e3fe45ull, 0xb64587477022696bull, "kitchen.ids_u64"}, // function ids_u64([u64]) -> [u64] + {0x2111b54980826dd7ull, 0x627bf01ab365b209ull, "kitchen.do_cancellable"}, // function do_cancellable(string) -> string async cancellable + {0x21c70f9371374d74ull, 0x25d5d88a30fe0a79ull, "kitchen.ReadyListener.favorite"}, // callback_method #4 favorite() -> Gadget + {0x23de5bd597372f46ull, 0xcf1bc6bb37872798ull, "kitchen.stream_chunks"}, // function stream_chunks() -> iter<[i32]> + {0x249f1ddaec6a6cbeull, 0x0710d6c9a4de93daull, "kitchen.take_from_pantry"}, // function take_from_pantry(string) -> i32 throws PantryError + {0x29dddbc60bf8d7dbull, 0x452a3e573d57f6dbull, "kitchen.Priority"}, // enum Priority {Low = 0, Normal = 1, High = 2} + {0x2b227003b74a0d16ull, 0x5dae036b8a6aed30ull, "kitchen.maybe_subscribe"}, // function maybe_subscribe(ReadyListener?) -> bool + {0x2dd66cb0064f0494ull, 0x74b131724dd0d867ull, "kitchen.Item"}, // record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + {0x33c265bbf079b0e9ull, 0x97c32b764693f2cbull, "kitchen.u32_id"}, // function u32_id(u32) -> u32 + {0x33e6381d01660dfeull, 0x05e35a563089bf7dull, "kitchen.KitchenErrors"}, // errors KitchenErrors + {0x3d969ae76db6cb23ull, 0x65a325cfc772296bull, "kitchen.ReadyListener.latest"}, // callback_method #3 latest() -> Item? + {0x3f5fc99d73504b76ull, 0x417e4a2acd3210b3ull, "kitchen.Gadget.poke"}, // method poke(i32) -> i32 throws KitchenErrors + {0x4082f0085db28781ull, 0x18848e701962912eull, "kitchen.Sampler"}, // callback Sampler + {0x4340a6b1e7ec8c50ull, 0xa59d0f32a66dc7a2ull, "kitchen.Sampler.describe"}, // callback_method #2 describe() -> string throws any + {0x4c5764cf109528d1ull, 0xb374accf14676d90ull, "kitchen.maybe_item"}, // function maybe_item(i64) -> Item? throws KitchenErrors + {0x4de17dfb93fd494dull, 0x78be3203bcd10727ull, "kitchen.i32_id"}, // function i32_id(i32) -> i32 + {0x504e95b7f7e65570ull, 0x01b3e8e0c9a10e23ull, "kitchen.stream_items"}, // function stream_items() -> iter + {0x5dafabb7775fcb2dull, 0x6c4b78c7d3f858c1ull, "kitchen.PantryError.Spoiled"}, // code Spoiled = 2 + {0x5e0a8f6be4b00600ull, 0x18ea1a42036c96a0ull, "kitchen.Sampler.threshold"}, // callback_method #0 threshold(u32?) -> f64? + {0x5ec0af1d4ecc6fedull, 0x6540bdb74246420cull, "kitchen.async_samples"}, // function async_samples(u32) -> [f32] async cancellable + {0x61a358b0571e0a4eull, 0x11a24c4b50d11996ull, "kitchen.async_maybe_count"}, // function async_maybe_count() -> i64? async + {0x68cdb9a4bd3e3dd5ull, 0xcde022fe6bc3b1bfull, "kitchen.bool_id"}, // function bool_id(bool) -> bool + {0x69e7cd703d16eb85ull, 0xb13cb8d39e702c79ull, "kitchen.do_async"}, // function do_async(string) -> string async + {0x70865079ce41be82ull, 0xbe3b69464672af46ull, "kitchen.Gadget.describe"}, // method describe() -> string + {0x71c5a1116cb7c49aull, 0x1506cfe6f548fac4ull, "kitchen.echo_bytes"}, // function echo_bytes(bytes) -> bytes + {0x735ee1e0049ee91aull, 0xa713dbd1be707752ull, "kitchen.echo_string"}, // function echo_string(string) -> string + {0x76d7fc58f4e41d84ull, 0x6bdb4ccea2841d5eull, "kitchen.i64_id"}, // function i64_id(i64) -> i64 + {0x78d91fd617e162b8ull, 0xd88e2753503b36dfull, "kitchen.stream_maybe"}, // function stream_maybe() -> iter + {0x79abfe1aa960fd6aull, 0xbca7a9e5eb26f3a6ull, "kitchen.nested.hello"}, // function hello() -> string + {0x7b3865b536da998cull, 0x1c4e79d60a20e74full, "kitchen.mixed_slices"}, // function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + {0x7ba266dc26a58989ull, 0x0f86001cac0c21e8ull, "kitchen.KitchenErrors.InvalidInput"}, // code InvalidInput = 2 + {0x7d3dd7d5cf6749e3ull, 0xa192f5a7298adb39ull, "kitchen.new_op"}, // function new_op() -> i32 + {0x81ded69adee0abf2ull, 0x8c71141a931906f6ull, "kitchen.scale_i32"}, // function scale_i32([i32], i32) -> [i32] + {0x81fa72769330fed6ull, 0x89681e305b55a4f4ull, "kitchen.ReadyListener.label"}, // callback_method #2 label() -> string throws KitchenErrors + {0x89a5e664bc25c67bull, 0x357fd9b65ad879a9ull, "kitchen.f64_id"}, // function f64_id(f64) -> f64 + {0x8cfdbdcecac5f7f1ull, 0x07e272f09c41fffcull, "kitchen.Gadget.version"}, // static version() -> string + {0x8dc42397e47614b9ull, 0xcddf6fede09b2e7full, "kitchen.opt_priority"}, // function opt_priority(Priority?) -> Priority? + {0x92f060ef68961fc1ull, 0x4af6ce5b4f9fa508ull, "kitchen.ReadyListener.on_item"}, // callback_method #1 on_item(Item, Gadget) -> bool + {0x974b9f3e823c1738ull, 0xb81b91e975695f27ull, "kitchen.cross_module_token"}, // function cross_module_token() -> Token + {0x9f0f001aa85f97d5ull, 0x55815247f895bd45ull, "kitchen.sum_f64"}, // function sum_f64([f64]) -> f64 + {0xab2a29fd1ef5458bull, 0x471a34aa4c09ae2bull, "kitchen.KitchenErrors.NotFound"}, // code NotFound = 1 + {0xaca0655fac15d230ull, 0x11c4d2b28abe07bbull, "kitchen.list_items"}, // function list_items() -> [Item] + {0xb57c8e6ae3fb4383ull, 0x3f857851ab75366cull, "kitchen.PantryError"}, // errors PantryError + {0xbdc795b195edd428ull, 0x36127462f8b0969full, "kitchen.opt_mixed"}, // function opt_mixed(f64, f32?, i64?, string?) -> u16? + {0xbe4821ab3c340e71ull, 0xe1d32e38b766e623ull, "kitchen.ReadyListener.on_ready"}, // callback_method #0 on_ready(i32, string) -> void + {0xc2ad53f2a6a36323ull, 0x071e29abee823f17ull, "kitchen.opt_i32_id"}, // function opt_i32_id(i32?) -> i32? + {0xc7ab409554fd3d7bull, 0xfea12b6bc219fc46ull, "kitchen.parse_any"}, // function parse_any(string) -> i64 throws any + {0xc82a7f2d85f99f4eull, 0xa9bcb8bacedb036cull, "kitchen.sample_with"}, // function sample_with(Sampler, u32?) -> [i16] throws PantryError + {0xc883b4afae20543eull, 0xc40560e5411ff392ull, "kitchen.legacy_op"}, // function legacy_op() -> i32 + {0xd180bf646bff59e2ull, 0xa4515a6d00d7c600ull, "kitchen.PantryError.OutOfStock"}, // code OutOfStock = 1 {string} + {0xe6778df0f5c1b82bull, 0x38eaafdb350559e1ull, "kitchen.opt_bool_id"}, // function opt_bool_id(bool?) -> bool? + {0xf2f7753e6e1dffc2ull, 0xc9047a95bdf49e93ull, "kitchen.get_attrs"}, // function get_attrs() -> {string:i32} + {0xfae25d3391922eadull, 0xcc034cf506cae182ull, "kitchen.subscribe"}, // function subscribe(ReadyListener) -> i32 + {0xfd0059b05cb2fcfbull, 0x248bf8404a5823b4ull, "kitchen.Gadget"}, // interface Gadget + {0xfed9c551fb9fd2d6ull, 0xe1020573f084dc1dull, "kitchen.ReadyListener"}, // callback ReadyListener + {0xff3a99b52597dee2ull, 0x14c4ecac97bdfc9bull, "kitchen.Gadget.readings"}, // method readings(u32?) -> [f64] throws any }; } // namespace detail /** * Verifies that the linked kitchen_sink library implements C ABI revision - * 4 and every declaration this header was generated with, throwing + * 5 and every declaration this header was generated with, throwing * LoadError naming the first mismatch. The check runs once; every free * function, constructor, and static member calls this before its first * native call, so calling it at startup is optional. @@ -353,18 +731,10 @@ namespace detail { /** * Serializes values into the value-buffer wire format: little-endian, packed - * with no alignment, lengths and element counts as u32. + * with no alignment, lengths and element counts as u32. The `write` + * overloads below encode one value of each type. */ class BufferWriter { - std::vector buf_; - - template - void append_le(T v) { - for (size_t i = 0; i < sizeof(T); ++i) { - buf_.push_back(static_cast(v >> (8 * i))); - } - } - public: /** Pointer to the encoded bytes. */ const uint8_t* data() const noexcept { return buf_.data(); } @@ -372,206 +742,317 @@ public: /** Number of encoded bytes. */ size_t size() const noexcept { return buf_.size(); } - void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } - void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } - void write_u8(uint8_t v) { buf_.push_back(v); } - void write_i16(int16_t v) { append_le(static_cast(v)); } - void write_u16(uint16_t v) { append_le(v); } - void write_i32(int32_t v) { append_le(static_cast(v)); } - void write_u32(uint32_t v) { append_le(v); } - void write_i64(int64_t v) { append_le(static_cast(v)); } - void write_u64(uint64_t v) { append_le(v); } - - void write_f32(float v) { - uint32_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - void write_f64(double v) { - uint64_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } + /** Appends one byte. */ + void byte(uint8_t v) { buf_.push_back(v); } - /** Writes a byte length or an element count as a u32. */ - void write_len(size_t n) { - if (static_cast(n) > UINT32_MAX) throw std::length_error("value buffer length exceeds u32"); - append_le(static_cast(n)); + /** Appends an unsigned integer, little-endian. */ + template + void le(U v) { + for (size_t i = 0; i < sizeof(U); ++i) buf_.push_back(static_cast(v >> (8 * i))); } - void write_string(std::string_view v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); + /** Appends a byte length or an element count as a u32, failing with -3 past u32. */ + void len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + le(static_cast(n)); } - void write_bytes(const std::vector& v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); + /** Appends `n` raw bytes. */ + void bytes(const void* data, size_t n) { + const uint8_t* p = static_cast(data); + buf_.insert(buf_.end(), p, p + n); } - /** Writes an optional's presence flag: 0 absent, 1 present. */ - void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } - - /** - * Writes an object token: a fresh strong reference from the wrapper's - * `_clone`, which the reader adopts while `v` keeps its own. - */ - template - void write_object(const T& v) { - write_u64(static_cast(reinterpret_cast(v.clone_handle()))); - } +private: + std::vector buf_; }; /** * Decodes values from the value-buffer wire format. A malformed buffer is a * producer bug (both sides are generated from one API), so every decode * failure throws InternalError with the marshalling code -3. Object tokens - * adopted before a failure live in RAII wrappers that release them on - * unwind. + * adopted before a failure live in wrappers that release them on unwind. */ class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - void require(size_t n, const char* what) const { - if (len_ - pos_ < n) fail(what); - } - - template - T read_le(const char* what) { - require(sizeof(T), what); - uint64_t v = 0; - for (size_t i = 0; i < sizeof(T); ++i) { - v |= static_cast(data_[pos_ + i]) << (8 * i); - } - pos_ += sizeof(T); - return static_cast(v); - } - public: + /** Reads from the `len` bytes at `data` (null when empty). */ + BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0) {} + /** Throws the marshalling failure (-3) for a malformed buffer. */ [[noreturn]] static void fail(const char* what) { throw InternalError(-3, std::string("malformed value buffer: ") + what); } - /** Reads from the `len` bytes at `data` (null when empty). */ - BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} - /** Bytes not yet consumed. */ size_t remaining() const noexcept { return len_ - pos_; } - bool read_bool() { - uint8_t b = read_le("bool"); - if (b > 1) fail("bool byte out of range"); - return b != 0; + /** Consumes `n` bytes, returning where they start. */ + const uint8_t* take(size_t n, const char* what) { + if (remaining() < n) fail(what); + const uint8_t* p = data_ + pos_; + pos_ += n; + return p; } - int8_t read_i8() { return read_le("i8"); } - uint8_t read_u8() { return read_le("u8"); } - int16_t read_i16() { return read_le("i16"); } - uint16_t read_u16() { return read_le("u16"); } - int32_t read_i32() { return read_le("i32"); } - uint32_t read_u32() { return read_le("u32"); } - int64_t read_i64() { return read_le("i64"); } - uint64_t read_u64() { return read_le("u64"); } - - float read_f32() { - uint32_t bits = read_le("f32"); - float v = 0; - std::memcpy(&v, &bits, sizeof(v)); + /** Reads an unsigned integer, little-endian. */ + template + U le(const char* what) { + const uint8_t* p = take(sizeof(U), what); + U v = 0; + for (size_t i = 0; i < sizeof(U); ++i) v = static_cast(v | static_cast(static_cast(p[i]) << (8 * i))); return v; } - double read_f64() { - uint64_t bits = read_le("f64"); - double v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; + /** Reads a 0/1 byte (a bool or an optional's presence flag). */ + bool flag(const char* what) { + uint8_t b = le(what); + if (b > 1) fail(what); + return b != 0; } /** Reads a byte length, rejecting one larger than the bytes remaining. */ - size_t read_len() { - uint32_t n = read_le("length"); - if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); - return static_cast(n); + size_t len() { + size_t n = le("length"); + if (n > remaining()) fail("length prefix exceeds remaining buffer"); + return n; } /** * Reads an element count. Elements may encode to zero bytes, so a count - * larger than the bytes remaining is legal; callers cap preallocation - * with reserve_hint(). + * larger than the bytes remaining is legal; preallocate with reserve_hint(). */ - size_t read_count() { return static_cast(read_le("count")); } + size_t count() { return le("count"); } /** A preallocation size for `n` elements that a malformed count can't inflate. */ size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } - std::string read_string() { - size_t n = read_len(); - std::string v(reinterpret_cast(data_) + pos_, n); - pos_ += n; - return v; - } - - std::vector read_bytes() { - size_t n = read_len(); - std::vector v(data_ + pos_, data_ + pos_ + n); - pos_ += n; - return v; - } - - bool read_option_flag() { - uint8_t b = read_le("option flag"); - if (b > 1) fail("option flag byte out of range"); - return b != 0; - } - - /** Reads an object token, adopting the strong reference it carries into a `T` wrapper. */ - template - T read_object() { - auto* raw = reinterpret_cast(static_cast(read_u64())); - if (raw == nullptr) fail("null object token"); - return T(adopt, raw); - } - /** Rejects unconsumed bytes after decoding a complete value. */ void expect_end() const { if (pos_ != len_) fail("trailing bytes after value"); } + +private: + const uint8_t* data_; + size_t len_; + size_t pos_ = 0; }; -/** Releases a producer-allocated buffer with kitchen_sink_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; +/** Selects a `read` overload by the type it decodes. */ +template +struct type_tag {}; + +/** Reads one `T` (declared below and per value type). */ +template +T read(BufferReader& r) { + return read(r, type_tag{}); +} + +inline void write(BufferWriter& w, bool v) { w.byte(v ? 1 : 0); } +inline void write(BufferWriter& w, int8_t v) { w.byte(static_cast(v)); } +inline void write(BufferWriter& w, uint8_t v) { w.byte(v); } +inline void write(BufferWriter& w, int16_t v) { w.le(static_cast(v)); } +inline void write(BufferWriter& w, uint16_t v) { w.le(v); } +inline void write(BufferWriter& w, int32_t v) { w.le(static_cast(v)); } +inline void write(BufferWriter& w, uint32_t v) { w.le(v); } +inline void write(BufferWriter& w, int64_t v) { w.le(static_cast(v)); } +inline void write(BufferWriter& w, uint64_t v) { w.le(v); } + +inline void write(BufferWriter& w, float v) { + uint32_t bits = 0; + std::memcpy(&bits, &v, sizeof bits); + w.le(bits); +} + +inline void write(BufferWriter& w, double v) { + uint64_t bits = 0; + std::memcpy(&bits, &v, sizeof bits); + w.le(bits); +} + +inline void write(BufferWriter& w, std::string_view v) { + w.len(v.size()); + w.bytes(v.data(), v.size()); +} + +inline void write(BufferWriter& w, const std::vector& v) { + w.len(v.size()); + w.bytes(v.data(), v.size()); +} + +inline bool read(BufferReader& r, type_tag) { return r.flag("bool"); } +inline int8_t read(BufferReader& r, type_tag) { return static_cast(r.le("i8")); } +inline uint8_t read(BufferReader& r, type_tag) { return r.le("u8"); } +inline int16_t read(BufferReader& r, type_tag) { return static_cast(r.le("i16")); } +inline uint16_t read(BufferReader& r, type_tag) { return r.le("u16"); } +inline int32_t read(BufferReader& r, type_tag) { return static_cast(r.le("i32")); } +inline uint32_t read(BufferReader& r, type_tag) { return r.le("u32"); } +inline int64_t read(BufferReader& r, type_tag) { return static_cast(r.le("i64")); } +inline uint64_t read(BufferReader& r, type_tag) { return r.le("u64"); } + +inline float read(BufferReader& r, type_tag) { + uint32_t bits = r.le("f32"); + float v = 0; + std::memcpy(&v, &bits, sizeof v); + return v; +} + +inline double read(BufferReader& r, type_tag) { + uint64_t bits = r.le("f64"); + double v = 0; + std::memcpy(&v, &bits, sizeof v); + return v; +} + +inline std::string read(BufferReader& r, type_tag) { + size_t n = r.len(); + return std::string(reinterpret_cast(r.take(n, "string")), n); +} + +inline std::vector read(BufferReader& r, type_tag>) { + size_t n = r.len(); + const uint8_t* p = r.take(n, "bytes"); + return std::vector(p, p + n); +} + +/** A C-style enum crosses as its `int32_t` discriminant. */ +template , int> = 0> +void write(BufferWriter& w, E v) { + write(w, static_cast(v)); +} + +template , int> = 0> +E read(BufferReader& r, type_tag) { + return static_cast(read(r)); +} + +/** Whether `T` is an interface wrapper (it names the C type it wraps). */ +template +struct is_object : std::false_type {}; + +template +struct is_object> : std::true_type {}; + +/** + * An object crosses as a token: a fresh strong reference from the wrapper's + * `_clone`, which the reader adopts while the writer's wrapper keeps its own. + */ +template ::value, int> = 0> +void write(BufferWriter& w, const T& v) { + write(w, static_cast(reinterpret_cast(v.clone_handle()))); +} + +template ::value, int> = 0> +T read(BufferReader& r, type_tag) { + auto* raw = reinterpret_cast(static_cast(read(r))); + if (raw == nullptr) BufferReader::fail("null object token"); + return T(adopt, raw); +} + +/** `T?`: a presence flag, then the value when present. */ +template +void write(BufferWriter& w, const std::optional& v) { + w.byte(v.has_value() ? 1 : 0); + if (v.has_value()) write(w, *v); +} + +template +std::optional read(BufferReader& r, type_tag>) { + if (!r.flag("option flag")) return std::nullopt; + return std::optional(read(r)); +} + +/** `[T]`: a u32 count, then the elements. */ +template +void write(BufferWriter& w, const std::vector& v) { + w.len(v.size()); + for (const auto& item : v) write(w, item); +} - BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} - BufferGuard(const BufferGuard&) = delete; - BufferGuard& operator=(const BufferGuard&) = delete; +template +std::vector read(BufferReader& r, type_tag>) { + size_t n = r.count(); + std::vector v; + v.reserve(r.reserve_hint(n)); + for (size_t i = 0; i < n; ++i) v.push_back(read(r)); + return v; +} - ~BufferGuard() { - if (ptr != nullptr) kitchen_sink_free_bytes(const_cast(ptr), len); +/** `{K: V}`: a u32 count, then each key and value. A repeated key is malformed. */ +template +void write(BufferWriter& w, const std::unordered_map& v) { + w.len(v.size()); + for (const auto& entry : v) { + write(w, entry.first); + write(w, entry.second); } -}; +} + +template +std::unordered_map read(BufferReader& r, type_tag>) { + size_t n = r.count(); + std::unordered_map v; + v.reserve(r.reserve_hint(n)); + for (size_t i = 0; i < n; ++i) { + K key = read(r); + V value = read(r); + if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); + } + return v; +} -/** Decodes one complete `T` from a borrowed buffer with `read`, rejecting trailing bytes. */ +/** Encodes one value into a fresh buffer. */ template -T decode(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { +BufferWriter encode(const T& value) { + BufferWriter w; + write(w, value); + return w; +} + +/** Decodes one complete `T` from a borrowed buffer, rejecting trailing bytes. */ +template +T decode(const uint8_t* ptr, size_t len) { BufferReader r(ptr, len); - T value = read(r); + T value = read(r); r.expect_end(); return value; } -/** Decodes one complete `T` from a producer-returned buffer with `read`, then releases the buffer. */ +/** Decodes one complete `T` from a producer-returned buffer, then releases the buffer. */ template -T take(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferGuard guard(ptr, len); - return decode(ptr, len, read); +T take(const uint8_t* ptr, size_t len) { + Run run{ptr, len}; + return decode(ptr, len); +} + +/** Hands a callback's buffered return to the producer as a kitchen_sink_alloc run. */ +inline void give(const BufferWriter& value, uint8_t** out_ptr, size_t* out_len) { + give_run(value.data(), value.size(), out_ptr, out_len); +} + +/** Throws the error class `E` of a domain code with fields, decoding them from the error's payload. */ +template +[[noreturn]] void raise_with_fields(const error& err) { + BufferReader r(err.payload_ptr, err.payload_len); + E e{message(err), read(r)...}; + r.expect_end(); + throw e; +} + +/** + * Reports a domain exception from a callback: its code and message, and its + * `fields` encoded as the payload. A field that can't be encoded reports a + * plain failure (-1) instead. + */ +template +void report_with_fields(error* out_err, const Error& e, const Fields&... fields) noexcept { + try { + BufferWriter payload; + (write(payload, fields), ...); + set_error(out_err, e.code(), e.what()); + kitchen_sink_error_set_payload(out_err, payload.data(), payload.size()); + } catch (...) { + set_error(out_err, -1, e.what()); + } } } // namespace detail @@ -590,54 +1071,16 @@ struct Token; struct Item; class Gadget; class ReadyListener; +class Sampler; /** A pocket-sized interface exercising the object surface */ class Gadget { - kitchen_sink_kitchen_Gadget* raw_; - public: /** The C type this class wraps. */ using raw_type = kitchen_sink_kitchen_Gadget; /** Adopts one strong reference to a producer object: `Gadget(adopt, raw)`. */ - explicit Gadget(adopt_t, kitchen_sink_kitchen_Gadget* h) noexcept : raw_(h) {} - - /** Releases this wrapper's reference; the object is dropped with its last one. */ - ~Gadget() { - if (raw_ != nullptr) kitchen_sink_kitchen_Gadget_destroy(raw_); - } - - /** Copies share the object: the copy takes a new strong reference. */ - Gadget(const Gadget& other) : raw_(kitchen_sink_kitchen_Gadget_clone(other.raw_)) {} - - /** Takes a new reference to `other`'s object and releases the current one. */ - Gadget& operator=(const Gadget& other) { - if (this != &other) { - kitchen_sink_kitchen_Gadget* h = kitchen_sink_kitchen_Gadget_clone(other.raw_); - if (raw_ != nullptr) kitchen_sink_kitchen_Gadget_destroy(raw_); - raw_ = h; - } - return *this; - } - - /** Transfers `other`'s reference; `other` becomes empty. */ - Gadget(Gadget&& other) noexcept : raw_(other.raw_) { other.raw_ = nullptr; } - - /** Releases the current reference and takes over `other`'s. */ - Gadget& operator=(Gadget&& other) noexcept { - if (this != &other) { - if (raw_ != nullptr) kitchen_sink_kitchen_Gadget_destroy(raw_); - raw_ = other.raw_; - other.raw_ = nullptr; - } - return *this; - } - - /** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */ - const kitchen_sink_kitchen_Gadget* handle() const noexcept { return raw_; } - - /** A new strong reference to the object, which the caller owns. */ - kitchen_sink_kitchen_Gadget* clone_handle() const { return kitchen_sink_kitchen_Gadget_clone(raw_); } + explicit Gadget(adopt_t, raw_type* raw) noexcept : raw_(raw) {} /** Create a gadget with the given id */ explicit Gadget(int64_t id); @@ -648,313 +1091,264 @@ public: /** * Poke the gadget a number of times, failing on a negative count * - * @throws KitchenErrorsError when the call fails with one of the domain's codes. + * @throws KitchenError (or one of its codes' classes) when the call fails with one of the domain's codes. */ int32_t poke(int32_t times) const; + /** + * The gadget's sensor readings, optionally limited + * + * @throws Error with code -1 and the producer's message when the call fails. + */ + std::vector readings(std::optional limit) const; + /** The gadget subsystem version string */ static std::string version(); + /** The wrapped pointer, borrowed (null after a move): this wrapper keeps its reference. */ + const raw_type* handle() const noexcept { return raw_.get(); } + + /** A new strong reference to the object, which the caller owns. */ + raw_type* clone_handle() const noexcept { return raw_.clone(); } + + /** Whether both wrap the same producer object. */ + friend bool operator==(const Gadget& a, const Gadget& b) noexcept { return a.raw_.get() == b.raw_.get(); } + friend bool operator!=(const Gadget& a, const Gadget& b) noexcept { return !(a == b); } + +private: + struct traits { + using raw_type = kitchen_sink_kitchen_Gadget; + static raw_type* clone(const raw_type* raw) noexcept { return kitchen_sink_kitchen_Gadget_clone(raw); } + static void destroy(raw_type* raw) noexcept { kitchen_sink_kitchen_Gadget_destroy(raw); } + }; + + detail::Handle raw_; }; /** A reusable token referenced across modules */ struct Token { - int64_t id; - std::string label; + int64_t id{}; + std::string label{}; + + /** Memberwise equality; interface members compare by identity. */ + friend bool operator==(const Token& a, const Token& b) { return a.id == b.id && a.label == b.label; } + friend bool operator!=(const Token& a, const Token& b) { return !(a == b); } }; /** Kitchen sink struct exercising every field feature */ struct Item { /** Stable identifier */ - int64_t id; + int64_t id{}; /** Display name */ - std::string name; + std::string name{}; /** Initial count */ - int32_t count; + int32_t count{}; /** Whether the item is enabled */ - bool enabled; + bool enabled{}; /** Scaling ratio */ - double ratio; - uint32_t nick; - std::vector payload; - std::vector tags; - std::unordered_map attrs; - std::optional parent; + double ratio{}; + uint32_t nick{}; + std::vector payload{}; + std::vector tags{}; + std::unordered_map attrs{}; + std::optional parent{}; /** Cross-module struct reference */ - Token token; - Priority priority; + Token token{}; + Priority priority{}; /** An optional object inside a record */ - std::optional gadget; + std::optional gadget{}; + + /** Memberwise equality; interface members compare by identity. */ + friend bool operator==(const Item& a, const Item& b) { + return a.id == b.id + && a.name == b.name + && a.count == b.count + && a.enabled == b.enabled + && a.ratio == b.ratio + && a.nick == b.nick + && a.payload == b.payload + && a.tags == b.tags + && a.attrs == b.attrs + && a.parent == b.parent + && a.token == b.token + && a.priority == b.priority + && a.gadget == b.gadget; + } + friend bool operator!=(const Item& a, const Item& b) { return !(a == b); } }; namespace detail { -inline void write_Token(BufferWriter& w, const Token& v); -inline Token read_Token(BufferReader& r); -inline void write_Item(BufferWriter& w, const Item& v); -inline Item read_Item(BufferReader& r); -inline void write_list_string(BufferWriter& w, const std::vector& v); -inline std::vector read_list_string(BufferReader& r); -inline void write_map_string_string(BufferWriter& w, const std::unordered_map& v); -inline std::unordered_map read_map_string_string(BufferReader& r); -inline void write_opt_i64(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_i64(BufferReader& r); -inline void write_opt_Gadget(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Gadget(BufferReader& r); -inline void write_list_Gadget(BufferWriter& w, const std::vector& v); -inline std::vector read_list_Gadget(BufferReader& r); -inline void write_opt_Item(BufferWriter& w, const std::optional& v); -inline std::optional read_opt_Item(BufferReader& r); -inline void write_list_Item(BufferWriter& w, const std::vector& v); -inline std::vector read_list_Item(BufferReader& r); -inline void write_map_string_i32(BufferWriter& w, const std::unordered_map& v); -inline std::unordered_map read_map_string_i32(BufferReader& r); - -/** Writes one `Token` to a value buffer. */ -inline void write_Token(BufferWriter& w, const Token& v) { - w.write_i64(v.id); - w.write_string(v.label); -} - -/** Reads one `Token` from a value buffer. */ -inline Token read_Token(BufferReader& r) { - return Token{ - r.read_i64(), - r.read_string(), - }; -} - -/** Writes one `Item` to a value buffer. */ -inline void write_Item(BufferWriter& w, const Item& v) { - w.write_i64(v.id); - w.write_string(v.name); - w.write_i32(v.count); - w.write_bool(v.enabled); - w.write_f64(v.ratio); - w.write_u32(v.nick); - w.write_bytes(v.payload); - detail::write_list_string(w, v.tags); - detail::write_map_string_string(w, v.attrs); - detail::write_opt_i64(w, v.parent); - detail::write_Token(w, v.token); - w.write_i32(static_cast(v.priority)); - detail::write_opt_Gadget(w, v.gadget); -} - -/** Reads one `Item` from a value buffer. */ -inline Item read_Item(BufferReader& r) { - return Item{ - r.read_i64(), - r.read_string(), - r.read_i32(), - r.read_bool(), - r.read_f64(), - r.read_u32(), - r.read_bytes(), - detail::read_list_string(r), - detail::read_map_string_string(r), - detail::read_opt_i64(r), - detail::read_Token(r), - static_cast(r.read_i32()), - detail::read_opt_Gadget(r), - }; -} - -/** Writes one `[string]` to a value buffer. */ -inline void write_list_string(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_string(item); -} - -/** Reads one `[string]` from a value buffer. */ -inline std::vector read_list_string(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(r.read_string()); - return v; -} - -/** Writes one `{string:string}` to a value buffer. */ -inline void write_map_string_string(BufferWriter& w, const std::unordered_map& v) { - w.write_len(v.size()); - for (const auto& entry : v) { - w.write_string(entry.first); - w.write_string(entry.second); - } -} - -/** Reads one `{string:string}` from a value buffer. */ -inline std::unordered_map read_map_string_string(BufferReader& r) { - size_t n = r.read_count(); - std::unordered_map v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) { - std::string key = r.read_string(); - std::string value = r.read_string(); - if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); - } - return v; -} - -/** Writes one `i64?` to a value buffer. */ -inline void write_opt_i64(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_i64(*v); -} +inline void write(BufferWriter& w, const Token& v); +inline Token read(BufferReader& r, type_tag); +inline void write(BufferWriter& w, const Item& v); +inline Item read(BufferReader& r, type_tag); -/** Reads one `i64?` from a value buffer. */ -inline std::optional read_opt_i64(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(r.read_i64()); +inline void write(BufferWriter& w, const Token& v) { + write(w, v.id); + write(w, v.label); } -/** Writes one `Gadget?` to a value buffer. */ -inline void write_opt_Gadget(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) w.write_object(*v); +inline Token read(BufferReader& r, type_tag) { + return Token{read(r), read(r)}; } -/** Reads one `Gadget?` from a value buffer. */ -inline std::optional read_opt_Gadget(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(r.read_object()); +inline void write(BufferWriter& w, const Item& v) { + write(w, v.id); + write(w, v.name); + write(w, v.count); + write(w, v.enabled); + write(w, v.ratio); + write(w, v.nick); + write(w, v.payload); + write(w, v.tags); + write(w, v.attrs); + write(w, v.parent); + write(w, v.token); + write(w, v.priority); + write(w, v.gadget); } -/** Writes one `[Gadget]` to a value buffer. */ -inline void write_list_Gadget(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) w.write_object(item); +inline Item read(BufferReader& r, type_tag) { + return Item{ + read(r), + read(r), + read(r), + read(r), + read(r), + read(r), + read>(r), + read>(r), + read>(r), + read>(r), + read(r), + read(r), + read>(r) + }; } -/** Reads one `[Gadget]` from a value buffer. */ -inline std::vector read_list_Gadget(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(r.read_object()); - return v; -} +} // namespace detail -/** Writes one `Item?` to a value buffer. */ -inline void write_opt_Item(BufferWriter& w, const std::optional& v) { - w.write_option_flag(v.has_value()); - if (v.has_value()) detail::write_Item(w, *v); -} +/** The `KitchenErrors` error domain of module `kitchen`: catch a code's class or this. */ +class KitchenError : public Error { +public: + /** Builds an error carrying one of the domain's `code`s and its `message`. */ + KitchenError(int32_t code, const std::string& message) : Error(code, message) {} +}; -/** Reads one `Item?` from a value buffer. */ -inline std::optional read_opt_Item(BufferReader& r) { - if (!r.read_option_flag()) return std::nullopt; - return std::optional(detail::read_Item(r)); -} +/** + * Item not found + * + * Code 1. + */ +class NotFoundError : public KitchenError { +public: + /** Builds the error with the producer's `message` and the code's fields. */ + explicit NotFoundError(const std::string& message) : KitchenError(1, message) {} +}; -/** Writes one `[Item]` to a value buffer. */ -inline void write_list_Item(BufferWriter& w, const std::vector& v) { - w.write_len(v.size()); - for (const auto& item : v) detail::write_Item(w, item); -} +/** + * Invalid input + * + * Code 2. + */ +class InvalidInputError : public KitchenError { +public: + /** Builds the error with the producer's `message` and the code's fields. */ + explicit InvalidInputError(const std::string& message) : KitchenError(2, message) {} +}; -/** Reads one `[Item]` from a value buffer. */ -inline std::vector read_list_Item(BufferReader& r) { - size_t n = r.read_count(); - std::vector v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) v.push_back(detail::read_Item(r)); - return v; -} +namespace detail { -/** Writes one `{string:i32}` to a value buffer. */ -inline void write_map_string_i32(BufferWriter& w, const std::unordered_map& v) { - w.write_len(v.size()); - for (const auto& entry : v) { - w.write_string(entry.first); - w.write_i32(entry.second); +/** Raises and reports `KitchenError` and its codes' classes. */ +template <> +struct Errors { + [[noreturn]] static void raise(const kitchen_sink_error& err) { + if (err.code < 0) raise_runtime(err); + switch (err.code) { + case 1: throw NotFoundError(message(err)); + case 2: throw InvalidInputError(message(err)); + default: throw KitchenError(err.code, message(err)); + } } -} -/** Reads one `{string:i32}` from a value buffer. */ -inline std::unordered_map read_map_string_i32(BufferReader& r) { - size_t n = r.read_count(); - std::unordered_map v; - v.reserve(r.reserve_hint(n)); - for (size_t i = 0; i < n; ++i) { - std::string key = r.read_string(); - int32_t value = r.read_i32(); - if (!v.emplace(std::move(key), std::move(value)).second) BufferReader::fail("repeated map key"); + static void report(kitchen_sink_error* out_err) noexcept { + try { + throw; + } catch (const KitchenError& e) { + set_error(out_err, e.code(), e.what()); + } catch (...) { + report_current(out_err, -1); + } } - return v; -} +}; } // namespace detail -/** The errors the `kitchen` module's throwing calls report; catch a code's subclass or this. */ -class KitchenErrorsError : public Error { +/** The `PantryError` error domain of module `kitchen`: catch a code's class or this. */ +class PantryError : public Error { public: - /** Builds an error carrying a domain `code` and `message`. */ - KitchenErrorsError(int32_t code, const std::string& message) : Error(code, message) {} + /** Builds an error carrying one of the domain's `code`s and its `message`. */ + PantryError(int32_t code, const std::string& message) : Error(code, message) {} }; /** - * Item not found + * Out of stock * * Code 1. */ -class NotFoundError : public KitchenErrorsError { +class OutOfStockError : public PantryError { public: + std::string item; + /** Builds the error with the producer's `message` and the code's fields. */ - explicit NotFoundError(const std::string& message) : KitchenErrorsError(1, message) {} + OutOfStockError(const std::string& message, std::string item) : PantryError(1, message), item(std::move(item)) {} }; /** - * Invalid input + * Spoiled * * Code 2. */ -class InvalidInputError : public KitchenErrorsError { +class SpoiledError : public PantryError { public: /** Builds the error with the producer's `message` and the code's fields. */ - explicit InvalidInputError(const std::string& message) : KitchenErrorsError(2, message) {} + explicit SpoiledError(const std::string& message) : PantryError(2, message) {} }; namespace detail { -/** The exception for a failed `KitchenErrorsError` call: its code's subclass, Cancelled, or the root Error. */ -inline std::exception_ptr make_kitchen_error(int32_t code, const std::string& message, const uint8_t* payload_ptr, size_t payload_len) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - if (code < 0) return std::make_exception_ptr(Error(code, message)); - (void)payload_ptr; - (void)payload_len; - switch (code) { - case 1: return std::make_exception_ptr(NotFoundError(message)); - case 2: return std::make_exception_ptr(InvalidInputError(message)); - default: return std::make_exception_ptr(KitchenErrorsError(code, message)); +/** Raises and reports `PantryError` and its codes' classes. */ +template <> +struct Errors { + [[noreturn]] static void raise(const kitchen_sink_error& err) { + if (err.code < 0) raise_runtime(err); + switch (err.code) { + case 1: raise_with_fields(err); + case 2: throw SpoiledError(message(err)); + default: throw PantryError(err.code, message(err)); + } } -} - -/** Throws the typed `KitchenErrorsError` exception if `err` carries a nonzero code. */ -inline void check_kitchen(kitchen_sink_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_kitchen_error(err.code, error_message(err), err.payload_ptr, err.payload_len); - kitchen_sink_error_clear(&err); - std::rethrow_exception(ex); -} -/** Reports a `KitchenErrorsError` a callback implementation threw through the vtable entry's `out_err`. */ -inline void report_kitchen_error(const KitchenErrorsError& e, kitchen_sink_error* out_err) { - switch (e.code()) { - case 1: kitchen_sink_error_set(out_err, 1, e.what()); return; - case 2: kitchen_sink_error_set(out_err, 2, e.what()); return; - default: break; + static void report(kitchen_sink_error* out_err) noexcept { + try { + throw; + } catch (const OutOfStockError& e) { + report_with_fields(out_err, e, e.item); + } catch (const PantryError& e) { + set_error(out_err, e.code(), e.what()); + } catch (...) { + report_current(out_err, -1); + } } - kitchen_sink_error_set(out_err, -4, e.what()); -} +}; } // namespace detail /** * Consumer-implemented observer of ready items * - * Subclass it and pass a `std::shared_ptr` where the API takes a `ReadyListener`. The producer may call the methods from any thread, so they must be thread-safe, until it releases the implementation (also from any thread). A method that declares errors may throw a `KitchenErrorsError` code's exception, which reaches the producer with its fields; any other exception reaches it as a callback failure (code -4). + * Subclass it and pass a `std::shared_ptr` where the API takes a `ReadyListener`. The producer may call the methods from any thread, so they must be thread-safe, and releases the implementation (also from any thread) once it's done with it. An exception a method throws never unwinds into the producer: a method that declares errors reports it as described on the method, and any other method reports a callback failure (-4). */ class ReadyListener { public: @@ -969,7 +1363,7 @@ public: /** * A display label, failing with a kitchen error * - * @throws KitchenErrorsError to report one of the domain's codes to the producer. + * @throws KitchenError (or one of its codes' classes) to report one of the domain's codes, fields included; any other exception reports a plain failure. */ virtual std::string label() = 0; @@ -982,125 +1376,162 @@ public: namespace detail { -/** Trampolines adapting a `ReadyListener` implementation to `kitchen_sink_kitchen_ReadyListener_vtable`. */ -struct ReadyListener_trampolines { +/** Adapts a `ReadyListener` implementation to `kitchen_sink_kitchen_ReadyListener_vtable`. */ +template <> +struct Callbacks { static void on_ready(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err) { - try { - ReadyListener& impl = **static_cast*>(ctx); - impl.on_ready(code, detail::borrow_string(msg_ptr, msg_len)); - } catch (const std::exception& e) { - kitchen_sink_error_set(out_err, -4, e.what()); - } catch (...) { - kitchen_sink_error_set(out_err, -4, "ReadyListener::on_ready threw a non-standard exception"); - } + callback(out_err, [&] { + implementation(ctx).on_ready(code, borrow_string(msg_ptr, msg_len)); + }); } static bool on_item(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err) { Gadget gadget_arg(adopt, gadget); - try { - ReadyListener& impl = **static_cast*>(ctx); - Item item_arg = detail::decode(item_ptr, item_len, &detail::read_Item); - return impl.on_item(std::move(item_arg), std::move(gadget_arg)); - } catch (const std::exception& e) { - kitchen_sink_error_set(out_err, -4, e.what()); - } catch (...) { - kitchen_sink_error_set(out_err, -4, "ReadyListener::on_item threw a non-standard exception"); - } - return bool{}; + return callback(out_err, [&] { + auto item_arg = decode(item_ptr, item_len); + return implementation(ctx).on_item(std::move(item_arg), std::move(gadget_arg)); + }); } static void label(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { - try { - ReadyListener& impl = **static_cast*>(ctx); - std::string ret = impl.label(); - detail::hand_over(ret.data(), ret.size(), out_ptr, out_len); - } catch (const KitchenErrorsError& e) { - detail::report_kitchen_error(e, out_err); - } catch (const std::exception& e) { - kitchen_sink_error_set(out_err, -4, e.what()); - } catch (...) { - kitchen_sink_error_set(out_err, -4, "ReadyListener::label threw a non-standard exception"); - } + callback(out_err, [&] { + give(implementation(ctx).label(), out_ptr, out_len); + }); } static void latest(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { - try { - ReadyListener& impl = **static_cast*>(ctx); - std::optional ret = impl.latest(); - detail::BufferWriter ret_buf; - detail::write_opt_Item(ret_buf, ret); - detail::hand_over(ret_buf.data(), ret_buf.size(), out_ptr, out_len); - } catch (const std::exception& e) { - kitchen_sink_error_set(out_err, -4, e.what()); - } catch (...) { - kitchen_sink_error_set(out_err, -4, "ReadyListener::latest threw a non-standard exception"); - } + callback(out_err, [&] { + give(encode(implementation(ctx).latest()), out_ptr, out_len); + }); } static kitchen_sink_kitchen_Gadget* favorite(void* ctx, kitchen_sink_error* out_err) { - try { - ReadyListener& impl = **static_cast*>(ctx); - return impl.favorite().clone_handle(); - } catch (const std::exception& e) { - kitchen_sink_error_set(out_err, -4, e.what()); - } catch (...) { - kitchen_sink_error_set(out_err, -4, "ReadyListener::favorite threw a non-standard exception"); - } - return nullptr; + return callback(out_err, [&] { + return implementation(ctx).favorite().clone_handle(); + }); } - /** Deletes the implementation box once the producer releases it. */ - static void free_ctx(void* ctx) { - delete static_cast*>(ctx); + /** The vtable every `ReadyListener` implementation is passed with. */ + static const kitchen_sink_kitchen_ReadyListener_vtable& vtable() noexcept { + static const kitchen_sink_kitchen_ReadyListener_vtable table = { + sizeof(kitchen_sink_kitchen_ReadyListener_vtable), + 0, + &release, + &on_ready, + &on_item, + &label, + &latest, + &favorite, + }; + return table; } }; -/** The vtable every `ReadyListener` implementation is passed with. */ -inline const kitchen_sink_kitchen_ReadyListener_vtable& ReadyListener_vtable() { - static const kitchen_sink_kitchen_ReadyListener_vtable vtable = { - static_cast(sizeof(kitchen_sink_kitchen_ReadyListener_vtable)), - 0, - &ReadyListener_trampolines::free_ctx, - &ReadyListener_trampolines::on_ready, - &ReadyListener_trampolines::on_item, - &ReadyListener_trampolines::label, - &ReadyListener_trampolines::latest, - &ReadyListener_trampolines::favorite, - }; - return vtable; -} +} // namespace detail + +/** + * Consumer-implemented source of scalar samples + * + * Subclass it and pass a `std::shared_ptr` where the API takes a `Sampler`. The producer may call the methods from any thread, so they must be thread-safe, and releases the implementation (also from any thread) once it's done with it. An exception a method throws never unwinds into the producer: a method that declares errors reports it as described on the method, and any other method reports a callback failure (-4). + */ +class Sampler { +public: + virtual ~Sampler() = default; + + /** The cutoff for a level, if the sampler has one */ + virtual std::optional threshold(std::optional level) = 0; + + /** + * Up to `limit` samples, failing when the pantry is empty + * + * @throws PantryError (or one of its codes' classes) to report one of the domain's codes, fields included; any other exception reports a plain failure. + */ + virtual std::vector samples(std::optional limit, const std::vector& weights) = 0; + + /** + * A free-form description, failing with an untyped error + * + * @throws std::exception to report a failure, with `what()` as its message. + */ + virtual std::string describe() = 0; +}; + +namespace detail { + +/** Adapts a `Sampler` implementation to `kitchen_sink_kitchen_Sampler_vtable`. */ +template <> +struct Callbacks { + static bool threshold(void* ctx, bool has_level, uint32_t level, double* out_value, kitchen_sink_error* out_err) { + return callback(out_err, [&] { + return give_optional(implementation(ctx).threshold(lift_optional(has_level, level)), out_value); + }); + } + + static void samples(void* ctx, bool has_limit, uint32_t limit, const double* weights_ptr, size_t weights_len, int16_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { + callback(out_err, [&] { + give_slice(implementation(ctx).samples(lift_optional(has_limit, limit), borrow_slice(weights_ptr, weights_len)), out_ptr, out_len); + }); + } + + static void describe(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { + callback(out_err, [&] { + give(implementation(ctx).describe(), out_ptr, out_len); + }); + } + + /** The vtable every `Sampler` implementation is passed with. */ + static const kitchen_sink_kitchen_Sampler_vtable& vtable() noexcept { + static const kitchen_sink_kitchen_Sampler_vtable table = { + sizeof(kitchen_sink_kitchen_Sampler_vtable), + 0, + &release, + &threshold, + &samples, + &describe, + }; + return table; + } +}; } // namespace detail -inline Gadget::Gadget(int64_t id) : raw_(nullptr) { +inline Gadget::Gadget(int64_t id) { check_library(); kitchen_sink_error err{}; - auto result = kitchen_sink_kitchen_Gadget_new(id, &err); - detail::check(err); - raw_ = result; + auto* result = kitchen_sink_kitchen_Gadget_new(id, &err); + detail::check(err); + raw_.reset(result); } inline std::string Gadget::describe() const { - size_t out_len = 0; kitchen_sink_error err{}; - auto result = kitchen_sink_kitchen_Gadget_describe(raw_, &out_len, &err); - detail::check(err); + size_t out_len{}; + auto result = kitchen_sink_kitchen_Gadget_describe(raw_.get(), &out_len, &err); + detail::check(err); return detail::take_string(result, out_len); } inline int32_t Gadget::poke(int32_t times) const { kitchen_sink_error err{}; - auto result = kitchen_sink_kitchen_Gadget_poke(raw_, times, &err); - detail::check_kitchen(err); + auto result = kitchen_sink_kitchen_Gadget_poke(raw_.get(), times, &err); + detail::check(err); return result; } +inline std::vector Gadget::readings(std::optional limit) const { + kitchen_sink_error err{}; + size_t out_len{}; + auto result = kitchen_sink_kitchen_Gadget_readings(raw_.get(), limit.has_value(), detail::value_or_zero(limit), &out_len, &err); + detail::check(err); + return detail::take_slice(result, out_len); +} + inline std::string Gadget::version() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_Gadget_version(&out_len, &err); - detail::check(err); + detail::check(err); return detail::take_string(result, out_len); } @@ -1109,10 +1540,10 @@ namespace shared { /** Trivial cross-module helper */ inline std::string ping() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_shared_ping(&out_len, &err); - detail::check(err); + detail::check(err); return detail::take_string(result, out_len); } @@ -1125,7 +1556,7 @@ inline bool bool_id(bool x) { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_bool_id(x, &err); - detail::check(err); + detail::check(err); return result; } @@ -1133,7 +1564,7 @@ inline int32_t i32_id(int32_t x) { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_i32_id(x, &err); - detail::check(err); + detail::check(err); return result; } @@ -1141,7 +1572,7 @@ inline uint32_t u32_id(uint32_t x) { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_u32_id(x, &err); - detail::check(err); + detail::check(err); return result; } @@ -1149,7 +1580,7 @@ inline int64_t i64_id(int64_t x) { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_i64_id(x, &err); - detail::check(err); + detail::check(err); return result; } @@ -1157,47 +1588,46 @@ inline double f64_id(double x) { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_f64_id(x, &err); - detail::check(err); + detail::check(err); return result; } inline std::string echo_string(std::string_view s) { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_echo_string(detail::utf8(s), s.size(), &out_len, &err); - detail::check(err); + detail::check(err); return detail::take_string(result, out_len); } inline std::vector echo_bytes(const std::vector& b) { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_echo_bytes(b.data(), b.size(), &out_len, &err); - detail::check(err); + detail::check(err); return detail::take_bytes(result, out_len); } /** Register a listener, returning how many are attached */ inline int32_t subscribe(std::shared_ptr listener) { check_library(); - if (!listener) throw std::invalid_argument("listener: null callback interface"); - auto listener_ctx = std::make_unique>(std::move(listener)); + auto listener_ctx = detail::lend_required(std::move(listener), "listener"); kitchen_sink_error err{}; - auto result = kitchen_sink_kitchen_subscribe(static_cast(listener_ctx.release()), &detail::ReadyListener_vtable(), &err); - detail::check(err); + auto result = kitchen_sink_kitchen_subscribe(listener_ctx.release(), &detail::Callbacks::vtable(), &err); + detail::check(err); return result; } /** Register a listener if one is given */ inline bool maybe_subscribe(std::shared_ptr listener) { check_library(); - const auto* listener_vtable = listener ? &detail::ReadyListener_vtable() : nullptr; - std::unique_ptr> listener_ctx(listener ? new std::shared_ptr(std::move(listener)) : nullptr); + auto listener_ctx = detail::lend(std::move(listener)); + const auto* listener_vtable = listener_ctx ? &detail::Callbacks::vtable() : nullptr; kitchen_sink_error err{}; - auto result = kitchen_sink_kitchen_maybe_subscribe(static_cast(listener_ctx.release()), listener_vtable, &err); - detail::check(err); + auto result = kitchen_sink_kitchen_maybe_subscribe(listener_ctx.release(), listener_vtable, &err); + detail::check(err); return result; } @@ -1205,250 +1635,108 @@ inline bool maybe_subscribe(std::shared_ptr listener) { inline std::optional maybe_gadget(const std::optional& fallback) { check_library(); kitchen_sink_error err{}; - auto result = kitchen_sink_kitchen_maybe_gadget(fallback.has_value() ? fallback->handle() : nullptr, &err); - detail::check(err); - return result != nullptr ? std::optional(std::in_place, adopt, result) : std::nullopt; + auto result = kitchen_sink_kitchen_maybe_gadget(detail::handle_of(fallback), &err); + detail::check(err); + return detail::adopt_optional(result); } /** A list of objects */ inline std::vector gadgets() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_gadgets(&out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_list_Gadget); + detail::check(err); + return detail::take>(result, out_len); } -/** - * A lazy, move-only range over the `Gadget` elements `stream_gadgets()` produces. - * - * Each step pulls one element from the producer, so results stream in constant memory. The range releases the producer iterator exactly once: when it's exhausted, or from the destructor when iteration stops early. - */ -class StreamGadgetsIterator { - kitchen_sink_kitchen_StreamGadgetsIterator* handle_; - -public: - /** Adopts a producer iterator. */ - explicit StreamGadgetsIterator(adopt_t, kitchen_sink_kitchen_StreamGadgetsIterator* h) noexcept : handle_(h) {} - - /** Releases the producer iterator if it's still held. */ - ~StreamGadgetsIterator() { - if (handle_ != nullptr) kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle_); - } - - StreamGadgetsIterator(const StreamGadgetsIterator&) = delete; - StreamGadgetsIterator& operator=(const StreamGadgetsIterator&) = delete; - - /** Transfers `other`'s iterator; `other` becomes empty. */ - StreamGadgetsIterator(StreamGadgetsIterator&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current iterator and takes over `other`'s. */ - StreamGadgetsIterator& operator=(StreamGadgetsIterator&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element, or `std::nullopt` once exhausted (which releases the producer iterator). A producer error releases the iterator and throws InternalError. */ - std::optional next() { - if (handle_ == nullptr) return std::nullopt; - kitchen_sink_error err{}; - kitchen_sink_kitchen_Gadget* item{}; - int32_t has_item = kitchen_sink_kitchen_StreamGadgetsIterator_next(handle_, &item, &err); - if (err.code != 0 || has_item == 0) { - kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - return std::nullopt; - } - return std::optional(std::in_place, Gadget(adopt, item)); - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element. */ - class iterator { - StreamGadgetsIterator* range_; - std::optional current_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = Gadget; - using difference_type = std::ptrdiff_t; - using pointer = Gadget*; - using reference = Gadget&; - - /** Binds to `range` and pulls the first element. */ - explicit iterator(StreamGadgetsIterator* range) : range_(range), current_(range->next()) {} - reference operator*() { return *current_; } - pointer operator->() { return &*current_; } - iterator& operator++() { current_ = range_->next(); return *this; } - void operator++(int) { current_ = range_->next(); } - bool operator==(sentinel) const { return !current_.has_value(); } - bool operator!=(sentinel) const { return current_.has_value(); } - }; - - /** Begins iteration by pulling the first element. */ - iterator begin() { return iterator(this); } - - /** The past-the-end sentinel. */ - sentinel end() const { return sentinel{}; } -}; - /** * An iterator over objects * - * @return A lazy `StreamGadgetsIterator` range that pulls one element per step and releases the producer iterator when exhausted or destroyed. + * @return A lazy, single-pass range; each step makes one producer call. */ -inline StreamGadgetsIterator stream_gadgets() { +inline Range stream_gadgets() { check_library(); kitchen_sink_error err{}; kitchen_sink_kitchen_StreamGadgetsIterator* iter = kitchen_sink_kitchen_stream_gadgets(&err); - detail::check(err); - return StreamGadgetsIterator(adopt, iter); + detail::check(err); + return Range(adopt, iter, [](void* raw, std::optional& item) { + kitchen_sink_error next_err{}; + kitchen_sink_kitchen_Gadget* out_item{}; + if (kitchen_sink_kitchen_StreamGadgetsIterator_next(static_cast(raw), &out_item, &next_err) == 0) { + detail::check(next_err); + return false; + } + item.emplace(Gadget(adopt, out_item)); + return true; + }, [](void* raw) { kitchen_sink_kitchen_StreamGadgetsIterator_destroy(static_cast(raw)); }); } /** * Optional struct return * - * @throws KitchenErrorsError when the call fails with one of the domain's codes. + * @throws KitchenError (or one of its codes' classes) when the call fails with one of the domain's codes. */ inline std::optional maybe_item(int64_t id) { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_maybe_item(id, &out_len, &err); - detail::check_kitchen(err); - return detail::take(result, out_len, &detail::read_opt_Item); + detail::check(err); + return detail::take>(result, out_len); } /** List of structs */ inline std::vector list_items() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_list_items(&out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_list_Item); + detail::check(err); + return detail::take>(result, out_len); } /** Map return type */ inline std::unordered_map get_attrs() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_get_attrs(&out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_map_string_i32); + detail::check(err); + return detail::take>(result, out_len); } -/** - * A lazy, move-only range over the `Item` elements `stream_items()` produces. - * - * Each step pulls one element from the producer, so results stream in constant memory. The range releases the producer iterator exactly once: when it's exhausted, or from the destructor when iteration stops early. - */ -class StreamItemsIterator { - kitchen_sink_kitchen_StreamItemsIterator* handle_; - -public: - /** Adopts a producer iterator. */ - explicit StreamItemsIterator(adopt_t, kitchen_sink_kitchen_StreamItemsIterator* h) noexcept : handle_(h) {} - - /** Releases the producer iterator if it's still held. */ - ~StreamItemsIterator() { - if (handle_ != nullptr) kitchen_sink_kitchen_StreamItemsIterator_destroy(handle_); - } - - StreamItemsIterator(const StreamItemsIterator&) = delete; - StreamItemsIterator& operator=(const StreamItemsIterator&) = delete; - - /** Transfers `other`'s iterator; `other` becomes empty. */ - StreamItemsIterator(StreamItemsIterator&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current iterator and takes over `other`'s. */ - StreamItemsIterator& operator=(StreamItemsIterator&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) kitchen_sink_kitchen_StreamItemsIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element, or `std::nullopt` once exhausted (which releases the producer iterator). A producer error releases the iterator and throws InternalError. */ - std::optional next() { - if (handle_ == nullptr) return std::nullopt; - kitchen_sink_error err{}; - const uint8_t* item{}; - size_t item_len = 0; - int32_t has_item = kitchen_sink_kitchen_StreamItemsIterator_next(handle_, &item, &item_len, &err); - if (err.code != 0 || has_item == 0) { - kitchen_sink_kitchen_StreamItemsIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - return std::nullopt; - } - return std::optional(std::in_place, detail::take(item, item_len, &detail::read_Item)); - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element. */ - class iterator { - StreamItemsIterator* range_; - std::optional current_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = Item; - using difference_type = std::ptrdiff_t; - using pointer = Item*; - using reference = Item&; - - /** Binds to `range` and pulls the first element. */ - explicit iterator(StreamItemsIterator* range) : range_(range), current_(range->next()) {} - reference operator*() { return *current_; } - pointer operator->() { return &*current_; } - iterator& operator++() { current_ = range_->next(); return *this; } - void operator++(int) { current_ = range_->next(); } - bool operator==(sentinel) const { return !current_.has_value(); } - bool operator!=(sentinel) const { return current_.has_value(); } - }; - - /** Begins iteration by pulling the first element. */ - iterator begin() { return iterator(this); } - - /** The past-the-end sentinel. */ - sentinel end() const { return sentinel{}; } -}; - /** * Iterator return type * - * @return A lazy `StreamItemsIterator` range that pulls one element per step and releases the producer iterator when exhausted or destroyed. + * @return A lazy, single-pass range; each step makes one producer call. */ -inline StreamItemsIterator stream_items() { +inline Range stream_items() { check_library(); kitchen_sink_error err{}; kitchen_sink_kitchen_StreamItemsIterator* iter = kitchen_sink_kitchen_stream_items(&err); - detail::check(err); - return StreamItemsIterator(adopt, iter); + detail::check(err); + return Range(adopt, iter, [](void* raw, std::optional& item) { + kitchen_sink_error next_err{}; + const uint8_t* out_item{}; + size_t out_len{}; + if (kitchen_sink_kitchen_StreamItemsIterator_next(static_cast(raw), &out_item, &out_len, &next_err) == 0) { + detail::check(next_err); + return false; + } + item.emplace(detail::take(out_item, out_len)); + return true; + }, [](void* raw) { kitchen_sink_kitchen_StreamItemsIterator_destroy(static_cast(raw)); }); } /** Returns the shared Token type from another module */ inline Token cross_module_token() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_cross_module_token(&out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Token); + detail::check(err); + return detail::take(result, out_len); } /** @@ -1461,19 +1749,8 @@ inline std::future do_async(std::string_view input) { auto promise = std::make_unique>(); auto future = promise->get_future(); kitchen_sink_kitchen_do_async(detail::utf8(input), input.size(), [](void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(detail::take_string(result_ptr, result_len)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - kitchen_sink_error_free(err); - }, static_cast(promise.release())); + detail::settle(context, err, [&] { return detail::take_string(result_ptr, result_len); }); + }, promise.release()); return future; } @@ -1488,38 +1765,223 @@ inline std::future do_cancellable(std::string_view input, const Can auto promise = std::make_unique>(); auto future = promise->get_future(); kitchen_sink_kitchen_do_cancellable(detail::utf8(input), input.size(), cancel_token.handle(), [](void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { - std::unique_ptr> p(static_cast*>(context)); - try { - if (err != nullptr && err->code != 0) { - std::string message = detail::error_message(*err); - p->set_exception(detail::make_internal_error(err->code, message)); - } else { - p->set_value(detail::take_string(result_ptr, result_len)); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - kitchen_sink_error_free(err); - }, static_cast(promise.release())); + detail::settle(context, err, [&] { return detail::take_string(result_ptr, result_len); }); + }, promise.release()); return future; } /** Legacy operation kept for compatibility */ -[[deprecated("Use new_op instead")]] +[[deprecated("Use `new_op` instead")]] inline int32_t legacy_op() { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_legacy_op(&err); - detail::check(err); + detail::check(err); return result; } -/** Replacement for legacy_op */ +/** Replacement for `legacy_op` */ inline int32_t new_op() { check_library(); kitchen_sink_error err{}; auto result = kitchen_sink_kitchen_new_op(&err); - detail::check(err); + detail::check(err); + return result; +} + +/** Optional scalar in both directions */ +inline std::optional opt_i32_id(std::optional x) { + check_library(); + kitchen_sink_error err{}; + int32_t out_value{}; + auto result = kitchen_sink_kitchen_opt_i32_id(x.has_value(), detail::value_or_zero(x), &out_value, &err); + detail::check(err); + return detail::lift_optional(result, out_value); +} + +inline std::optional opt_bool_id(std::optional x) { + check_library(); + kitchen_sink_error err{}; + bool out_value{}; + auto result = kitchen_sink_kitchen_opt_bool_id(x.has_value(), detail::value_or_zero(x), &out_value, &err); + detail::check(err); + return detail::lift_optional(result, out_value); +} + +/** Optional C-style enum in both directions */ +inline std::optional opt_priority(std::optional p) { + check_library(); + kitchen_sink_error err{}; + kitchen_sink_kitchen_Priority out_value{}; + auto result = kitchen_sink_kitchen_opt_priority(p.has_value(), detail::value_or_zero(p), &out_value, &err); + detail::check(err); + return detail::lift_optional(result, out_value); +} + +/** Optional scalars next to ordinary parameters */ +inline std::optional opt_mixed(double base, std::optional scale, std::optional offset, const std::optional& label) { + check_library(); + const auto label_buf = detail::encode(label); + kitchen_sink_error err{}; + uint16_t out_value{}; + auto result = kitchen_sink_kitchen_opt_mixed(base, scale.has_value(), detail::value_or_zero(scale), offset.has_value(), detail::value_or_zero(offset), label_buf.data(), label_buf.size(), &out_value, &err); + detail::check(err); + return detail::lift_optional(result, out_value); +} + +/** A typed-array parameter */ +inline double sum_f64(const std::vector& xs) { + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_sum_f64(xs.data(), xs.size(), &err); + detail::check(err); + return result; +} + +/** Typed arrays in both directions */ +inline std::vector scale_i32(const std::vector& xs, int32_t factor) { + check_library(); + kitchen_sink_error err{}; + size_t out_len{}; + auto result = kitchen_sink_kitchen_scale_i32(xs.data(), xs.size(), factor, &out_len, &err); + detail::check(err); + return detail::take_slice(result, out_len); +} + +inline std::vector ids_u64(const std::vector& ids) { + check_library(); + kitchen_sink_error err{}; + size_t out_len{}; + auto result = kitchen_sink_kitchen_ids_u64(ids.data(), ids.size(), &out_len, &err); + detail::check(err); + return detail::take_slice(result, out_len); +} + +/** Every slice element type */ +inline std::vector mixed_slices(const std::vector& a, const std::vector& b, const std::vector& c, const std::vector& d, const std::vector& e, const std::vector& f) { + check_library(); + kitchen_sink_error err{}; + size_t out_len{}; + auto result = kitchen_sink_kitchen_mixed_slices(a.data(), a.size(), b.data(), b.size(), c.data(), c.size(), d.data(), d.size(), e.data(), e.size(), f.data(), f.size(), &out_len, &err); + detail::check(err); + return detail::take_slice(result, out_len); +} + +/** + * Async returning an optional scalar + * + * @return A future settled from a producer thread; get() rethrows the call's error. + */ +inline std::future> async_maybe_count() { + check_library(); + auto promise = std::make_unique>>(); + auto future = promise->get_future(); + kitchen_sink_kitchen_async_maybe_count([](void* context, kitchen_sink_error* err, bool has_result, int64_t result) { + detail::settle>(context, err, [&] { return detail::lift_optional(has_result, result); }); + }, promise.release()); + return future; +} + +/** + * Async returning a typed array + * + * @param cancel_token Cancels the call; the future then throws Cancelled. + * @return A future settled from a producer thread; get() rethrows the call's error. + */ +inline std::future> async_samples(uint32_t n, const CancelToken& cancel_token = CancelToken::none()) { + check_library(); + auto promise = std::make_unique>>(); + auto future = promise->get_future(); + kitchen_sink_kitchen_async_samples(n, cancel_token.handle(), [](void* context, kitchen_sink_error* err, const float* result_ptr, size_t result_len) { + detail::settle>(context, err, [&] { return detail::take_slice(result_ptr, result_len); }); + }, promise.release()); + return future; +} + +/** + * Iterator of optional scalars + * + * @return A lazy, single-pass range; each step makes one producer call. + */ +inline Range> stream_maybe() { + check_library(); + kitchen_sink_error err{}; + kitchen_sink_kitchen_StreamMaybeIterator* iter = kitchen_sink_kitchen_stream_maybe(&err); + detail::check(err); + return Range>(adopt, iter, [](void* raw, std::optional>& item) { + kitchen_sink_error next_err{}; + bool out_has_item{}; + int32_t out_item{}; + if (kitchen_sink_kitchen_StreamMaybeIterator_next(static_cast(raw), &out_has_item, &out_item, &next_err) == 0) { + detail::check(next_err); + return false; + } + item.emplace(detail::lift_optional(out_has_item, out_item)); + return true; + }, [](void* raw) { kitchen_sink_kitchen_StreamMaybeIterator_destroy(static_cast(raw)); }); +} + +/** + * Iterator of typed arrays + * + * @return A lazy, single-pass range; each step makes one producer call. + */ +inline Range> stream_chunks() { + check_library(); + kitchen_sink_error err{}; + kitchen_sink_kitchen_StreamChunksIterator* iter = kitchen_sink_kitchen_stream_chunks(&err); + detail::check(err); + return Range>(adopt, iter, [](void* raw, std::optional>& item) { + kitchen_sink_error next_err{}; + int32_t* out_item{}; + size_t out_len{}; + if (kitchen_sink_kitchen_StreamChunksIterator_next(static_cast(raw), &out_item, &out_len, &next_err) == 0) { + detail::check(next_err); + return false; + } + item.emplace(detail::take_slice(out_item, out_len)); + return true; + }, [](void* raw) { kitchen_sink_kitchen_StreamChunksIterator_destroy(static_cast(raw)); }); +} + +/** + * Drive a consumer-implemented sampler + * + * @throws PantryError (or one of its codes' classes) when the call fails with one of the domain's codes. + */ +inline std::vector sample_with(std::shared_ptr sampler, std::optional level) { + check_library(); + auto sampler_ctx = detail::lend_required(std::move(sampler), "sampler"); + kitchen_sink_error err{}; + size_t out_len{}; + auto result = kitchen_sink_kitchen_sample_with(sampler_ctx.release(), &detail::Callbacks::vtable(), level.has_value(), detail::value_or_zero(level), &out_len, &err); + detail::check(err); + return detail::take_slice(result, out_len); +} + +/** + * Fails with the second domain + * + * @throws PantryError (or one of its codes' classes) when the call fails with one of the domain's codes. + */ +inline int32_t take_from_pantry(std::string_view item) { + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_take_from_pantry(detail::utf8(item), item.size(), &err); + detail::check(err); + return result; +} + +/** + * Fails with an untyped error + * + * @throws Error with code -1 and the producer's message when the call fails. + */ +inline int64_t parse_any(std::string_view text) { + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_parse_any(detail::utf8(text), text.size(), &err); + detail::check(err); return result; } @@ -1530,10 +1992,10 @@ namespace kitchen::nested { /** Trivial nested-module function */ inline std::string hello() { check_library(); - size_t out_len = 0; kitchen_sink_error err{}; + size_t out_len{}; auto result = kitchen_sink_kitchen_nested_hello(&out_len, &err); - detail::check(err); + detail::check(err); return detail::take_string(result, out_len); } diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_hpp.snap deleted file mode 100644 index 11f61ed0..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_hpp.snap +++ /dev/null @@ -1,732 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#pragma once - -#include "nested_modules.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace nested_modules { - -/** - * An error a call that declares errors reports. - * - * A positive code is a declared code of the module's error domain, thrown as - * that domain's typed subclass. A negative code is a runtime failure: -1 a - * generic producer error, -2 a producer panic, -3 a marshalling failure, and - * -4 a callback-interface implementation that failed. A cancelled async call - * (-5) throws Cancelled. - */ -class Error : public std::runtime_error { - int32_t code_; - -public: - /** Builds an error carrying `code` and `message`. */ - Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} - - /** The numeric error code. */ - int32_t code() const noexcept { return code_; } -}; - -/** Thrown by an async call that was cancelled through its CancelToken (code -5). */ -class Cancelled : public Error { -public: - /** Builds a cancellation error carrying the producer's message. */ - explicit Cancelled(const std::string& message) : Error(-5, message) {} -}; - -/** - * Thrown by a call that declares no errors when it fails anyway: a producer - * panic (-2), a marshalling failure (-3), a callback-interface failure the - * producer let through (-4), or another runtime failure (-1). These are bugs, - * so no signature declares them; `what()` names the code and the producer's - * message. - */ -class InternalError : public std::runtime_error { - int32_t code_; - std::string message_; - -public: - /** Builds an internal error carrying `code` and the producer's `message`. */ - InternalError(int32_t code, const std::string& message) - : std::runtime_error("nested_modules: runtime error " + std::to_string(code) + ": " + message), - code_(code), - message_(message) {} - - /** The runtime error code (negative). */ - int32_t code() const noexcept { return code_; } - - /** The producer's message. */ - const std::string& message() const noexcept { return message_; } -}; - -/** - * Thrown on first use when the linked nested_modules library implements a - * different C ABI revision than this header, or lacks or changed a - * declaration this header was generated with. - */ -class LoadError : public std::runtime_error { -public: - /** Builds a load error describing the mismatch. */ - explicit LoadError(const std::string& message) : std::runtime_error(message) {} -}; - -/** - * Tag selecting the constructor that adopts a raw C pointer (one strong - * reference the wrapper then owns), in the style of std::adopt_lock. - */ -struct adopt_t { - explicit adopt_t() = default; -}; - -/** The adopt_t tag value: `Store s(adopt, raw)`. */ -inline constexpr adopt_t adopt{}; - -/** - * A cancellation token for cancellable async functions. - * - * Create one, pass it to any number of cancellable calls, and call cancel() - * from any thread. A cancelled call settles its future with Cancelled. Each - * call takes its own reference to the native token, so the token can be - * destroyed at any time; the destructor releases this wrapper's reference. - */ -class CancelToken { - nested_modules_cancel_token* handle_; - - struct none_t {}; - explicit CancelToken(none_t) noexcept : handle_(nullptr) {} - -public: - /** Creates a new, uncancelled token. */ - CancelToken() : handle_(nested_modules_cancel_token_create()) { - if (handle_ == nullptr) throw std::bad_alloc(); - } - - /** Releases this wrapper's reference to the native token. */ - ~CancelToken() { - if (handle_ != nullptr) nested_modules_cancel_token_destroy(handle_); - } - - CancelToken(const CancelToken&) = delete; - CancelToken& operator=(const CancelToken&) = delete; - - /** Transfers the native token; `other` becomes empty. */ - CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current token and takes over `other`'s. */ - CancelToken& operator=(CancelToken&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) nested_modules_cancel_token_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** The empty token a cancellable call uses when none is passed. */ - static const CancelToken& none() noexcept { - static const CancelToken token{none_t{}}; - return token; - } - - /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ - void cancel() const noexcept { - if (handle_ != nullptr) nested_modules_cancel_token_cancel(handle_); - } - - /** Whether cancel() has been called. */ - bool is_cancelled() const noexcept { - return handle_ != nullptr && nested_modules_cancel_token_is_cancelled(handle_); - } - - /** The native token, borrowed (null for an empty token). */ - nested_modules_cancel_token* handle() const noexcept { return handle_; } -}; - -namespace detail { - -/** The exception for a failed call that declares no errors: Cancelled for -5, else InternalError. */ -inline std::exception_ptr make_internal_error(int32_t code, const std::string& message) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - return std::make_exception_ptr(InternalError(code, message)); -} - -/** The message of a nonzero error slot. */ -inline std::string error_message(const nested_modules_error& err) { - return std::string(err.message != nullptr ? err.message : "unknown error"); -} - -/** Throws InternalError if `err` carries a nonzero code (a call that declares no errors). */ -inline void check(nested_modules_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_internal_error(err.code, error_message(err)); - nested_modules_error_clear(&err); - std::rethrow_exception(ex); -} - -/** Copies a producer-returned UTF-8 run into a string, then releases it. */ -inline std::string take_string(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::string(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { nested_modules_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::string(reinterpret_cast(ptr), len); -} - -/** Copies a producer-returned byte run into a vector, then releases it. */ -inline std::vector take_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::vector(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { nested_modules_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::vector(ptr, ptr + len); -} - -/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ -inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { - if (ptr == nullptr || len == 0) return std::string_view(); - return std::string_view(reinterpret_cast(ptr), len); -} - -/** Copies a borrowed byte run (null when empty) into a vector. */ -inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr || len == 0) return std::vector(); - return std::vector(ptr, ptr + len); -} - -/** The data pointer of a string argument, as the C ABI expects it. */ -inline const uint8_t* utf8(std::string_view s) noexcept { - return reinterpret_cast(s.data()); -} - -/** - * Hands `len` bytes from `data` to the producer through a callback method's - * out slots: copies them into a run from nested_modules_alloc, which the - * producer adopts and frees. - */ -inline void hand_over(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = nullptr; - if (len > 0) { - run = nested_modules_alloc(len); - if (run == nullptr) throw std::bad_alloc(); - std::memcpy(run, data, len); - } - *out_ptr = run; - *out_len = len; -} - -/** One declaration this header was generated with: its contract id and hash, and its dotted path. */ -struct ContractEntry { - /** FNV-1a 64 of the declaration's dotted path. */ - uint64_t id; - /** FNV-1a 64 of the declaration's canonical signature. */ - uint64_t hash; - /** The declaration's dotted path (`kv.Store.put`). */ - const char* path; -}; - -/** - * Why the contract table `contract` returns doesn't satisfy `expected`, - * naming the first declaration it lacks or declares differently, or an empty - * string when it has them all. Entries the producer has and `expected` - * doesn't are fine. - */ -template -std::string contract_mismatch(const nested_modules_contract_entry* (*contract)(size_t*), - const ContractEntry (&expected)[N]) { - size_t len = 0; - const nested_modules_contract_entry* table = contract(&len); - if (table == nullptr) len = 0; - for (const ContractEntry& want : expected) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < want.id) { - lo = mid + 1; - } else { - hi = mid; - } - } - if (lo == len || table[lo].id != want.id) { - return std::string(want.path) + " is missing from the library"; - } - if (table[lo].hash != want.hash) { - return std::string(want.path) + " changed since these bindings were generated"; - } - } - return std::string(); -} - -/** The declarations of module `graphics` this header was generated with. */ -inline constexpr ContractEntry graphics_contract[] = { - {0x2c29eef9327a6bd4ull, 0x12983d5271651389ull, "graphics.Unit"}, - {0x304088bbc28f0015ull, 0x97e6e809913be6b6ull, "graphics.new_canvas"}, - {0x49f8837532df9ac6ull, 0x32e5b27e81ee0957ull, "graphics.Canvas"}, - {0x55717fa027336b47ull, 0xb2bd3a6bdcce48c8ull, "graphics.shapes.describe"}, - {0x7626b91b8f1f53cdull, 0x867bbf7cf0f41c74ull, "graphics.shapes.geometry.compute"}, - {0xb3efcb50d9a61ab7ull, 0x3cb0207dfad2a149ull, "graphics.GraphicsError"}, - {0xc62b16a9591b744bull, 0x7895610c8487eb1bull, "graphics.shapes.Shape"}, - {0xe13001f4983ccd35ull, 0x16e7de070b4a4cfcull, "graphics.shapes.geometry.Detail"}, - {0xf3f658bcfc5dba62ull, 0x2665ccb47356e746ull, "graphics.shapes.make_shape"}, -}; - -} // namespace detail - -/** - * Verifies that the linked nested_modules library implements C ABI revision - * 4 and every declaration this header was generated with, throwing - * LoadError naming the first mismatch. The check runs once; every free - * function, constructor, and static member calls this before its first - * native call, so calling it at startup is optional. - */ -inline void check_library() { - static const std::string failure = []() -> std::string { - uint32_t abi = nested_modules_abi_version(); - if (abi != NESTED_MODULES_ABI_VERSION) { - return "nested_modules: C ABI revision mismatch (header " + std::to_string(NESTED_MODULES_ABI_VERSION) + - ", library " + std::to_string(abi) + ")"; - } - if (std::string why = detail::contract_mismatch(nested_modules_graphics_contract, detail::graphics_contract); !why.empty()) { - return "nested_modules: " + why; - } - return std::string(); - }(); - if (!failure.empty()) throw LoadError(failure); -} - -namespace detail { - -/** - * Serializes values into the value-buffer wire format: little-endian, packed - * with no alignment, lengths and element counts as u32. - */ -class BufferWriter { - std::vector buf_; - - template - void append_le(T v) { - for (size_t i = 0; i < sizeof(T); ++i) { - buf_.push_back(static_cast(v >> (8 * i))); - } - } - -public: - /** Pointer to the encoded bytes. */ - const uint8_t* data() const noexcept { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const noexcept { return buf_.size(); } - - void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } - void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } - void write_u8(uint8_t v) { buf_.push_back(v); } - void write_i16(int16_t v) { append_le(static_cast(v)); } - void write_u16(uint16_t v) { append_le(v); } - void write_i32(int32_t v) { append_le(static_cast(v)); } - void write_u32(uint32_t v) { append_le(v); } - void write_i64(int64_t v) { append_le(static_cast(v)); } - void write_u64(uint64_t v) { append_le(v); } - - void write_f32(float v) { - uint32_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - void write_f64(double v) { - uint64_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - /** Writes a byte length or an element count as a u32. */ - void write_len(size_t n) { - if (static_cast(n) > UINT32_MAX) throw std::length_error("value buffer length exceeds u32"); - append_le(static_cast(n)); - } - - void write_string(std::string_view v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - void write_bytes(const std::vector& v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - /** Writes an optional's presence flag: 0 absent, 1 present. */ - void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } - - /** - * Writes an object token: a fresh strong reference from the wrapper's - * `_clone`, which the reader adopts while `v` keeps its own. - */ - template - void write_object(const T& v) { - write_u64(static_cast(reinterpret_cast(v.clone_handle()))); - } -}; - -/** - * Decodes values from the value-buffer wire format. A malformed buffer is a - * producer bug (both sides are generated from one API), so every decode - * failure throws InternalError with the marshalling code -3. Object tokens - * adopted before a failure live in RAII wrappers that release them on - * unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - void require(size_t n, const char* what) const { - if (len_ - pos_ < n) fail(what); - } - - template - T read_le(const char* what) { - require(sizeof(T), what); - uint64_t v = 0; - for (size_t i = 0; i < sizeof(T); ++i) { - v |= static_cast(data_[pos_ + i]) << (8 * i); - } - pos_ += sizeof(T); - return static_cast(v); - } - -public: - /** Throws the marshalling failure (-3) for a malformed buffer. */ - [[noreturn]] static void fail(const char* what) { - throw InternalError(-3, std::string("malformed value buffer: ") + what); - } - - /** Reads from the `len` bytes at `data` (null when empty). */ - BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const noexcept { return len_ - pos_; } - - bool read_bool() { - uint8_t b = read_le("bool"); - if (b > 1) fail("bool byte out of range"); - return b != 0; - } - - int8_t read_i8() { return read_le("i8"); } - uint8_t read_u8() { return read_le("u8"); } - int16_t read_i16() { return read_le("i16"); } - uint16_t read_u16() { return read_le("u16"); } - int32_t read_i32() { return read_le("i32"); } - uint32_t read_u32() { return read_le("u32"); } - int64_t read_i64() { return read_le("i64"); } - uint64_t read_u64() { return read_le("u64"); } - - float read_f32() { - uint32_t bits = read_le("f32"); - float v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - double read_f64() { - uint64_t bits = read_le("f64"); - double v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - /** Reads a byte length, rejecting one larger than the bytes remaining. */ - size_t read_len() { - uint32_t n = read_le("length"); - if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); - return static_cast(n); - } - - /** - * Reads an element count. Elements may encode to zero bytes, so a count - * larger than the bytes remaining is legal; callers cap preallocation - * with reserve_hint(). - */ - size_t read_count() { return static_cast(read_le("count")); } - - /** A preallocation size for `n` elements that a malformed count can't inflate. */ - size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } - - std::string read_string() { - size_t n = read_len(); - std::string v(reinterpret_cast(data_) + pos_, n); - pos_ += n; - return v; - } - - std::vector read_bytes() { - size_t n = read_len(); - std::vector v(data_ + pos_, data_ + pos_ + n); - pos_ += n; - return v; - } - - bool read_option_flag() { - uint8_t b = read_le("option flag"); - if (b > 1) fail("option flag byte out of range"); - return b != 0; - } - - /** Reads an object token, adopting the strong reference it carries into a `T` wrapper. */ - template - T read_object() { - auto* raw = reinterpret_cast(static_cast(read_u64())); - if (raw == nullptr) fail("null object token"); - return T(adopt, raw); - } - - /** Rejects unconsumed bytes after decoding a complete value. */ - void expect_end() const { - if (pos_ != len_) fail("trailing bytes after value"); - } -}; - -/** Releases a producer-allocated buffer with nested_modules_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; - - BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} - BufferGuard(const BufferGuard&) = delete; - BufferGuard& operator=(const BufferGuard&) = delete; - - ~BufferGuard() { - if (ptr != nullptr) nested_modules_free_bytes(const_cast(ptr), len); - } -}; - -/** Decodes one complete `T` from a borrowed buffer with `read`, rejecting trailing bytes. */ -template -T decode(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferReader r(ptr, len); - T value = read(r); - r.expect_end(); - return value; -} - -/** Decodes one complete `T` from a producer-returned buffer with `read`, then releases the buffer. */ -template -T take(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferGuard guard(ptr, len); - return decode(ptr, len, read); -} - -} // namespace detail - -/** A measurement unit shared with nested submodules. */ -enum class Unit : int32_t { - pixels = 0, - points = 1, -}; - -struct Canvas; -struct Shape; -struct Detail; - -/** A struct two levels deep. */ -struct Detail { - double area; -}; - -/** A struct one level deep that references both an ancestor enum and a deeper struct. */ -struct Shape { - int32_t sides; - /** Reference up to the parent module's enum */ - Unit unit; - /** Reference down into a deeper module */ - Detail detail; -}; - -/** Top-level struct that references a struct defined one level deeper. */ -struct Canvas { - int32_t width; - /** Reference down into a nested module */ - Shape root; -}; - -namespace detail { - -inline void write_Canvas(BufferWriter& w, const Canvas& v); -inline Canvas read_Canvas(BufferReader& r); -inline void write_Shape(BufferWriter& w, const Shape& v); -inline Shape read_Shape(BufferReader& r); -inline void write_Detail(BufferWriter& w, const Detail& v); -inline Detail read_Detail(BufferReader& r); - -/** Writes one `Canvas` to a value buffer. */ -inline void write_Canvas(BufferWriter& w, const Canvas& v) { - w.write_i32(v.width); - detail::write_Shape(w, v.root); -} - -/** Reads one `Canvas` from a value buffer. */ -inline Canvas read_Canvas(BufferReader& r) { - return Canvas{ - r.read_i32(), - detail::read_Shape(r), - }; -} - -/** Writes one `Shape` to a value buffer. */ -inline void write_Shape(BufferWriter& w, const Shape& v) { - w.write_i32(v.sides); - w.write_i32(static_cast(v.unit)); - detail::write_Detail(w, v.detail); -} - -/** Reads one `Shape` from a value buffer. */ -inline Shape read_Shape(BufferReader& r) { - return Shape{ - r.read_i32(), - static_cast(r.read_i32()), - detail::read_Detail(r), - }; -} - -/** Writes one `Detail` to a value buffer. */ -inline void write_Detail(BufferWriter& w, const Detail& v) { - w.write_f64(v.area); -} - -/** Reads one `Detail` from a value buffer. */ -inline Detail read_Detail(BufferReader& r) { - return Detail{ - r.read_f64(), - }; -} - -} // namespace detail - -/** The errors the `graphics` module's throwing calls report; catch a code's subclass or this. */ -class GraphicsError : public Error { -public: - /** Builds an error carrying a domain `code` and `message`. */ - GraphicsError(int32_t code, const std::string& message) : Error(code, message) {} -}; - -/** - * coordinate out of bounds - * - * Code 1. - */ -class OutOfBoundsError : public GraphicsError { -public: - /** Builds the error with the producer's `message` and the code's fields. */ - explicit OutOfBoundsError(const std::string& message) : GraphicsError(1, message) {} -}; - -namespace detail { - -/** The exception for a failed `GraphicsError` call: its code's subclass, Cancelled, or the root Error. */ -inline std::exception_ptr make_graphics_error(int32_t code, const std::string& message, const uint8_t* payload_ptr, size_t payload_len) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - if (code < 0) return std::make_exception_ptr(Error(code, message)); - (void)payload_ptr; - (void)payload_len; - switch (code) { - case 1: return std::make_exception_ptr(OutOfBoundsError(message)); - default: return std::make_exception_ptr(GraphicsError(code, message)); - } -} - -/** Throws the typed `GraphicsError` exception if `err` carries a nonzero code. */ -inline void check_graphics(nested_modules_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_graphics_error(err.code, error_message(err), err.payload_ptr, err.payload_len); - nested_modules_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -namespace graphics { - -/** Construct a Canvas. */ -inline Canvas new_canvas(int32_t width) { - check_library(); - size_t out_len = 0; - nested_modules_error err{}; - auto result = nested_modules_graphics_new_canvas(width, &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Canvas); -} - -} // namespace graphics - -namespace graphics::shapes { - -/** Construct a Shape. */ -inline Shape make_shape(int32_t sides) { - check_library(); - size_t out_len = 0; - nested_modules_error err{}; - auto result = nested_modules_graphics_shapes_make_shape(sides, &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Shape); -} - -/** Nested function returning a struct owned by an ancestor module. */ -inline Canvas describe() { - check_library(); - size_t out_len = 0; - nested_modules_error err{}; - auto result = nested_modules_graphics_shapes_describe(&out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Canvas); -} - -} // namespace graphics::shapes - -namespace graphics::shapes::geometry { - -/** - * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - * - * @throws GraphicsError when the call fails with one of the domain's codes. - */ -inline Detail compute(const Shape& shape) { - check_library(); - detail::BufferWriter shape_buf; - detail::write_Shape(shape_buf, shape); - size_t out_len = 0; - nested_modules_error err{}; - auto result = nested_modules_graphics_shapes_geometry_compute(shape_buf.data(), shape_buf.size(), &out_len, &err); - detail::check_graphics(err); - return detail::take(result, out_len, &detail::read_Detail); -} - -} // namespace graphics::shapes::geometry - -} // namespace nested_modules - -// END nested_modules.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_hpp.snap deleted file mode 100644 index a96adda5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_hpp.snap +++ /dev/null @@ -1,775 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#pragma once - -#include "shapes.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace shapes { - -/** - * An error a call that declares errors reports. - * - * A positive code is a declared code of the module's error domain, thrown as - * that domain's typed subclass. A negative code is a runtime failure: -1 a - * generic producer error, -2 a producer panic, -3 a marshalling failure, and - * -4 a callback-interface implementation that failed. A cancelled async call - * (-5) throws Cancelled. - */ -class Error : public std::runtime_error { - int32_t code_; - -public: - /** Builds an error carrying `code` and `message`. */ - Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} - - /** The numeric error code. */ - int32_t code() const noexcept { return code_; } -}; - -/** Thrown by an async call that was cancelled through its CancelToken (code -5). */ -class Cancelled : public Error { -public: - /** Builds a cancellation error carrying the producer's message. */ - explicit Cancelled(const std::string& message) : Error(-5, message) {} -}; - -/** - * Thrown by a call that declares no errors when it fails anyway: a producer - * panic (-2), a marshalling failure (-3), a callback-interface failure the - * producer let through (-4), or another runtime failure (-1). These are bugs, - * so no signature declares them; `what()` names the code and the producer's - * message. - */ -class InternalError : public std::runtime_error { - int32_t code_; - std::string message_; - -public: - /** Builds an internal error carrying `code` and the producer's `message`. */ - InternalError(int32_t code, const std::string& message) - : std::runtime_error("shapes: runtime error " + std::to_string(code) + ": " + message), - code_(code), - message_(message) {} - - /** The runtime error code (negative). */ - int32_t code() const noexcept { return code_; } - - /** The producer's message. */ - const std::string& message() const noexcept { return message_; } -}; - -/** - * Thrown on first use when the linked shapes library implements a - * different C ABI revision than this header, or lacks or changed a - * declaration this header was generated with. - */ -class LoadError : public std::runtime_error { -public: - /** Builds a load error describing the mismatch. */ - explicit LoadError(const std::string& message) : std::runtime_error(message) {} -}; - -/** - * Tag selecting the constructor that adopts a raw C pointer (one strong - * reference the wrapper then owns), in the style of std::adopt_lock. - */ -struct adopt_t { - explicit adopt_t() = default; -}; - -/** The adopt_t tag value: `Store s(adopt, raw)`. */ -inline constexpr adopt_t adopt{}; - -/** - * A cancellation token for cancellable async functions. - * - * Create one, pass it to any number of cancellable calls, and call cancel() - * from any thread. A cancelled call settles its future with Cancelled. Each - * call takes its own reference to the native token, so the token can be - * destroyed at any time; the destructor releases this wrapper's reference. - */ -class CancelToken { - shapes_cancel_token* handle_; - - struct none_t {}; - explicit CancelToken(none_t) noexcept : handle_(nullptr) {} - -public: - /** Creates a new, uncancelled token. */ - CancelToken() : handle_(shapes_cancel_token_create()) { - if (handle_ == nullptr) throw std::bad_alloc(); - } - - /** Releases this wrapper's reference to the native token. */ - ~CancelToken() { - if (handle_ != nullptr) shapes_cancel_token_destroy(handle_); - } - - CancelToken(const CancelToken&) = delete; - CancelToken& operator=(const CancelToken&) = delete; - - /** Transfers the native token; `other` becomes empty. */ - CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } - - /** Releases the current token and takes over `other`'s. */ - CancelToken& operator=(CancelToken&& other) noexcept { - if (this != &other) { - if (handle_ != nullptr) shapes_cancel_token_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** The empty token a cancellable call uses when none is passed. */ - static const CancelToken& none() noexcept { - static const CancelToken token{none_t{}}; - return token; - } - - /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ - void cancel() const noexcept { - if (handle_ != nullptr) shapes_cancel_token_cancel(handle_); - } - - /** Whether cancel() has been called. */ - bool is_cancelled() const noexcept { - return handle_ != nullptr && shapes_cancel_token_is_cancelled(handle_); - } - - /** The native token, borrowed (null for an empty token). */ - shapes_cancel_token* handle() const noexcept { return handle_; } -}; - -namespace detail { - -/** The exception for a failed call that declares no errors: Cancelled for -5, else InternalError. */ -inline std::exception_ptr make_internal_error(int32_t code, const std::string& message) { - if (code == -5) return std::make_exception_ptr(Cancelled(message)); - return std::make_exception_ptr(InternalError(code, message)); -} - -/** The message of a nonzero error slot. */ -inline std::string error_message(const shapes_error& err) { - return std::string(err.message != nullptr ? err.message : "unknown error"); -} - -/** Throws InternalError if `err` carries a nonzero code (a call that declares no errors). */ -inline void check(shapes_error& err) { - if (err.code == 0) return; - std::exception_ptr ex = make_internal_error(err.code, error_message(err)); - shapes_error_clear(&err); - std::rethrow_exception(ex); -} - -/** Copies a producer-returned UTF-8 run into a string, then releases it. */ -inline std::string take_string(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::string(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { shapes_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::string(reinterpret_cast(ptr), len); -} - -/** Copies a producer-returned byte run into a vector, then releases it. */ -inline std::vector take_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr) return std::vector(); - struct Release { - const uint8_t* ptr; - size_t len; - ~Release() { shapes_free_bytes(const_cast(ptr), len); } - } release{ptr, len}; - return std::vector(ptr, ptr + len); -} - -/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ -inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { - if (ptr == nullptr || len == 0) return std::string_view(); - return std::string_view(reinterpret_cast(ptr), len); -} - -/** Copies a borrowed byte run (null when empty) into a vector. */ -inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { - if (ptr == nullptr || len == 0) return std::vector(); - return std::vector(ptr, ptr + len); -} - -/** The data pointer of a string argument, as the C ABI expects it. */ -inline const uint8_t* utf8(std::string_view s) noexcept { - return reinterpret_cast(s.data()); -} - -/** - * Hands `len` bytes from `data` to the producer through a callback method's - * out slots: copies them into a run from shapes_alloc, which the - * producer adopts and frees. - */ -inline void hand_over(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = nullptr; - if (len > 0) { - run = shapes_alloc(len); - if (run == nullptr) throw std::bad_alloc(); - std::memcpy(run, data, len); - } - *out_ptr = run; - *out_len = len; -} - -/** One declaration this header was generated with: its contract id and hash, and its dotted path. */ -struct ContractEntry { - /** FNV-1a 64 of the declaration's dotted path. */ - uint64_t id; - /** FNV-1a 64 of the declaration's canonical signature. */ - uint64_t hash; - /** The declaration's dotted path (`kv.Store.put`). */ - const char* path; -}; - -/** - * Why the contract table `contract` returns doesn't satisfy `expected`, - * naming the first declaration it lacks or declares differently, or an empty - * string when it has them all. Entries the producer has and `expected` - * doesn't are fine. - */ -template -std::string contract_mismatch(const shapes_contract_entry* (*contract)(size_t*), - const ContractEntry (&expected)[N]) { - size_t len = 0; - const shapes_contract_entry* table = contract(&len); - if (table == nullptr) len = 0; - for (const ContractEntry& want : expected) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < want.id) { - lo = mid + 1; - } else { - hi = mid; - } - } - if (lo == len || table[lo].id != want.id) { - return std::string(want.path) + " is missing from the library"; - } - if (table[lo].hash != want.hash) { - return std::string(want.path) + " changed since these bindings were generated"; - } - } - return std::string(); -} - -/** The declarations of module `shapes` this header was generated with. */ -inline constexpr ContractEntry shapes_contract[] = { - {0x0d7b3eec49243e7aull, 0x5f232311808e1b1full, "shapes.Channel"}, - {0x183fa8198100860eull, 0xe40c6993cb31d359ull, "shapes.describe"}, - {0x2d1395eb837b848eull, 0x42202a0c12f3a16eull, "shapes.sum_bytes"}, - {0x7f9a5b87ba09b86bull, 0xc5b32fb0efbbaa17ull, "shapes.scale"}, - {0xc8edd779240fda17ull, 0x68e1be78fddb20e4ull, "shapes.Numerics"}, - {0xd3d3881b2fd18a62ull, 0xf24d0eb76a99d918ull, "shapes.make_numerics"}, - {0xf010844afff5ac3cull, 0x50de28ae9b41ff15ull, "shapes.Shape"}, -}; - -} // namespace detail - -/** - * Verifies that the linked shapes library implements C ABI revision - * 4 and every declaration this header was generated with, throwing - * LoadError naming the first mismatch. The check runs once; every free - * function, constructor, and static member calls this before its first - * native call, so calling it at startup is optional. - */ -inline void check_library() { - static const std::string failure = []() -> std::string { - uint32_t abi = shapes_abi_version(); - if (abi != SHAPES_ABI_VERSION) { - return "shapes: C ABI revision mismatch (header " + std::to_string(SHAPES_ABI_VERSION) + - ", library " + std::to_string(abi) + ")"; - } - if (std::string why = detail::contract_mismatch(shapes_shapes_contract, detail::shapes_contract); !why.empty()) { - return "shapes: " + why; - } - return std::string(); - }(); - if (!failure.empty()) throw LoadError(failure); -} - -namespace detail { - -/** - * Serializes values into the value-buffer wire format: little-endian, packed - * with no alignment, lengths and element counts as u32. - */ -class BufferWriter { - std::vector buf_; - - template - void append_le(T v) { - for (size_t i = 0; i < sizeof(T); ++i) { - buf_.push_back(static_cast(v >> (8 * i))); - } - } - -public: - /** Pointer to the encoded bytes. */ - const uint8_t* data() const noexcept { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const noexcept { return buf_.size(); } - - void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } - void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } - void write_u8(uint8_t v) { buf_.push_back(v); } - void write_i16(int16_t v) { append_le(static_cast(v)); } - void write_u16(uint16_t v) { append_le(v); } - void write_i32(int32_t v) { append_le(static_cast(v)); } - void write_u32(uint32_t v) { append_le(v); } - void write_i64(int64_t v) { append_le(static_cast(v)); } - void write_u64(uint64_t v) { append_le(v); } - - void write_f32(float v) { - uint32_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - void write_f64(double v) { - uint64_t bits = 0; - std::memcpy(&bits, &v, sizeof(bits)); - append_le(bits); - } - - /** Writes a byte length or an element count as a u32. */ - void write_len(size_t n) { - if (static_cast(n) > UINT32_MAX) throw std::length_error("value buffer length exceeds u32"); - append_le(static_cast(n)); - } - - void write_string(std::string_view v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - void write_bytes(const std::vector& v) { - write_len(v.size()); - buf_.insert(buf_.end(), v.begin(), v.end()); - } - - /** Writes an optional's presence flag: 0 absent, 1 present. */ - void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } - - /** - * Writes an object token: a fresh strong reference from the wrapper's - * `_clone`, which the reader adopts while `v` keeps its own. - */ - template - void write_object(const T& v) { - write_u64(static_cast(reinterpret_cast(v.clone_handle()))); - } -}; - -/** - * Decodes values from the value-buffer wire format. A malformed buffer is a - * producer bug (both sides are generated from one API), so every decode - * failure throws InternalError with the marshalling code -3. Object tokens - * adopted before a failure live in RAII wrappers that release them on - * unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - void require(size_t n, const char* what) const { - if (len_ - pos_ < n) fail(what); - } - - template - T read_le(const char* what) { - require(sizeof(T), what); - uint64_t v = 0; - for (size_t i = 0; i < sizeof(T); ++i) { - v |= static_cast(data_[pos_ + i]) << (8 * i); - } - pos_ += sizeof(T); - return static_cast(v); - } - -public: - /** Throws the marshalling failure (-3) for a malformed buffer. */ - [[noreturn]] static void fail(const char* what) { - throw InternalError(-3, std::string("malformed value buffer: ") + what); - } - - /** Reads from the `len` bytes at `data` (null when empty). */ - BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const noexcept { return len_ - pos_; } - - bool read_bool() { - uint8_t b = read_le("bool"); - if (b > 1) fail("bool byte out of range"); - return b != 0; - } - - int8_t read_i8() { return read_le("i8"); } - uint8_t read_u8() { return read_le("u8"); } - int16_t read_i16() { return read_le("i16"); } - uint16_t read_u16() { return read_le("u16"); } - int32_t read_i32() { return read_le("i32"); } - uint32_t read_u32() { return read_le("u32"); } - int64_t read_i64() { return read_le("i64"); } - uint64_t read_u64() { return read_le("u64"); } - - float read_f32() { - uint32_t bits = read_le("f32"); - float v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - double read_f64() { - uint64_t bits = read_le("f64"); - double v = 0; - std::memcpy(&v, &bits, sizeof(v)); - return v; - } - - /** Reads a byte length, rejecting one larger than the bytes remaining. */ - size_t read_len() { - uint32_t n = read_le("length"); - if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); - return static_cast(n); - } - - /** - * Reads an element count. Elements may encode to zero bytes, so a count - * larger than the bytes remaining is legal; callers cap preallocation - * with reserve_hint(). - */ - size_t read_count() { return static_cast(read_le("count")); } - - /** A preallocation size for `n` elements that a malformed count can't inflate. */ - size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } - - std::string read_string() { - size_t n = read_len(); - std::string v(reinterpret_cast(data_) + pos_, n); - pos_ += n; - return v; - } - - std::vector read_bytes() { - size_t n = read_len(); - std::vector v(data_ + pos_, data_ + pos_ + n); - pos_ += n; - return v; - } - - bool read_option_flag() { - uint8_t b = read_le("option flag"); - if (b > 1) fail("option flag byte out of range"); - return b != 0; - } - - /** Reads an object token, adopting the strong reference it carries into a `T` wrapper. */ - template - T read_object() { - auto* raw = reinterpret_cast(static_cast(read_u64())); - if (raw == nullptr) fail("null object token"); - return T(adopt, raw); - } - - /** Rejects unconsumed bytes after decoding a complete value. */ - void expect_end() const { - if (pos_ != len_) fail("trailing bytes after value"); - } -}; - -/** Releases a producer-allocated buffer with shapes_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; - - BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} - BufferGuard(const BufferGuard&) = delete; - BufferGuard& operator=(const BufferGuard&) = delete; - - ~BufferGuard() { - if (ptr != nullptr) shapes_free_bytes(const_cast(ptr), len); - } -}; - -/** Decodes one complete `T` from a borrowed buffer with `read`, rejecting trailing bytes. */ -template -T decode(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferReader r(ptr, len); - T value = read(r); - r.expect_end(); - return value; -} - -/** Decodes one complete `T` from a producer-returned buffer with `read`, then releases the buffer. */ -template -T take(const uint8_t* ptr, size_t len, T (*read)(BufferReader&)) { - BufferGuard guard(ptr, len); - return decode(ptr, len, read); -} - -} // namespace detail - -/** A plain C-style enum (no payloads) */ -enum class Channel : int32_t { - Red = 0, - Green = 1, - Blue = 2, -}; - -struct Numerics; -struct Shape; - -/** One field per supported numeric primitive */ -struct Numerics { - int8_t a; - int16_t b; - int32_t c; - int64_t d; - uint8_t e; - uint16_t f; - uint32_t g; - uint64_t h; - float i; - double j; -}; - -/** An algebraic shape (sum type with associated data) */ -struct Shape { - /** Discriminant identifying the active variant of `Shape`. */ - enum class Tag : int32_t { - Empty = 0, - Circle = 1, - Rectangle = 2, - Bounded = 3, - Labeled = 4, - }; - - /** The empty shape (a unit variant) */ - struct Empty { - }; - - /** A circle with a radius */ - struct Circle { - /** Radius in points */ - double radius; - }; - - /** An axis-aligned rectangle */ - struct Rectangle { - float width; - float height; - }; - - /** An integer-bounded shape (exercises narrow + wide ints) */ - struct Bounded { - int16_t min; - int16_t max; - uint64_t weight; - }; - - /** A labeled shape with a small count */ - struct Labeled { - std::string label; - uint8_t count; - }; - - /** The active variant's payload. */ - std::variant value; - - /** The tag of the active variant. */ - Tag tag() const noexcept { - static constexpr Tag tags[] = {Tag::Empty, Tag::Circle, Tag::Rectangle, Tag::Bounded, Tag::Labeled}; - return tags[value.index()]; - } -}; - -namespace detail { - -inline void write_Numerics(BufferWriter& w, const Numerics& v); -inline Numerics read_Numerics(BufferReader& r); -inline void write_Shape(BufferWriter& w, const Shape& v); -inline Shape read_Shape(BufferReader& r); - -/** Writes one `Numerics` to a value buffer. */ -inline void write_Numerics(BufferWriter& w, const Numerics& v) { - w.write_i8(v.a); - w.write_i16(v.b); - w.write_i32(v.c); - w.write_i64(v.d); - w.write_u8(v.e); - w.write_u16(v.f); - w.write_u32(v.g); - w.write_u64(v.h); - w.write_f32(v.i); - w.write_f64(v.j); -} - -/** Reads one `Numerics` from a value buffer. */ -inline Numerics read_Numerics(BufferReader& r) { - return Numerics{ - r.read_i8(), - r.read_i16(), - r.read_i32(), - r.read_i64(), - r.read_u8(), - r.read_u16(), - r.read_u32(), - r.read_u64(), - r.read_f32(), - r.read_f64(), - }; -} - -/** Writes one `Shape` to a value buffer. */ -inline void write_Shape(BufferWriter& w, const Shape& v) { - switch (v.value.index()) { - case 0: { - w.write_i32(0); - break; - } - case 1: { - w.write_i32(1); - const Shape::Circle& p = std::get<1>(v.value); - w.write_f64(p.radius); - break; - } - case 2: { - w.write_i32(2); - const Shape::Rectangle& p = std::get<2>(v.value); - w.write_f32(p.width); - w.write_f32(p.height); - break; - } - case 3: { - w.write_i32(3); - const Shape::Bounded& p = std::get<3>(v.value); - w.write_i16(p.min); - w.write_i16(p.max); - w.write_u64(p.weight); - break; - } - case 4: { - w.write_i32(4); - const Shape::Labeled& p = std::get<4>(v.value); - w.write_string(p.label); - w.write_u8(p.count); - break; - } - } -} - -/** Reads one `Shape` from a value buffer. */ -inline Shape read_Shape(BufferReader& r) { - int32_t tag = r.read_i32(); - switch (tag) { - case 0: return Shape{Shape::Empty{}}; - case 1: - return Shape{Shape::Circle{ - r.read_f64(), - }}; - case 2: - return Shape{Shape::Rectangle{ - r.read_f32(), - r.read_f32(), - }}; - case 3: - return Shape{Shape::Bounded{ - r.read_i16(), - r.read_i16(), - r.read_u64(), - }}; - case 4: - return Shape{Shape::Labeled{ - r.read_string(), - r.read_u8(), - }}; - default: break; - } - BufferReader::fail("unknown Shape tag"); -} - -} // namespace detail - -namespace shapes { - -/** Render a shape to a string */ -inline std::string describe(const Shape& shape) { - check_library(); - detail::BufferWriter shape_buf; - detail::write_Shape(shape_buf, shape); - size_t out_len = 0; - shapes_error err{}; - auto result = shapes_shapes_describe(shape_buf.data(), shape_buf.size(), &out_len, &err); - detail::check(err); - return detail::take_string(result, out_len); -} - -/** Scale a shape by a factor, returning a new shape */ -inline Shape scale(const Shape& shape, double factor) { - check_library(); - detail::BufferWriter shape_buf; - detail::write_Shape(shape_buf, shape); - size_t out_len = 0; - shapes_error err{}; - auto result = shapes_shapes_scale(shape_buf.data(), shape_buf.size(), factor, &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Shape); -} - -/** Sum a list of bytes into a wide integer (numerics smoke) */ -inline uint64_t sum_bytes(const std::vector& values) { - check_library(); - shapes_error err{}; - auto result = shapes_shapes_sum_bytes(values.data(), values.size(), &err); - detail::check(err); - return result; -} - -/** Build a Numerics struct from every numeric primitive */ -inline Numerics make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j) { - check_library(); - size_t out_len = 0; - shapes_error err{}; - auto result = shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &out_len, &err); - detail::check(err); - return detail::take(result, out_len, &detail::read_Numerics); -} - -} // namespace shapes - -} // namespace shapes - -// END shapes.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_docs_everywhere_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_docs_everywhere_dart.snap deleted file mode 100644 index 200bd830..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_docs_everywhere_dart.snap +++ /dev/null @@ -1,1272 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element - -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -// ── Library loading ── -// The native library opens on first use and is verified before any other -// lookup: it must implement the C ABI revision these bindings were generated -// against and carry every declaration they were generated with, unchanged. - -/// The native library every binding in this file calls. -final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); - -/// The native library couldn't be loaded, or it doesn't match these -/// bindings: a different C ABI revision, or a declaration that's missing or -/// changed. Regenerate the bindings from the library you ship. -final class NativeLibraryError extends Error { - /// Creates a load failure described by [message]. - NativeLibraryError(this.message); - - /// What failed, naming the library or declaration. - final String message; - - @override - String toString() => 'NativeLibraryError: $message'; -} - -/// The platform file name of the library on the system search path. -String get _libraryFile => Platform.isMacOS - ? 'libdocs_everywhere.dylib' - : Platform.isWindows - ? 'docs_everywhere.dll' - : 'libdocs_everywhere.so'; - -DynamicLibrary _openLibrary() { - // An explicit path wins, so a caller can point at any build artifact. - final override = Platform.environment['DOCS_EVERYWHERE_LIBRARY']; - if (override != null && override.isNotEmpty) { - return _open(override); - } - // iOS links native code into the app binary. - if (Platform.isIOS) return DynamicLibrary.process(); - try { - return DynamicLibrary.open(_libraryFile); - } on ArgumentError catch (e) { - // A macOS app may link the library into its executable instead. - if (Platform.isMacOS) return DynamicLibrary.process(); - throw NativeLibraryError("can't open '$_libraryFile' ($e); set " - 'DOCS_EVERYWHERE_LIBRARY to its path'); - } -} - -DynamicLibrary _open(String path) { - try { - return DynamicLibrary.open(path); - } on ArgumentError catch (e) { - throw NativeLibraryError("can't open '$path': $e"); - } -} - -/// The C ABI revision these bindings were generated against. -const int _abiVersion = 4; - -/// One `docs_everywhere_contract_entry` of a module's contract table. -final class _ContractEntry extends Struct { - @Uint64() - external int id; - @Uint64() - external int hash; -} - -DynamicLibrary _verifyLibrary(DynamicLibrary lib) { - final int Function() abiVersion; - try { - abiVersion = lib.lookupFunction( - 'docs_everywhere_abi_version'); - } on ArgumentError { - throw NativeLibraryError( - "the native library 'docs_everywhere' isn't loaded; set DOCS_EVERYWHERE_LIBRARY " - 'to its path'); - } - final found = abiVersion(); - if (found != _abiVersion) { - throw NativeLibraryError('docs_everywhere: these bindings expect C ABI revision ' - '$_abiVersion, but the loaded library implements revision $found'); - } - for (final (symbol, expected) in _contracts) { - _checkContract(lib, symbol, expected); - } - return lib; -} - -/// Checks that every `(id, hash, path)` entry in [expected] is in the table -/// [symbol] returns with an equal hash. Entries only the library has are -/// fine: they're declarations added after these bindings were generated. -void _checkContract( - DynamicLibrary lib, String symbol, List<(int, int, String)> expected) { - final Pointer<_ContractEntry> Function(Pointer) table; - try { - table = lib.lookupFunction Function(Pointer), - Pointer<_ContractEntry> Function(Pointer)>(symbol); - } on ArgumentError { - throw NativeLibraryError( - "docs_everywhere: the loaded library doesn't export '$symbol'"); - } - final hashes = {}; - final len = calloc(); - try { - final entries = table(len); - for (var i = 0; i < len.value; i++) { - hashes[entries[i].id] = entries[i].hash; - } - } finally { - calloc.free(len); - } - for (final (id, hash, path) in expected) { - final found = hashes[id]; - if (found == null) { - throw NativeLibraryError('docs_everywhere: $path is missing from the library'); - } - if (found != hash) { - throw NativeLibraryError( - 'docs_everywhere: $path changed since these bindings were generated'); - } - } -} - -/// Each top-level module's contract-table symbol and the declarations -/// these bindings were generated with: (id, signature hash, path). -const List<(String, List<(int, int, String)>)> _contracts = [ - ('docs_everywhere_docs_contract', [ - (0x00b2187fd75634ec, 0x2263c6f787bf6081, 'docs.publish_async'), - (0x12fc242b9dc6444c, 0x96405f484a642b31, 'docs.Notebook.insert'), - (0x2213822ed05cd5bd, 0x26c5833392aa63ce, 'docs.Notebook.new'), - (0x2bd762bcdaa207ff, 0x1b0ebab2d04c49b0, 'docs.SavedListener'), - (0x2e086c45e9d617de, 0x4d43bf32358c01ff, 'docs.Notebook.find'), - (0x4817dc98197ad960, 0xf8017b5a7d58b2c7, 'docs.Notebook.size'), - (0x614de47452d5a55c, 0xd9aae1472494ffef, 'docs.get_document'), - (0x67c5cb9fc119bcd5, 0x08d5b25a1a22a7fa, 'docs.Document'), - (0x8d484bf2d78c9b70, 0xe9093cf96912194c, 'docs.Notebook.max_documents'), - (0xaea61d6451691c67, 0xe4704128b3524b3d, 'docs.watch'), - (0xd68d6ad1540cdec5, 0x01afeb40b0a8bfe1, 'docs.Notebook'), - (0xdb0601444a4760cc, 0xe56c69629b444797, 'docs.create_document'), - (0xe12d6f4838347b3e, 0xa9bab86646d84f51, 'docs.Status'), - (0xee2f8d25b3313b50, 0x9691232f35439a2b, 'docs.DocsErrors'), - ]), -]; - -// ── Errors and memory ── - -/// The C `docs_everywhere_error` slot every fallible call writes. A non-zero -/// `code` carries a producer-owned `message` and, for a domain error with -/// fields, a value-buffer `payload`; `docs_everywhere_error_clear` releases both. -final class _Error extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -final _errorClear = _lib.lookupFunction), - void Function(Pointer<_Error>)>('docs_everywhere_error_clear', isLeaf: true); - -final _freeBytes = _lib.lookupFunction, Size), - void Function(Pointer, int)>('docs_everywhere_free_bytes', isLeaf: true); - -// Synchronous calls on this isolate share one error slot and one length -// slot: each call reads (and clears) them right after it returns, before any -// other call can reuse them, including a nested call from a callback. -final Pointer<_Error> _err = calloc<_Error>(); -final Pointer _outLen = calloc(); - -/// A failure the native library reported to a function declared to throw. -/// Domain errors extend this class with a positive [code]; a negative code is -/// one of the runtime constants below. -class NativeException implements Exception { - /// Creates an exception carrying [code] and [message]. - NativeException(this.code, this.message); - - /// The producer reported an untyped error. - static const int genericCode = -1; - - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - - /// An argument or a callback's return couldn't be lifted by the producer. - static const int marshalCode = -3; - - /// A callback implementation failed; [message] carries its text. - static const int foreignCode = -4; - - /// The call was cancelled (see [CancelledException]). - static const int cancelledCode = -5; - - /// The error code: positive for a domain error, negative for a runtime - /// failure. - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => '$runtimeType($code): $message'; -} - -/// A cancellable call completed because its token was cancelled. -class CancelledException extends NativeException { - /// Creates a cancellation with the producer's [message]. - CancelledException([String message = 'cancelled']) - : super(NativeException.cancelledCode, message); -} - -/// A function that isn't declared to throw failed anyway. That's a bug in -/// the native library (a panic, an argument it couldn't lift, a callback -/// failure it let through), so it's an [Error], not an [Exception]. -final class NativeError extends Error { - /// Creates the error a failed call reported. - NativeError(this.code, this.message); - - /// The runtime code the producer reported (see [NativeException]). - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => 'NativeError($code): $message'; -} - -/// Maps a reported code, message, and payload onto the object to throw. -typedef _ErrorMapper = Object Function( - int code, String message, Uint8List payload); - -/// The mapper of a function that can't fail: every code but cancellation -/// is a [NativeError]. -Object _trap(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeError(code, message); - -/// Maps a runtime code (or one the domain doesn't declare) onto its -/// exception, for a function declared to throw. -NativeException _runtimeException(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeException(code, message); - -/// Builds the error [err] reports, using [map]. -Object _readError(Pointer<_Error> err, _ErrorMapper map) { - final e = err.ref; - final message = e.message == nullptr ? '' : e.message.toDartString(); - return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); -} - -/// Throws the error a synchronous call left in [err], clearing the slot. -/// [map] is the domain's mapper for a function declared to throw. -void _check(Pointer<_Error> err, [_ErrorMapper map = _trap]) { - if (err.ref.code == 0) return; - final error = _readError(err, map); - _errorClear(err); - throw error; -} - -/// Copies a borrowed native byte run into Dart memory. -Uint8List _copyBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -/// Copies an owned native byte run and releases it. -Uint8List _takeBytes(Pointer ptr, int len) { - if (ptr == nullptr) return Uint8List(0); - final bytes = Uint8List.fromList(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return bytes; -} - -/// Decodes an owned UTF-8 run and releases it. -String _takeString(Pointer ptr, int len) { - if (ptr == nullptr) return ''; - final text = utf8.decode(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return text; -} - -/// Decodes a borrowed UTF-8 run. -String _readString(Pointer ptr, int len) => - ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); - -/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. -Pointer _stage(Arena arena, List bytes) { - if (bytes.isEmpty) return nullptr; - final ptr = arena(bytes.length); - ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -// ── Objects ── - -/// The lifetime machinery every interface wrapper shares. A wrapper owns one -/// strong reference to its native object and releases it exactly once: in -/// [dispose], or through a GC finalizer when it's collected undisposed. -/// -/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] -/// that arrives while a call is in flight (from a callback the call made) -/// only marks the wrapper; the reference is released when the last call -/// leaves, so the native object never disappears under a running call. -abstract base class _NativeObject implements Finalizable { - _NativeObject(this._ptr) { - _finalizer.attach(this, _ptr, detach: this); - } - - final Pointer _ptr; - int _calls = 0; - bool _disposed = false; - - NativeFinalizer get _finalizer; - void _destroy(Pointer ptr); - Pointer _clone(Pointer ptr); - - /// Releases this wrapper's native reference. Safe to call more than once; - /// the native object is dropped when its last reference (this one, another - /// wrapper's, or the producer's) goes away. Using the wrapper afterwards - /// throws a [StateError]. - void dispose() { - if (_disposed) return; - _disposed = true; - if (_calls == 0) _release(); - } - - void _release() { - _finalizer.detach(this); - _destroy(_ptr); - } - - Pointer _enter() { - if (_disposed) throw StateError('$runtimeType used after dispose()'); - _calls++; - return _ptr; - } - - void _leave() { - if (--_calls == 0 && _disposed) _release(); - } - - /// A second strong reference, for an object token in a value buffer. - Pointer _cloneRef() { - final ptr = _enter(); - try { - return _clone(ptr); - } finally { - _leave(); - } - } -} - -/// Borrows [object]'s pointer until [arena] is released. -Pointer _borrow(Arena arena, _NativeObject object) { - final ptr = object._enter(); - arena.onReleaseAll(object._leave); - return ptr; -} - -// ── Value buffers ── -// Records, rich enums, optionals, lists, maps, and error payloads cross the -// C ABI serialized in this little-endian, packed format. A malformed buffer -// is a contract violation between producer and bindings, never a domain -// error. - -Never _bufferError(String context) => - throw StateError('malformed value buffer: $context'); - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - late ByteData _data = ByteData.sublistView(_buf); - int _len = 0; - - void _reserve(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - _buf = Uint8List(cap)..setRange(0, _len, _buf); - _data = ByteData.sublistView(_buf); - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeU8(v ? 1 : 0); - - void writeFlag(bool present) => writeU8(present ? 1 : 0); - - void writeI8(int v) { - _reserve(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeU8(int v) { - _reserve(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeI16(int v) { - _reserve(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeU16(int v) { - _reserve(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeI32(int v) { - _reserve(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeU32(int v) { - _reserve(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeI64(int v) { - _reserve(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - // Dart ints are signed 64-bit; a u64 travels as its bit pattern. - void writeU64(int v) => writeI64(v); - - void writeF32(double v) { - _reserve(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeF64(double v) { - _reserve(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeU32(v); - - void writeString(String v) => writeBytes(utf8.encode(v)); - - void writeBytes(List v) { - writeLength(v.length); - _reserve(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } - - void writeOptional(T? v, void Function(T) write) { - writeFlag(v != null); - if (v != null) write(v); - } - - void writeList(List v, void Function(T) write) { - writeLength(v.length); - for (final e in v) { - write(e); - } - } - - void writeMap( - Map v, void Function(K) writeKey, void Function(V) writeValue) { - writeLength(v.length); - for (final e in v.entries) { - writeKey(e.key); - writeValue(e.value); - } - } -} - -final class _BufferReader { - _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); - - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - int _take(int n, String context) { - if (_buf.length - _pos < n) _bufferError('truncated $context'); - final at = _pos; - _pos += n; - return at; - } - - bool readBool() { - final b = _buf[_take(1, 'bool')]; - if (b > 1) _bufferError('bool byte $b'); - return b == 1; - } - - bool readFlag() { - final b = _buf[_take(1, 'option flag')]; - if (b > 1) _bufferError('option flag $b'); - return b == 1; - } - - int readI8() => _data.getInt8(_take(1, 'i8')); - - int readU8() => _buf[_take(1, 'u8')]; - - int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); - - int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); - - int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); - - int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); - - int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); - - // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. - int readU64() => readI64(); - - double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); - - double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); - - int readLength() => readU32(); - - String readString() { - final n = readLength(); - final at = _take(n, 'string'); - return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); - } - - Uint8List readBytes() { - final n = readLength(); - final at = _take(n, 'bytes'); - return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); - } - - T? readOptional(T Function() read) => - readFlag() ? read() : null; - - // Collections grow as they decode, so a corrupt count can't force a huge - // allocation up front. - List readList(T Function() read) => - [for (var i = readLength(); i > 0; i--) read()]; - - Map readMap(K Function() readKey, V Function() readValue) { - final map = {}; - for (var i = readLength(); i > 0; i--) { - final key = readKey(); - if (map.containsKey(key)) _bufferError('duplicate map key $key'); - map[key] = readValue(); - } - return map; - } - - void expectEnd() { - if (_pos != _buf.length) _bufferError('trailing bytes'); - } -} - -/// Encodes one value with [write] into a fresh buffer. -Uint8List _encode(T value, void Function(_BufferWriter, T) write) { - final writer = _BufferWriter(); - write(writer, value); - return writer.takeBytes(); -} - -/// Decodes exactly one value with [read] from [bytes]. -T _decode(Uint8List bytes, T Function(_BufferReader) read) { - final reader = _BufferReader(bytes); - final value = read(reader); - reader.expectEnd(); - return value; -} - -// Records and rich enums compare by value: lists, maps, and byte arrays -// element by element, and everything else with `==` (interface wrappers by -// identity). - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) return true; - if (a is List && b is List) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!_deepEquals(a[i], b[i])) return false; - } - return true; - } - if (a is Map && b is Map) { - if (a.length != b.length) return false; - for (final key in a.keys) { - if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false; - } - return true; - } - return a == b; -} - -int _deepHash(Object? v) => switch (v) { - List() => Object.hashAll(v.map(_deepHash)), - Map() => Object.hashAllUnordered( - v.entries.map((e) => Object.hash(_deepHash(e.key), _deepHash(e.value)))), - _ => v.hashCode, - }; - -// ── Async completion ── -// An async call completes through a `NativeCallable.listener` the producer -// invokes exactly once, from any thread. Everything the completion carries is -// owned by these bindings, so it's decoded later on the event loop and then -// released. - -final _errorFree = - _lib.lookupFunction), void Function(Pointer<_Error>)>( - 'docs_everywhere_error_free'); - -/// Builds the error a completion reports and frees its boxed error. -Object _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { - final error = _readError(err, map); - _errorFree(err); - return error; -} - -// ── Callback interfaces ── -// An implementation passed to the producer is parked in [_callbacks] under -// an integer key. The `ctx` the producer receives points at a small native -// [_CallbackContext] holding that key, so the producer never holds a Dart -// object and the GC never sees a raw pointer. The entry lives until the -// producer calls the vtable's `free`. -// -// Methods that return a value are `NativeCallable.isolateLocal` trampolines: -// they run synchronously, so the producer must call them on this isolate's -// thread while a call from Dart is in progress (the VM aborts the process -// otherwise, and an entry point that may run on any thread can't re-enter -// the isolate to run the method). Void methods and `free` are -// `NativeCallable.isolateGroupBound` forwarders, which may run on any -// thread: they copy their arguments into a message for this isolate -// (borrowed bytes are copied before the forwarder returns) and the -// implementation runs later on the event loop, in the zone that passed the -// callback. - -/// Where forwarders send messages: the allocator they build a message with -/// and the isolate port that receives it. Forwarders run outside the -/// isolate and can't read Dart globals, so everything they need is native. -final class _CallbackPost extends Struct { - external Pointer Function(Size)>> alloc; - external Pointer)>> release; - external Pointer)>> post; - @Int64() - external int port; -} - -/// The native `ctx` of one registered implementation. -final class _CallbackContext extends Struct { - @Int64() - external int key; - external Pointer<_CallbackPost> post; -} - -// The `Dart_CObject` layout `Dart_PostCObject` reads. -final class _CArray extends Struct { - @IntPtr() - external int length; - external Pointer> values; -} - -final class _CTypedData extends Struct { - @Int32() - external int type; - @IntPtr() - external int length; - external Pointer values; -} - -final class _CValue extends Union { - @Bool() - external bool asBool; - @Int64() - external int asInt64; - @Double() - external double asDouble; - external _CArray asArray; - external _CTypedData asTypedData; -} - -final class _CObject extends Struct { - @Int32() - external int type; - external _CValue value; -} - -typedef _CallbackDispatch = void Function( - Object impl, int method, List message); - -final class _CallbackEntry { - _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; - - final Object impl; - final _CallbackDispatch dispatch; - final Pointer<_CallbackContext> context; - final Zone zone; -} - -final Map _callbacks = {}; -int _nextCallbackKey = 1; - -/// Registers [impl] and returns the `ctx` the producer receives. -Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { - final key = _nextCallbackKey++; - final context = calloc<_CallbackContext>(); - context.ref - ..key = key - ..post = _callbackPost; - _callbacks[key] = _CallbackEntry(impl, dispatch, context); - return context.cast(); -} - -/// The implementation registered under [ctx]. -Object _callbackTarget(Pointer ctx) { - final key = ctx.cast<_CallbackContext>().ref.key; - final entry = _callbacks[key]; - if (entry == null) { - throw StateError('callback context $key is not registered'); - } - return entry.impl; -} - -final Pointer<_CallbackPost> _callbackPost = () { - final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); - port.keepIsolateAlive = false; - _callbackPort = port; - final allocator = Platform.isWindows - ? DynamicLibrary.open('ole32.dll') - : DynamicLibrary.process(); - final post = calloc<_CallbackPost>(); - post.ref - ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') - ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') - ..post = NativeApi.postCObject.cast() - ..port = port.sendPort.nativePort; - return post; -}(); - -RawReceivePort? _callbackPort; - -void _receiveCallbackMessage(Object? message) { - final fields = message! as List; - final key = fields[0]! as int; - final method = fields[1]! as int; - if (method == _CallbackMessage.free) { - final entry = _callbacks.remove(key); - if (entry != null) calloc.free(entry.context); - return; - } - final entry = _callbacks[key]; - if (entry == null) return; - entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); -} - -/// One message from a forwarder: `[key, method, arguments...]`, built in -/// native memory and posted with `Dart_PostCObject`, which copies it. Runs -/// outside the isolate, so it touches no Dart globals. -final class _CallbackMessage { - _CallbackMessage(Pointer ctx, int method, int arguments) - : _context = ctx.cast<_CallbackContext>().ref, - _capacity = arguments + 2 { - final post = _context.post.ref; - final alloc = post.alloc.asFunction Function(int)>(); - _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); - _slots = alloc(sizeOf>() * _capacity).cast(); - int64(_context.key); - int64(method); - } - - static const int free = -1; - static const int _kBool = 1; - static const int _kInt64 = 3; - static const int _kDouble = 4; - static const int _kArray = 6; - static const int _kTypedData = 7; - static const int _kUint8 = 2; - - final _CallbackContext _context; - final int _capacity; - late final Pointer<_CObject> _objects; - late final Pointer> _slots; - int _count = 0; - - _CObject _next(int type) { - final object = _objects + (_count + 1); - _slots[_count++] = object; - return object.ref..type = type; - } - - void boolean(bool v) => _next(_kBool).value.asBool = v; - - void int64(int v) => _next(_kInt64).value.asInt64 = v; - - void float64(double v) => _next(_kDouble).value.asDouble = v; - - void pointer(Pointer v) => int64(v.address); - - /// A borrowed byte run; posting copies it. - void bytes(Pointer ptr, int len) { - final data = _next(_kTypedData).value.asTypedData; - data - ..type = _kUint8 - ..length = ptr == nullptr ? 0 : len - ..values = ptr; - } - - void send() { - final post = _context.post.ref; - _objects.ref.type = _kArray; - _objects.ref.value.asArray - ..length = _count - ..values = _slots; - post.post.asFunction)>()( - post.port, _objects); - final release = post.release.asFunction)>(); - release(_slots.cast()); - release(_objects.cast()); - } -} - -// The vtable `free` entry every callback interface shares. -void _forwardCallbackFree(Pointer ctx) => - _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); - -final Pointer)>> _callbackFree = - _pin(NativeCallable)>.isolateGroupBound( - _forwardCallbackFree)); - -// Vtable entries live for the process: they're never closed, so they're -// anchored here, and they don't keep an idle isolate alive. -final List> _pinned = >[]; - -Pointer> _pin(NativeCallable callable) { - callable.keepIsolateAlive = false; - _pinned.add(callable); - return callable.nativeFunction; -} - -final _errorSet = _lib.lookupFunction< - Void Function(Pointer<_Error>, Int32, Pointer), - void Function(Pointer<_Error>, int, Pointer)>('docs_everywhere_error_set'); - -final _errorSetPayload = _lib.lookupFunction< - Void Function(Pointer<_Error>, Pointer, Size), - void Function( - Pointer<_Error>, Pointer, int)>('docs_everywhere_error_set_payload'); - -final _alloc = _lib.lookupFunction Function(Size), - Pointer Function(int)>('docs_everywhere_alloc', isLeaf: true); - -/// Reports a failed callback method to the producer through [outErr]: -/// `docs_everywhere_error_set` copies [message], and `docs_everywhere_error_set_payload` -/// copies a domain code's encoded fields. Never throws: it runs in the catch -/// path of a trampoline an exception must not unwind through. -void _failCallback( - Pointer<_Error> outErr, int code, String message, Uint8List? payload) { - if (outErr == nullptr) return; - final text = message.toNativeUtf8(allocator: calloc); - try { - _errorSet(outErr, code, text); - } finally { - calloc.free(text); - } - if (payload == null || payload.isEmpty) return; - final bytes = calloc(payload.length); - try { - bytes.asTypedList(payload.length).setAll(0, payload); - _errorSetPayload(outErr, bytes, payload.length); - } finally { - calloc.free(bytes); - } -} - -/// Reports an implementation's exception that isn't a domain error of the -/// method with the foreign code (-4) and the exception's text. -void _foreignError(Pointer<_Error> outErr, Object error) { - try { - _failCallback(outErr, NativeException.foreignCode, '$error', null); - } catch (_) { - _errorSet(outErr, NativeException.foreignCode, nullptr); - } -} - -/// Hands [bytes] to the producer through a method's `out_ptr`/`out_len` -/// slots as a run allocated with `docs_everywhere_alloc`, which the producer -/// adopts and frees. An empty run is null with length 0. -void _handOver( - List bytes, Pointer> outPtr, Pointer outLen) { - final run = bytes.isEmpty ? nullptr : _alloc(bytes.length); - if (run != nullptr) run.asTypedList(bytes.length).setAll(0, bytes); - outPtr.value = run; - outLen.value = bytes.length; -} - -void _packListOfString(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeString(e)); - -List _unpackListOfString(_BufferReader r) => - r.readList(() => r.readString()); - -void _packOptionalOfDocument(_BufferWriter w, Document? v) => - w.writeOptional(v, (e) => _packDocument(w, e)); - -Document? _unpackOptionalOfDocument(_BufferReader r) => - r.readOptional(() => _unpackDocument(r)); - -/// The `DocsErrors` error domain of module `docs`: catch it to handle any of -/// its codes, or a subclass for one code. -sealed class DocsErrorsException extends NativeException { - /// Creates a domain error carrying [code] and [message]. - DocsErrorsException(super.code, super.message); -} - -/// Returned when no document matches the requested id -final class NotFoundException extends DocsErrorsException { - /// Creates a [NotFoundException] (code 1). - NotFoundException([String message = 'Document not found']) : super(1, message); -} - -/// Raised when attempting to move into a forbidden state -final class InvalidStatusException extends DocsErrorsException { - /// Creates an [InvalidStatusException] (code 2). - InvalidStatusException([String message = 'Invalid status transition']) : super(2, message); -} - -NativeException _mapDocsErrorsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return NotFoundException(message); - case 2: - return InvalidStatusException(message); - default: - return _runtimeException(code, message, payload); - } -} - -/// Lifecycle status of a document. -/// -/// Used to drive UI badges and filters. -enum Status { - /// Document is being edited - draft(0), - /// Document is publicly visible - published(1), - /// Document is hidden from listings - archived(2); - - const Status(this.value); - - /// The C discriminant. - final int value; - - /// The variant whose discriminant is [value]. - static Status fromValue(int value) => values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError.value(value, 'value', 'not a Status discriminant')); -} - -/// Represents a single document tracked by the system. -/// -/// Documents are persisted to disk and exposed via the public API. -final class Document { - /// Creates a [Document]. - Document({required this.id, required this.title, required this.status, required this.tags}); - - /// Stable opaque identifier - final int id; - /// Human-readable title shown in lists - final String title; - /// Current lifecycle status - final Status status; - /// Free-form tags for grouping - final List tags; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Document && - id == other.id && - title == other.title && - status == other.status && - _deepEquals(tags, other.tags); - - @override - int get hashCode => Object.hashAll([Document, id, title, status, _deepHash(tags)]); - - @override - String toString() => 'Document(id: $id, title: $title, status: $status, tags: $tags)'; -} - -void _packDocument(_BufferWriter w, Document v) { - w.writeI64(v.id); - w.writeString(v.title); - w.writeI32(v.status.value); - _packListOfString(w, v.tags); -} - -Document _unpackDocument(_BufferReader r) => Document( - id: r.readI64(), - title: r.readString(), - status: Status.fromValue(r.readI32()), - tags: _unpackListOfString(r), - ); - -/// Consumer-implemented observer of document-saved notifications. -/// -/// Implementations must be safe to call from any thread. -/// -/// A callback interface: implement it and pass an instance to any -/// function taking a [SavedListener]. The library keeps the instance alive -/// until the producer releases it. -/// -/// Methods that return a value run synchronously and must be called by -/// the producer on this isolate's thread during a call from Dart; a -/// thrown exception fails that call with [NativeException.foreignCode] -/// (or, from a method that throws, the domain exception's code and -/// fields). Void methods may be called from any thread: they're -/// delivered asynchronously on this isolate's event loop, in the zone -/// that passed the instance, where a thrown exception is an uncaught -/// error of that zone. Object arguments are owned by the -/// implementation. -abstract class SavedListener { - /// Fires after a document has been persisted successfully - void onSaved(int docId, int revision); -} - -typedef _DocsEverywhereDocsSavedListenerOnSaved = Void Function(Pointer, Int64, Int32, Pointer<_Error>); - -// The C `docs_everywhere_docs_SavedListener_vtable` layout. -final class _SavedListenerVtable extends Struct { - @Uint32() - external int size; - @Uint32() - external int flags; - external Pointer)>> free; - external Pointer> onSaved; -} - -// Vtable entry `docs_everywhere_docs_SavedListener_vtable.on_saved`: forwarded to the isolate. -void _savedListenerOnSaved(Pointer ctx, int docId, int revision, Pointer<_Error> outErr) { - _CallbackMessage(ctx, 0, 2) - ..int64(docId) - ..int64(revision) - ..send(); -} - -void _savedListenerDispatch(Object impl, int method, List message) { - final target = impl as SavedListener; - switch (method) { - case 0: { - final a2 = message[2]! as int; - final a3 = message[3]! as int; - target.onSaved(a2, a3); - } - } -} - -final Pointer<_SavedListenerVtable> _savedListenerVtable = () { - final vtable = calloc<_SavedListenerVtable>(); - vtable.ref - ..size = sizeOf<_SavedListenerVtable>() - ..flags = 0 - ..free = _callbackFree - ..onSaved = _pin(NativeCallable<_DocsEverywhereDocsSavedListenerOnSaved>.isolateGroupBound(_savedListenerOnSaved)); - return vtable; -}(); - -final _docsEverywhereDocsNotebookClone = _lib.lookupFunction< - Pointer Function(Pointer), - Pointer Function(Pointer)>('docs_everywhere_docs_Notebook_clone'); - -final _docsEverywhereDocsNotebookDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('docs_everywhere_docs_Notebook_destroy'); -final _docsEverywhereDocsNotebookDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('docs_everywhere_docs_Notebook_destroy')); - -final _docsEverywhereDocsNotebookNew = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('docs_everywhere_docs_Notebook_new'); - -final _docsEverywhereDocsNotebookInsert = _lib.lookupFunction< - Int32 Function(Pointer, Pointer, Size, Pointer<_Error>), - int Function(Pointer, Pointer, int, Pointer<_Error>)>('docs_everywhere_docs_Notebook_insert'); - -final _docsEverywhereDocsNotebookFind = _lib.lookupFunction< - Pointer Function(Pointer, Int64, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('docs_everywhere_docs_Notebook_find'); - -final _docsEverywhereDocsNotebookSize = _lib.lookupFunction< - Int32 Function(Pointer, Pointer<_Error>), - int Function(Pointer, Pointer<_Error>)>('docs_everywhere_docs_Notebook_size'); - -final _docsEverywhereDocsNotebookMaxDocuments = _lib.lookupFunction< - Int32 Function(Pointer<_Error>), - int Function(Pointer<_Error>)>('docs_everywhere_docs_Notebook_max_documents'); - -/// An ordered collection of documents. -/// -/// Notebooks own their documents and release them when destroyed. -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, when an undisposed instance is -/// collected, by a finalizer. -final class Notebook extends _NativeObject { - Notebook._(super._ptr); - - @override - NativeFinalizer get _finalizer => _docsEverywhereDocsNotebookDestroyFinalizer; - - @override - void _destroy(Pointer ptr) => _docsEverywhereDocsNotebookDestroy(ptr); - - @override - Pointer _clone(Pointer ptr) => _docsEverywhereDocsNotebookClone(ptr); - - /// Create an empty notebook - /// - /// The returned object is owned by the caller: call its `dispose()` - /// when you're done with it. - factory Notebook() { - final _result = _docsEverywhereDocsNotebookNew(_err); - _check(_err); - return Notebook._(_result); - } - - /// Add a document, returning its position in the notebook - int insert(Document document) { - final _self = _enter(); - final _arena = Arena(); - try { - final _documentBytes = _encode(document, _packDocument); - final _result = _docsEverywhereDocsNotebookInsert(_self, _stage(_arena, _documentBytes), _documentBytes.length, _err); - _check(_err); - return _result; - } finally { - _arena.releaseAll(); - _leave(); - } - } - - /// Look up a document by identifier, failing when none matches - /// - /// Throws [DocsErrorsException] on domain errors. - Document find(int id) { - final _self = _enter(); - try { - final _result = _docsEverywhereDocsNotebookFind(_self, id, _outLen, _err); - _check(_err, _mapDocsErrorsException); - return _decode(_takeBytes(_result, _outLen.value), _unpackDocument); - } finally { - _leave(); - } - } - - /// The number of documents currently held - int size() { - final _self = _enter(); - try { - final _result = _docsEverywhereDocsNotebookSize(_self, _err); - _check(_err); - return _result; - } finally { - _leave(); - } - } - - /// The largest number of documents one notebook will hold - static int maxDocuments() { - final _result = _docsEverywhereDocsNotebookMaxDocuments(_err); - _check(_err); - return _result; - } -} - -final _docsEverywhereDocsCreateDocument = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('docs_everywhere_docs_create_document'); - -/// Create a brand new document. -/// -/// The document starts in `Draft` status; callers must publish -/// it explicitly to make it visible. -Document createDocument(String title, List tags) { - final _arena = Arena(); - try { - final _titleBytes = utf8.encode(title); - final _tagsBytes = _encode(tags, _packListOfString); - final _result = _docsEverywhereDocsCreateDocument(_stage(_arena, _titleBytes), _titleBytes.length, _stage(_arena, _tagsBytes), _tagsBytes.length, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackDocument); - } finally { - _arena.releaseAll(); - } -} - -final _docsEverywhereDocsGetDocument = _lib.lookupFunction< - Pointer Function(Int64, Pointer, Pointer<_Error>), - Pointer Function(int, Pointer, Pointer<_Error>)>('docs_everywhere_docs_get_document'); - -/// Look up a document by identifier -Document? getDocument(int id) { - final _result = _docsEverywhereDocsGetDocument(id, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackOptionalOfDocument); -} - -typedef _DocsEverywhereDocsPublishAsyncCallback = Void Function(Pointer, Pointer<_Error>, Bool); - -final _docsEverywhereDocsPublishAsync = _lib.lookupFunction< - Void Function(Int64, Pointer>, Pointer), - void Function(int, Pointer>, Pointer)>('docs_everywhere_docs_publish_async'); - -/// Publish a document in the background -Future publishAsync(int id) { - final _completer = Completer(); - late final NativeCallable<_DocsEverywhereDocsPublishAsyncCallback> _callback; - _callback = NativeCallable<_DocsEverywhereDocsPublishAsyncCallback>.listener(( - Pointer _, Pointer<_Error> error, bool value) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(value); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _docsEverywhereDocsPublishAsync(id, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -final _docsEverywhereDocsWatch = _lib.lookupFunction< - Void Function(Pointer, Pointer, Pointer<_Error>), - void Function(Pointer, Pointer, Pointer<_Error>)>('docs_everywhere_docs_watch'); - -/// Register a listener for saved notifications -void watch(SavedListener listener) { - final _listenerCtx = _registerCallback(listener, _savedListenerDispatch); - _docsEverywhereDocsWatch(_listenerCtx, _savedListenerVtable.cast(), _err); - _check(_err); -} - -// END docs_everywhere.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_edge_cases_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_edge_cases_dart.snap deleted file mode 100644 index c1bfe078..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_edge_cases_dart.snap +++ /dev/null @@ -1,2718 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element - -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -// ── Library loading ── -// The native library opens on first use and is verified before any other -// lookup: it must implement the C ABI revision these bindings were generated -// against and carry every declaration they were generated with, unchanged. - -/// The native library every binding in this file calls. -final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); - -/// The native library couldn't be loaded, or it doesn't match these -/// bindings: a different C ABI revision, or a declaration that's missing or -/// changed. Regenerate the bindings from the library you ship. -final class NativeLibraryError extends Error { - /// Creates a load failure described by [message]. - NativeLibraryError(this.message); - - /// What failed, naming the library or declaration. - final String message; - - @override - String toString() => 'NativeLibraryError: $message'; -} - -/// The platform file name of the library on the system search path. -String get _libraryFile => Platform.isMacOS - ? 'libedge_cases.dylib' - : Platform.isWindows - ? 'edge_cases.dll' - : 'libedge_cases.so'; - -DynamicLibrary _openLibrary() { - // An explicit path wins, so a caller can point at any build artifact. - final override = Platform.environment['EDGE_CASES_LIBRARY']; - if (override != null && override.isNotEmpty) { - return _open(override); - } - // iOS links native code into the app binary. - if (Platform.isIOS) return DynamicLibrary.process(); - try { - return DynamicLibrary.open(_libraryFile); - } on ArgumentError catch (e) { - // A macOS app may link the library into its executable instead. - if (Platform.isMacOS) return DynamicLibrary.process(); - throw NativeLibraryError("can't open '$_libraryFile' ($e); set " - 'EDGE_CASES_LIBRARY to its path'); - } -} - -DynamicLibrary _open(String path) { - try { - return DynamicLibrary.open(path); - } on ArgumentError catch (e) { - throw NativeLibraryError("can't open '$path': $e"); - } -} - -/// The C ABI revision these bindings were generated against. -const int _abiVersion = 4; - -/// One `edge_cases_contract_entry` of a module's contract table. -final class _ContractEntry extends Struct { - @Uint64() - external int id; - @Uint64() - external int hash; -} - -DynamicLibrary _verifyLibrary(DynamicLibrary lib) { - final int Function() abiVersion; - try { - abiVersion = lib.lookupFunction( - 'edge_cases_abi_version'); - } on ArgumentError { - throw NativeLibraryError( - "the native library 'edge_cases' isn't loaded; set EDGE_CASES_LIBRARY " - 'to its path'); - } - final found = abiVersion(); - if (found != _abiVersion) { - throw NativeLibraryError('edge_cases: these bindings expect C ABI revision ' - '$_abiVersion, but the loaded library implements revision $found'); - } - for (final (symbol, expected) in _contracts) { - _checkContract(lib, symbol, expected); - } - return lib; -} - -/// Checks that every `(id, hash, path)` entry in [expected] is in the table -/// [symbol] returns with an equal hash. Entries only the library has are -/// fine: they're declarations added after these bindings were generated. -void _checkContract( - DynamicLibrary lib, String symbol, List<(int, int, String)> expected) { - final Pointer<_ContractEntry> Function(Pointer) table; - try { - table = lib.lookupFunction Function(Pointer), - Pointer<_ContractEntry> Function(Pointer)>(symbol); - } on ArgumentError { - throw NativeLibraryError( - "edge_cases: the loaded library doesn't export '$symbol'"); - } - final hashes = {}; - final len = calloc(); - try { - final entries = table(len); - for (var i = 0; i < len.value; i++) { - hashes[entries[i].id] = entries[i].hash; - } - } finally { - calloc.free(len); - } - for (final (id, hash, path) in expected) { - final found = hashes[id]; - if (found == null) { - throw NativeLibraryError('edge_cases: $path is missing from the library'); - } - if (found != hash) { - throw NativeLibraryError( - 'edge_cases: $path changed since these bindings were generated'); - } - } -} - -/// Each top-level module's contract-table symbol and the declarations -/// these bindings were generated with: (id, signature hash, path). -const List<(String, List<(int, int, String)>)> _contracts = [ - ('edge_cases_edge_cases_contract', [ - (0x056a86511785ba2b, 0x5cba3f16d57c6cdb, 'edge_cases.make_widget'), - (0x07bfa9ecb2b720d2, 0x3813b3f57c64bf35, 'edge_cases.EdgeErrors'), - (0x0aaa11867e37f991, 0xf090dd52146ebdf0, 'edge_cases.Widget.refresh'), - (0x0f9701493898ae4c, 0x6944f47a275c96e8, 'edge_cases.Inner'), - (0x1226b1adac179f7d, 0x0e95a00ab0bef637, 'edge_cases.async_keywords'), - (0x16d36bef1541136c, 0x5ea7021ec324298f, 'edge_cases.find_widget'), - (0x1b7324c55e980feb, 0xabf87fe372358f42, 'edge_cases.Widget.old_name'), - (0x1cbd731467c2bab8, 0x4915b9989c2c9074, 'edge_cases.Vault'), - (0x2c2a6de67d894b0a, 0x50a4496b1e7191ca, 'edge_cases.Vault.open'), - (0x322b4cfcd2d6861c, 0xca7527a379ae8465, 'edge_cases.Keywords'), - (0x3f8e23a5f32906f4, 0x8d4c908d796f130d, 'edge_cases.Leaf'), - (0x42ab8a685457b97c, 0xe5ce30500cadb32d, 'edge_cases.Vault.handle'), - (0x4a68540e2836d3d0, 0xa20e44c71e420ed2, 'edge_cases.mode_stream'), - (0x4e030580ac0b111d, 0xb68b7a18d910e175, 'edge_cases.legacy_value'), - (0x4f5e4e5291864ef3, 0x55a1a88b21a08f8b, 'edge_cases.Legacy'), - (0x4f84b06171b4213d, 0xbaedcfdebb6a2820, 'edge_cases.scalar_stream'), - (0x5067a5ff2d9ef910, 0xbfc79b18fc4714fc, 'edge_cases.Widget'), - (0x59aff159112fbf5b, 0x18e4def57a8cfa49, 'edge_cases.optional_params'), - (0x5daf65b98fa77c48, 0xc817a8cb202e59c2, 'edge_cases.keywords_roundtrip'), - (0x5de8a5c9726c2c5a, 0x7c50b5b65f7a896d, 'edge_cases.register_batch'), - (0x618a620309994c3c, 0x826ad0b1d208d723, 'edge_cases.async_bytes'), - (0x648285cdba0a7b8e, 0xcf6e80e783a4d8be, 'edge_cases.WidgetHolder'), - (0x649c78079824b10d, 0x5b12dd26e0634b69, 'edge_cases.async_widget'), - (0x7da7a7357d122a37, 0xf544e025ffaa3d20, 'edge_cases.async_void'), - (0x7e4015bc3a472e2d, 0xfe22291d90b82e4a, 'edge_cases.widget_stream'), - (0x805276e5d81fdba7, 0xd9b64e360bd03b2b, 'edge_cases.Outer'), - (0x80e1183326542df4, 0xe8a57e12cc6ed276, 'edge_cases.select'), - (0x923ce5e9bcd769a1, 0x0b973f5dc07dda8b, 'edge_cases.Vault.to_string'), - (0x98ca8c01f5239404, 0xfe11c93eb6b303ef, 'edge_cases.Widget.rename'), - (0xa13e22e98ea278d4, 0xf1fae215d7387800, 'edge_cases.widgets_everywhere'), - (0xa84a8c9bf2a881e1, 0xbe1dc840bf221996, 'edge_cases.async_optional'), - (0xad958c478fbf205d, 0xe386b0716d24f19b, 'edge_cases.Retired'), - (0xb2fc3964ef56ab39, 0x73c325c8bc351236, 'edge_cases.async_list'), - (0xb3403af521cf0006, 0x6abc30d48206a3ba, 'edge_cases.Widget.close'), - (0xb794304a22a406b8, 0xf6fb98711c516105, 'edge_cases.async_struct'), - (0xba3936106a49c47d, 0xb7025f2fadd6288e, 'edge_cases.Vault.peek'), - (0xc155b0c3cfb164f4, 0xc777ae57d92ebc47, 'edge_cases.noop'), - (0xcdc18607777019dd, 0xb81a31e5c20aa7a8, 'edge_cases.inspect_widget'), - (0xd164a82aeb64b8cc, 0xded02c68005c4737, 'edge_cases.nested_optional'), - (0xd978ccad2468038f, 0x30c39c65d6235153, 'edge_cases.Mode'), - (0xdbe9d947431c9124, 0x39537fb848fdf980, 'edge_cases.BatchListener'), - (0xeae8b7862c08bb07, 0xfe12eeaa830452ea, 'edge_cases.Keyword'), - (0xf1e155a05ae262b8, 0xb575eb818a423d42, 'edge_cases.async_enum'), - (0xf22e210bdbce7343, 0x2a5e0f801bc8c768, 'edge_cases.Widget.dispose'), - (0xf3c7cf675563ee6c, 0x0ecffeef399facc8, 'edge_cases.Vault.probe'), - (0xfd34bc67acac379e, 0x576caa14f3c0901a, 'edge_cases.Widget.fetch'), - (0xff2e9d2bff941786, 0x7e96764ef1f5fbaa, 'edge_cases.string_stream'), - ]), -]; - -// ── Errors and memory ── - -/// The C `edge_cases_error` slot every fallible call writes. A non-zero -/// `code` carries a producer-owned `message` and, for a domain error with -/// fields, a value-buffer `payload`; `edge_cases_error_clear` releases both. -final class _Error extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -final _errorClear = _lib.lookupFunction), - void Function(Pointer<_Error>)>('edge_cases_error_clear', isLeaf: true); - -final _freeBytes = _lib.lookupFunction, Size), - void Function(Pointer, int)>('edge_cases_free_bytes', isLeaf: true); - -// Synchronous calls on this isolate share one error slot and one length -// slot: each call reads (and clears) them right after it returns, before any -// other call can reuse them, including a nested call from a callback. -final Pointer<_Error> _err = calloc<_Error>(); -final Pointer _outLen = calloc(); - -/// A failure the native library reported to a function declared to throw. -/// Domain errors extend this class with a positive [code]; a negative code is -/// one of the runtime constants below. -class NativeException implements Exception { - /// Creates an exception carrying [code] and [message]. - NativeException(this.code, this.message); - - /// The producer reported an untyped error. - static const int genericCode = -1; - - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - - /// An argument or a callback's return couldn't be lifted by the producer. - static const int marshalCode = -3; - - /// A callback implementation failed; [message] carries its text. - static const int foreignCode = -4; - - /// The call was cancelled (see [CancelledException]). - static const int cancelledCode = -5; - - /// The error code: positive for a domain error, negative for a runtime - /// failure. - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => '$runtimeType($code): $message'; -} - -/// A cancellable call completed because its token was cancelled. -class CancelledException extends NativeException { - /// Creates a cancellation with the producer's [message]. - CancelledException([String message = 'cancelled']) - : super(NativeException.cancelledCode, message); -} - -/// A function that isn't declared to throw failed anyway. That's a bug in -/// the native library (a panic, an argument it couldn't lift, a callback -/// failure it let through), so it's an [Error], not an [Exception]. -final class NativeError extends Error { - /// Creates the error a failed call reported. - NativeError(this.code, this.message); - - /// The runtime code the producer reported (see [NativeException]). - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => 'NativeError($code): $message'; -} - -/// Maps a reported code, message, and payload onto the object to throw. -typedef _ErrorMapper = Object Function( - int code, String message, Uint8List payload); - -/// The mapper of a function that can't fail: every code but cancellation -/// is a [NativeError]. -Object _trap(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeError(code, message); - -/// Maps a runtime code (or one the domain doesn't declare) onto its -/// exception, for a function declared to throw. -NativeException _runtimeException(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeException(code, message); - -/// Builds the error [err] reports, using [map]. -Object _readError(Pointer<_Error> err, _ErrorMapper map) { - final e = err.ref; - final message = e.message == nullptr ? '' : e.message.toDartString(); - return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); -} - -/// Throws the error a synchronous call left in [err], clearing the slot. -/// [map] is the domain's mapper for a function declared to throw. -void _check(Pointer<_Error> err, [_ErrorMapper map = _trap]) { - if (err.ref.code == 0) return; - final error = _readError(err, map); - _errorClear(err); - throw error; -} - -/// Copies a borrowed native byte run into Dart memory. -Uint8List _copyBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -/// Copies an owned native byte run and releases it. -Uint8List _takeBytes(Pointer ptr, int len) { - if (ptr == nullptr) return Uint8List(0); - final bytes = Uint8List.fromList(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return bytes; -} - -/// Decodes an owned UTF-8 run and releases it. -String _takeString(Pointer ptr, int len) { - if (ptr == nullptr) return ''; - final text = utf8.decode(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return text; -} - -/// Decodes a borrowed UTF-8 run. -String _readString(Pointer ptr, int len) => - ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); - -/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. -Pointer _stage(Arena arena, List bytes) { - if (bytes.isEmpty) return nullptr; - final ptr = arena(bytes.length); - ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -// ── Objects ── - -/// The lifetime machinery every interface wrapper shares. A wrapper owns one -/// strong reference to its native object and releases it exactly once: in -/// [dispose], or through a GC finalizer when it's collected undisposed. -/// -/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] -/// that arrives while a call is in flight (from a callback the call made) -/// only marks the wrapper; the reference is released when the last call -/// leaves, so the native object never disappears under a running call. -abstract base class _NativeObject implements Finalizable { - _NativeObject(this._ptr) { - _finalizer.attach(this, _ptr, detach: this); - } - - final Pointer _ptr; - int _calls = 0; - bool _disposed = false; - - NativeFinalizer get _finalizer; - void _destroy(Pointer ptr); - Pointer _clone(Pointer ptr); - - /// Releases this wrapper's native reference. Safe to call more than once; - /// the native object is dropped when its last reference (this one, another - /// wrapper's, or the producer's) goes away. Using the wrapper afterwards - /// throws a [StateError]. - void dispose() { - if (_disposed) return; - _disposed = true; - if (_calls == 0) _release(); - } - - void _release() { - _finalizer.detach(this); - _destroy(_ptr); - } - - Pointer _enter() { - if (_disposed) throw StateError('$runtimeType used after dispose()'); - _calls++; - return _ptr; - } - - void _leave() { - if (--_calls == 0 && _disposed) _release(); - } - - /// A second strong reference, for an object token in a value buffer. - Pointer _cloneRef() { - final ptr = _enter(); - try { - return _clone(ptr); - } finally { - _leave(); - } - } -} - -/// Borrows [object]'s pointer until [arena] is released. -Pointer _borrow(Arena arena, _NativeObject object) { - final ptr = object._enter(); - arena.onReleaseAll(object._leave); - return ptr; -} - -// ── Value buffers ── -// Records, rich enums, optionals, lists, maps, and error payloads cross the -// C ABI serialized in this little-endian, packed format. A malformed buffer -// is a contract violation between producer and bindings, never a domain -// error. - -Never _bufferError(String context) => - throw StateError('malformed value buffer: $context'); - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - late ByteData _data = ByteData.sublistView(_buf); - int _len = 0; - - void _reserve(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - _buf = Uint8List(cap)..setRange(0, _len, _buf); - _data = ByteData.sublistView(_buf); - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeU8(v ? 1 : 0); - - void writeFlag(bool present) => writeU8(present ? 1 : 0); - - void writeI8(int v) { - _reserve(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeU8(int v) { - _reserve(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeI16(int v) { - _reserve(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeU16(int v) { - _reserve(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeI32(int v) { - _reserve(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeU32(int v) { - _reserve(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeI64(int v) { - _reserve(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - // Dart ints are signed 64-bit; a u64 travels as its bit pattern. - void writeU64(int v) => writeI64(v); - - void writeF32(double v) { - _reserve(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeF64(double v) { - _reserve(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeU32(v); - - void writeString(String v) => writeBytes(utf8.encode(v)); - - void writeBytes(List v) { - writeLength(v.length); - _reserve(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } - - void writeOptional(T? v, void Function(T) write) { - writeFlag(v != null); - if (v != null) write(v); - } - - void writeList(List v, void Function(T) write) { - writeLength(v.length); - for (final e in v) { - write(e); - } - } - - void writeMap( - Map v, void Function(K) writeKey, void Function(V) writeValue) { - writeLength(v.length); - for (final e in v.entries) { - writeKey(e.key); - writeValue(e.value); - } - } -} - -final class _BufferReader { - _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); - - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - int _take(int n, String context) { - if (_buf.length - _pos < n) _bufferError('truncated $context'); - final at = _pos; - _pos += n; - return at; - } - - bool readBool() { - final b = _buf[_take(1, 'bool')]; - if (b > 1) _bufferError('bool byte $b'); - return b == 1; - } - - bool readFlag() { - final b = _buf[_take(1, 'option flag')]; - if (b > 1) _bufferError('option flag $b'); - return b == 1; - } - - int readI8() => _data.getInt8(_take(1, 'i8')); - - int readU8() => _buf[_take(1, 'u8')]; - - int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); - - int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); - - int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); - - int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); - - int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); - - // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. - int readU64() => readI64(); - - double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); - - double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); - - int readLength() => readU32(); - - String readString() { - final n = readLength(); - final at = _take(n, 'string'); - return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); - } - - Uint8List readBytes() { - final n = readLength(); - final at = _take(n, 'bytes'); - return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); - } - - T? readOptional(T Function() read) => - readFlag() ? read() : null; - - // Collections grow as they decode, so a corrupt count can't force a huge - // allocation up front. - List readList(T Function() read) => - [for (var i = readLength(); i > 0; i--) read()]; - - Map readMap(K Function() readKey, V Function() readValue) { - final map = {}; - for (var i = readLength(); i > 0; i--) { - final key = readKey(); - if (map.containsKey(key)) _bufferError('duplicate map key $key'); - map[key] = readValue(); - } - return map; - } - - void expectEnd() { - if (_pos != _buf.length) _bufferError('trailing bytes'); - } -} - -/// Encodes one value with [write] into a fresh buffer. -Uint8List _encode(T value, void Function(_BufferWriter, T) write) { - final writer = _BufferWriter(); - write(writer, value); - return writer.takeBytes(); -} - -/// Decodes exactly one value with [read] from [bytes]. -T _decode(Uint8List bytes, T Function(_BufferReader) read) { - final reader = _BufferReader(bytes); - final value = read(reader); - reader.expectEnd(); - return value; -} - -// Records and rich enums compare by value: lists, maps, and byte arrays -// element by element, and everything else with `==` (interface wrappers by -// identity). - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) return true; - if (a is List && b is List) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!_deepEquals(a[i], b[i])) return false; - } - return true; - } - if (a is Map && b is Map) { - if (a.length != b.length) return false; - for (final key in a.keys) { - if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false; - } - return true; - } - return a == b; -} - -int _deepHash(Object? v) => switch (v) { - List() => Object.hashAll(v.map(_deepHash)), - Map() => Object.hashAllUnordered( - v.entries.map((e) => Object.hash(_deepHash(e.key), _deepHash(e.value)))), - _ => v.hashCode, - }; - -// ── Async completion ── -// An async call completes through a `NativeCallable.listener` the producer -// invokes exactly once, from any thread. Everything the completion carries is -// owned by these bindings, so it's decoded later on the event loop and then -// released. - -final _errorFree = - _lib.lookupFunction), void Function(Pointer<_Error>)>( - 'edge_cases_error_free'); - -/// Builds the error a completion reports and frees its boxed error. -Object _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { - final error = _readError(err, map); - _errorFree(err); - return error; -} - -// ── Cancellation ── - -/// Cancels the cancellable calls it's passed to. One token can serve any -/// number of calls; once cancelled it stays cancelled, and a call that -/// receives a cancelled token completes with [CancelledException] without -/// waiting for its work. -final class CancelToken { - bool _cancelled = false; - final List _listeners = []; - - /// Whether [cancel] has been called. - bool get isCancelled => _cancelled; - - /// Requests cancellation of every call this token was passed to. Calls - /// that already completed are unaffected. Idempotent. - void cancel() { - if (_cancelled) return; - _cancelled = true; - final listeners = List.of(_listeners); - _listeners.clear(); - for (final listener in listeners) { - listener(); - } - } -} - -final _cancelTokenCreate = - _lib.lookupFunction Function(), Pointer Function()>( - 'edge_cases_cancel_token_create'); -final _cancelTokenCancel = _lib.lookupFunction), - void Function(Pointer)>('edge_cases_cancel_token_cancel'); -final _cancelTokenDestroy = _lib.lookupFunction), - void Function(Pointer)>('edge_cases_cancel_token_destroy'); - -/// The native token behind one launch: created for the call, cancelled when -/// the [CancelToken] is, and destroyed once the call completes. The producer -/// holds its own reference, so destroying ours never races the call. -final class _NativeCancel { - _NativeCancel._(this._token) : pointer = _cancelTokenCreate() { - if (_token.isCancelled) { - _cancelTokenCancel(pointer); - } else { - _token._listeners.add(_cancel); - } - } - - /// Binds a native token to [token], or returns null for no token. - static _NativeCancel? bind(CancelToken? token) => - token == null ? null : _NativeCancel._(token); - - final CancelToken _token; - - /// The `edge_cases_cancel_token*` passed to the launcher. - final Pointer pointer; - - void _cancel() => _cancelTokenCancel(pointer); - - /// Detaches from the [CancelToken] and drops the native reference. - void release() { - _token._listeners.remove(_cancel); - _cancelTokenDestroy(pointer); - } -} - -// ── Callback interfaces ── -// An implementation passed to the producer is parked in [_callbacks] under -// an integer key. The `ctx` the producer receives points at a small native -// [_CallbackContext] holding that key, so the producer never holds a Dart -// object and the GC never sees a raw pointer. The entry lives until the -// producer calls the vtable's `free`. -// -// Methods that return a value are `NativeCallable.isolateLocal` trampolines: -// they run synchronously, so the producer must call them on this isolate's -// thread while a call from Dart is in progress (the VM aborts the process -// otherwise, and an entry point that may run on any thread can't re-enter -// the isolate to run the method). Void methods and `free` are -// `NativeCallable.isolateGroupBound` forwarders, which may run on any -// thread: they copy their arguments into a message for this isolate -// (borrowed bytes are copied before the forwarder returns) and the -// implementation runs later on the event loop, in the zone that passed the -// callback. - -/// Where forwarders send messages: the allocator they build a message with -/// and the isolate port that receives it. Forwarders run outside the -/// isolate and can't read Dart globals, so everything they need is native. -final class _CallbackPost extends Struct { - external Pointer Function(Size)>> alloc; - external Pointer)>> release; - external Pointer)>> post; - @Int64() - external int port; -} - -/// The native `ctx` of one registered implementation. -final class _CallbackContext extends Struct { - @Int64() - external int key; - external Pointer<_CallbackPost> post; -} - -// The `Dart_CObject` layout `Dart_PostCObject` reads. -final class _CArray extends Struct { - @IntPtr() - external int length; - external Pointer> values; -} - -final class _CTypedData extends Struct { - @Int32() - external int type; - @IntPtr() - external int length; - external Pointer values; -} - -final class _CValue extends Union { - @Bool() - external bool asBool; - @Int64() - external int asInt64; - @Double() - external double asDouble; - external _CArray asArray; - external _CTypedData asTypedData; -} - -final class _CObject extends Struct { - @Int32() - external int type; - external _CValue value; -} - -typedef _CallbackDispatch = void Function( - Object impl, int method, List message); - -final class _CallbackEntry { - _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; - - final Object impl; - final _CallbackDispatch dispatch; - final Pointer<_CallbackContext> context; - final Zone zone; -} - -final Map _callbacks = {}; -int _nextCallbackKey = 1; - -/// Registers [impl] and returns the `ctx` the producer receives. -Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { - final key = _nextCallbackKey++; - final context = calloc<_CallbackContext>(); - context.ref - ..key = key - ..post = _callbackPost; - _callbacks[key] = _CallbackEntry(impl, dispatch, context); - return context.cast(); -} - -/// The implementation registered under [ctx]. -Object _callbackTarget(Pointer ctx) { - final key = ctx.cast<_CallbackContext>().ref.key; - final entry = _callbacks[key]; - if (entry == null) { - throw StateError('callback context $key is not registered'); - } - return entry.impl; -} - -final Pointer<_CallbackPost> _callbackPost = () { - final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); - port.keepIsolateAlive = false; - _callbackPort = port; - final allocator = Platform.isWindows - ? DynamicLibrary.open('ole32.dll') - : DynamicLibrary.process(); - final post = calloc<_CallbackPost>(); - post.ref - ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') - ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') - ..post = NativeApi.postCObject.cast() - ..port = port.sendPort.nativePort; - return post; -}(); - -RawReceivePort? _callbackPort; - -void _receiveCallbackMessage(Object? message) { - final fields = message! as List; - final key = fields[0]! as int; - final method = fields[1]! as int; - if (method == _CallbackMessage.free) { - final entry = _callbacks.remove(key); - if (entry != null) calloc.free(entry.context); - return; - } - final entry = _callbacks[key]; - if (entry == null) return; - entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); -} - -/// One message from a forwarder: `[key, method, arguments...]`, built in -/// native memory and posted with `Dart_PostCObject`, which copies it. Runs -/// outside the isolate, so it touches no Dart globals. -final class _CallbackMessage { - _CallbackMessage(Pointer ctx, int method, int arguments) - : _context = ctx.cast<_CallbackContext>().ref, - _capacity = arguments + 2 { - final post = _context.post.ref; - final alloc = post.alloc.asFunction Function(int)>(); - _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); - _slots = alloc(sizeOf>() * _capacity).cast(); - int64(_context.key); - int64(method); - } - - static const int free = -1; - static const int _kBool = 1; - static const int _kInt64 = 3; - static const int _kDouble = 4; - static const int _kArray = 6; - static const int _kTypedData = 7; - static const int _kUint8 = 2; - - final _CallbackContext _context; - final int _capacity; - late final Pointer<_CObject> _objects; - late final Pointer> _slots; - int _count = 0; - - _CObject _next(int type) { - final object = _objects + (_count + 1); - _slots[_count++] = object; - return object.ref..type = type; - } - - void boolean(bool v) => _next(_kBool).value.asBool = v; - - void int64(int v) => _next(_kInt64).value.asInt64 = v; - - void float64(double v) => _next(_kDouble).value.asDouble = v; - - void pointer(Pointer v) => int64(v.address); - - /// A borrowed byte run; posting copies it. - void bytes(Pointer ptr, int len) { - final data = _next(_kTypedData).value.asTypedData; - data - ..type = _kUint8 - ..length = ptr == nullptr ? 0 : len - ..values = ptr; - } - - void send() { - final post = _context.post.ref; - _objects.ref.type = _kArray; - _objects.ref.value.asArray - ..length = _count - ..values = _slots; - post.post.asFunction)>()( - post.port, _objects); - final release = post.release.asFunction)>(); - release(_slots.cast()); - release(_objects.cast()); - } -} - -// The vtable `free` entry every callback interface shares. -void _forwardCallbackFree(Pointer ctx) => - _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); - -final Pointer)>> _callbackFree = - _pin(NativeCallable)>.isolateGroupBound( - _forwardCallbackFree)); - -// Vtable entries live for the process: they're never closed, so they're -// anchored here, and they don't keep an idle isolate alive. -final List> _pinned = >[]; - -Pointer> _pin(NativeCallable callable) { - callable.keepIsolateAlive = false; - _pinned.add(callable); - return callable.nativeFunction; -} - -final _errorSet = _lib.lookupFunction< - Void Function(Pointer<_Error>, Int32, Pointer), - void Function(Pointer<_Error>, int, Pointer)>('edge_cases_error_set'); - -final _errorSetPayload = _lib.lookupFunction< - Void Function(Pointer<_Error>, Pointer, Size), - void Function( - Pointer<_Error>, Pointer, int)>('edge_cases_error_set_payload'); - -final _alloc = _lib.lookupFunction Function(Size), - Pointer Function(int)>('edge_cases_alloc', isLeaf: true); - -/// Reports a failed callback method to the producer through [outErr]: -/// `edge_cases_error_set` copies [message], and `edge_cases_error_set_payload` -/// copies a domain code's encoded fields. Never throws: it runs in the catch -/// path of a trampoline an exception must not unwind through. -void _failCallback( - Pointer<_Error> outErr, int code, String message, Uint8List? payload) { - if (outErr == nullptr) return; - final text = message.toNativeUtf8(allocator: calloc); - try { - _errorSet(outErr, code, text); - } finally { - calloc.free(text); - } - if (payload == null || payload.isEmpty) return; - final bytes = calloc(payload.length); - try { - bytes.asTypedList(payload.length).setAll(0, payload); - _errorSetPayload(outErr, bytes, payload.length); - } finally { - calloc.free(bytes); - } -} - -/// Reports an implementation's exception that isn't a domain error of the -/// method with the foreign code (-4) and the exception's text. -void _foreignError(Pointer<_Error> outErr, Object error) { - try { - _failCallback(outErr, NativeException.foreignCode, '$error', null); - } catch (_) { - _errorSet(outErr, NativeException.foreignCode, nullptr); - } -} - -/// Hands [bytes] to the producer through a method's `out_ptr`/`out_len` -/// slots as a run allocated with `edge_cases_alloc`, which the producer -/// adopts and frees. An empty run is null with length 0. -void _handOver( - List bytes, Pointer> outPtr, Pointer outLen) { - final run = bytes.isEmpty ? nullptr : _alloc(bytes.length); - if (run != nullptr) run.asTypedList(bytes.length).setAll(0, bytes); - outPtr.value = run; - outLen.value = bytes.length; -} - -// ── Iterators ── - -/// Anchors one live native iterator for its GC-finalizer backstop. A -/// suspended `sync*` body keeps its anchor reachable; abandoning the -/// iteration drops the body, and the finalizer destroys the native iterator. -/// An iteration that runs to completion (or fails) detaches and destroys it -/// eagerly instead, so the iterator is destroyed exactly once either way. -final class _IteratorAnchor implements Finalizable {} - -/// The `out_item` slot iterator steps write, wide enough for any element. -final Pointer _outItem = calloc(); - -void _packListOfI32(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeI32(e)); - -List _unpackListOfI32(_BufferReader r) => - r.readList(() => r.readI32()); - -void _packListOfLeaf(_BufferWriter w, List v) => - w.writeList(v, (e) => _packLeaf(w, e)); - -List _unpackListOfLeaf(_BufferReader r) => - r.readList(() => _unpackLeaf(r)); - -void _packListOfListOfI32(_BufferWriter w, List> v) => - w.writeList(v, (e) => _packListOfI32(w, e)); - -List> _unpackListOfListOfI32(_BufferReader r) => - r.readList(() => _unpackListOfI32(r)); - -void _packListOfMode(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeI32(e.value)); - -List _unpackListOfMode(_BufferReader r) => - r.readList(() => Mode.fromValue(r.readI32())); - -void _packListOfOptionalOfI64(_BufferWriter w, List v) => - w.writeList(v, (e) => _packOptionalOfI64(w, e)); - -List _unpackListOfOptionalOfI64(_BufferReader r) => - r.readList(() => _unpackOptionalOfI64(r)); - -void _packListOfOptionalOfOuter(_BufferWriter w, List v) => - w.writeList(v, (e) => _packOptionalOfOuter(w, e)); - -List _unpackListOfOptionalOfOuter(_BufferReader r) => - r.readList(() => _unpackOptionalOfOuter(r)); - -void _packListOfOptionalOfWidget(_BufferWriter w, List v) => - w.writeList(v, (e) => _packOptionalOfWidget(w, e)); - -List _unpackListOfOptionalOfWidget(_BufferReader r) => - r.readList(() => _unpackOptionalOfWidget(r)); - -void _packListOfOuter(_BufferWriter w, List v) => - w.writeList(v, (e) => _packOuter(w, e)); - -List _unpackListOfOuter(_BufferReader r) => - r.readList(() => _unpackOuter(r)); - -void _packListOfString(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeString(e)); - -List _unpackListOfString(_BufferReader r) => - r.readList(() => r.readString()); - -void _packListOfWidget(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeU64(e._cloneRef().address)); - -List _unpackListOfWidget(_BufferReader r) => - r.readList(() => Widget._(Pointer.fromAddress(r.readU64()))); - -void _packMapOfI64ToString(_BufferWriter w, Map v) => - w.writeMap(v, (k) => w.writeI64(k), (e) => w.writeString(e)); - -Map _unpackMapOfI64ToString(_BufferReader r) => - r.readMap(() => r.readI64(), () => r.readString()); - -void _packMapOfStringToInner(_BufferWriter w, Map v) => - w.writeMap(v, (k) => w.writeString(k), (e) => _packInner(w, e)); - -Map _unpackMapOfStringToInner(_BufferReader r) => - r.readMap(() => r.readString(), () => _unpackInner(r)); - -void _packMapOfStringToWidget(_BufferWriter w, Map v) => - w.writeMap(v, (k) => w.writeString(k), (e) => w.writeU64(e._cloneRef().address)); - -Map _unpackMapOfStringToWidget(_BufferReader r) => - r.readMap(() => r.readString(), () => Widget._(Pointer.fromAddress(r.readU64()))); - -void _packOptionalOfBytes(_BufferWriter w, List? v) => - w.writeOptional(v, (e) => w.writeBytes(e)); - -List? _unpackOptionalOfBytes(_BufferReader r) => - r.readOptional(() => r.readBytes()); - -void _packOptionalOfI32(_BufferWriter w, int? v) => - w.writeOptional(v, (e) => w.writeI32(e)); - -int? _unpackOptionalOfI32(_BufferReader r) => - r.readOptional(() => r.readI32()); - -void _packOptionalOfI64(_BufferWriter w, int? v) => - w.writeOptional(v, (e) => w.writeI64(e)); - -int? _unpackOptionalOfI64(_BufferReader r) => - r.readOptional(() => r.readI64()); - -void _packOptionalOfInner(_BufferWriter w, Inner? v) => - w.writeOptional(v, (e) => _packInner(w, e)); - -Inner? _unpackOptionalOfInner(_BufferReader r) => - r.readOptional(() => _unpackInner(r)); - -void _packOptionalOfListOfOptionalOfOuter(_BufferWriter w, List? v) => - w.writeOptional(v, (e) => _packListOfOptionalOfOuter(w, e)); - -List? _unpackOptionalOfListOfOptionalOfOuter(_BufferReader r) => - r.readOptional(() => _unpackListOfOptionalOfOuter(r)); - -void _packOptionalOfListOfString(_BufferWriter w, List? v) => - w.writeOptional(v, (e) => _packListOfString(w, e)); - -List? _unpackOptionalOfListOfString(_BufferReader r) => - r.readOptional(() => _unpackListOfString(r)); - -void _packOptionalOfMode(_BufferWriter w, Mode? v) => - w.writeOptional(v, (e) => w.writeI32(e.value)); - -Mode? _unpackOptionalOfMode(_BufferReader r) => - r.readOptional(() => Mode.fromValue(r.readI32())); - -void _packOptionalOfOuter(_BufferWriter w, Outer? v) => - w.writeOptional(v, (e) => _packOuter(w, e)); - -Outer? _unpackOptionalOfOuter(_BufferReader r) => - r.readOptional(() => _unpackOuter(r)); - -void _packOptionalOfString(_BufferWriter w, String? v) => - w.writeOptional(v, (e) => w.writeString(e)); - -String? _unpackOptionalOfString(_BufferReader r) => - r.readOptional(() => r.readString()); - -void _packOptionalOfWidget(_BufferWriter w, Widget? v) => - w.writeOptional(v, (e) => w.writeU64(e._cloneRef().address)); - -Widget? _unpackOptionalOfWidget(_BufferReader r) => - r.readOptional(() => Widget._(Pointer.fromAddress(r.readU64()))); - -/// The `EdgeErrors` error domain of module `edge_cases`: catch it to handle any of -/// its codes, or a subclass for one code. -sealed class EdgeErrorsException extends NativeException { - /// Creates a domain error carrying [code] and [message]. - EdgeErrorsException(super.code, super.message); -} - -/// Resource is busy -final class BusyException extends EdgeErrorsException { - /// Creates a [BusyException] (code 7). - BusyException([String message = 'Resource is busy']) : super(7, message); -} - -NativeException _mapEdgeErrorsException(int code, String message, Uint8List payload) { - switch (code) { - case 7: - return BusyException(message); - default: - return _runtimeException(code, message, payload); - } -} - -/// Deprecated enum with non-contiguous values and keyword-like variants -@Deprecated('Use Mode instead') -enum Legacy { - none(0), - default_(5), - class_(10); - - const Legacy(this.value); - - /// The C discriminant. - final int value; - - /// The variant whose discriminant is [value]. - static Legacy fromValue(int value) => values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError.value(value, 'value', 'not a Legacy discriminant')); -} - -enum Mode { - fast(1), - safe(2); - - const Mode(this.value); - - /// The C discriminant. - final int value; - - /// The variant whose discriminant is [value]. - static Mode fromValue(int value) => values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError.value(value, 'value', 'not a Mode discriminant')); -} - -final class Leaf { - /// Creates a [Leaf]. - Leaf({required this.weight}); - - final double weight; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Leaf && - weight == other.weight; - - @override - int get hashCode => Object.hashAll([Leaf, weight]); - - @override - String toString() => 'Leaf(weight: $weight)'; -} - -void _packLeaf(_BufferWriter w, Leaf v) { - w.writeF32(v.weight); -} - -Leaf _unpackLeaf(_BufferReader r) => Leaf( - weight: r.readF32(), - ); - -/// Middle layer of a three-deep struct nesting -final class Inner { - /// Creates an [Inner]. - Inner({required this.leaf, required this.leaves}); - - final Leaf leaf; - final List leaves; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Inner && - leaf == other.leaf && - _deepEquals(leaves, other.leaves); - - @override - int get hashCode => Object.hashAll([Inner, leaf, _deepHash(leaves)]); - - @override - String toString() => 'Inner(leaf: $leaf, leaves: $leaves)'; -} - -void _packInner(_BufferWriter w, Inner v) { - _packLeaf(w, v.leaf); - _packListOfLeaf(w, v.leaves); -} - -Inner _unpackInner(_BufferReader r) => Inner( - leaf: _unpackLeaf(r), - leaves: _unpackListOfLeaf(r), - ); - -final class Outer { - /// Creates an [Outer]. - Outer({required this.inner, this.maybeInner, required this.byName, required this.modes, required this.matrix, required this.sparse, this.blob, this.note, required this.byId, required this.hidden, required this.v2}); - - final Inner inner; - final Inner? maybeInner; - final Map byName; - final List modes; - final List> matrix; - final List sparse; - final List? blob; - final String? note; - final Map byId; - final bool hidden; - final int v2; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Outer && - inner == other.inner && - maybeInner == other.maybeInner && - _deepEquals(byName, other.byName) && - _deepEquals(modes, other.modes) && - _deepEquals(matrix, other.matrix) && - _deepEquals(sparse, other.sparse) && - _deepEquals(blob, other.blob) && - note == other.note && - _deepEquals(byId, other.byId) && - hidden == other.hidden && - v2 == other.v2; - - @override - int get hashCode => Object.hashAll([Outer, inner, maybeInner, _deepHash(byName), _deepHash(modes), _deepHash(matrix), _deepHash(sparse), _deepHash(blob), note, _deepHash(byId), hidden, v2]); - - @override - String toString() => 'Outer(inner: $inner, maybeInner: $maybeInner, byName: $byName, modes: $modes, matrix: $matrix, sparse: $sparse, blob: $blob, note: $note, byId: $byId, hidden: $hidden, v2: $v2)'; -} - -void _packOuter(_BufferWriter w, Outer v) { - _packInner(w, v.inner); - _packOptionalOfInner(w, v.maybeInner); - _packMapOfStringToInner(w, v.byName); - _packListOfMode(w, v.modes); - _packListOfListOfI32(w, v.matrix); - _packListOfOptionalOfI64(w, v.sparse); - _packOptionalOfBytes(w, v.blob); - _packOptionalOfString(w, v.note); - _packMapOfI64ToString(w, v.byId); - w.writeBool(v.hidden); - w.writeU8(v.v2); -} - -Outer _unpackOuter(_BufferReader r) => Outer( - inner: _unpackInner(r), - maybeInner: _unpackOptionalOfInner(r), - byName: _unpackMapOfStringToInner(r), - modes: _unpackListOfMode(r), - matrix: _unpackListOfListOfI32(r), - sparse: _unpackListOfOptionalOfI64(r), - blob: _unpackOptionalOfBytes(r), - note: _unpackOptionalOfString(r), - byId: _unpackMapOfI64ToString(r), - hidden: r.readBool(), - v2: r.readU8(), - ); - -/// Every field is a reserved word somewhere -final class Keywords { - /// Creates a [Keywords]. - Keywords({required this.class_, required this.def, required this.import, required this.from, required this.func, required this.range, required this.register, required this.volatile, required this.new_, required this.default_, required this.end, required this.in_, required this.is_, required this.int_, required this.var_, required this.val, required this.self, required this.this_, required this.object, required this.string, required this.case_, required this.select, required this.switch_, required this.void_, required this.union, required this.namespace, required this.operator, required this.template, required this.package, required this.protocol, required this.guard, required this.defer, required this.inout, required this.internal, required this.when, required this.fun, required this.data, required this.late, required this.part, required this.out, required this.ref, required this.event, required this.base, required this.lambda, required this.yield, required this.nil, required this.null_, required this.begin, required this.rescue, required this.module, required this.chan, required this.go, required this.let, required this.export, required this.delete, required this.typedef, required this.sizeof, required this.static, required this.super_}); - - final int class_; - final int def; - final int import; - final int from; - final int func; - final int range; - final int register; - final int volatile; - final int new_; - final int default_; - final int end; - final int in_; - final int is_; - final int int_; - final int var_; - final int val; - final int self; - final int this_; - final int object; - final int string; - final int case_; - final int select; - final int switch_; - final int void_; - final int union; - final int namespace; - final int operator; - final int template; - final int package; - final int protocol; - final int guard; - final int defer; - final int inout; - final int internal; - final int when; - final int fun; - final int data; - final int late; - final int part; - final int out; - final int ref; - final int event; - final int base; - final int lambda; - final int yield; - final int nil; - final int null_; - final int begin; - final int rescue; - final int module; - final int chan; - final int go; - final int let; - final int export; - final int delete; - final int typedef; - final int sizeof; - final int static; - final int super_; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Keywords && - class_ == other.class_ && - def == other.def && - import == other.import && - from == other.from && - func == other.func && - range == other.range && - register == other.register && - volatile == other.volatile && - new_ == other.new_ && - default_ == other.default_ && - end == other.end && - in_ == other.in_ && - is_ == other.is_ && - int_ == other.int_ && - var_ == other.var_ && - val == other.val && - self == other.self && - this_ == other.this_ && - object == other.object && - string == other.string && - case_ == other.case_ && - select == other.select && - switch_ == other.switch_ && - void_ == other.void_ && - union == other.union && - namespace == other.namespace && - operator == other.operator && - template == other.template && - package == other.package && - protocol == other.protocol && - guard == other.guard && - defer == other.defer && - inout == other.inout && - internal == other.internal && - when == other.when && - fun == other.fun && - data == other.data && - late == other.late && - part == other.part && - out == other.out && - ref == other.ref && - event == other.event && - base == other.base && - lambda == other.lambda && - yield == other.yield && - nil == other.nil && - null_ == other.null_ && - begin == other.begin && - rescue == other.rescue && - module == other.module && - chan == other.chan && - go == other.go && - let == other.let && - export == other.export && - delete == other.delete && - typedef == other.typedef && - sizeof == other.sizeof && - static == other.static && - super_ == other.super_; - - @override - int get hashCode => Object.hashAll([Keywords, class_, def, import, from, func, range, register, volatile, new_, default_, end, in_, is_, int_, var_, val, self, this_, object, string, case_, select, switch_, void_, union, namespace, operator, template, package, protocol, guard, defer, inout, internal, when, fun, data, late, part, out, ref, event, base, lambda, yield, nil, null_, begin, rescue, module, chan, go, let, export, delete, typedef, sizeof, static, super_]); - - @override - String toString() => 'Keywords(class_: $class_, def: $def, import: $import, from: $from, func: $func, range: $range, register: $register, volatile: $volatile, new_: $new_, default_: $default_, end: $end, in_: $in_, is_: $is_, int_: $int_, var_: $var_, val: $val, self: $self, this_: $this_, object: $object, string: $string, case_: $case_, select: $select, switch_: $switch_, void_: $void_, union: $union, namespace: $namespace, operator: $operator, template: $template, package: $package, protocol: $protocol, guard: $guard, defer: $defer, inout: $inout, internal: $internal, when: $when, fun: $fun, data: $data, late: $late, part: $part, out: $out, ref: $ref, event: $event, base: $base, lambda: $lambda, yield: $yield, nil: $nil, null_: $null_, begin: $begin, rescue: $rescue, module: $module, chan: $chan, go: $go, let: $let, export: $export, delete: $delete, typedef: $typedef, sizeof: $sizeof, static: $static, super_: $super_)'; -} - -void _packKeywords(_BufferWriter w, Keywords v) { - w.writeI32(v.class_); - w.writeI32(v.def); - w.writeI32(v.import); - w.writeI32(v.from); - w.writeI32(v.func); - w.writeI32(v.range); - w.writeI32(v.register); - w.writeI32(v.volatile); - w.writeI32(v.new_); - w.writeI32(v.default_); - w.writeI32(v.end); - w.writeI32(v.in_); - w.writeI32(v.is_); - w.writeI32(v.int_); - w.writeI32(v.var_); - w.writeI32(v.val); - w.writeI32(v.self); - w.writeI32(v.this_); - w.writeI32(v.object); - w.writeI32(v.string); - w.writeI32(v.case_); - w.writeI32(v.select); - w.writeI32(v.switch_); - w.writeI32(v.void_); - w.writeI32(v.union); - w.writeI32(v.namespace); - w.writeI32(v.operator); - w.writeI32(v.template); - w.writeI32(v.package); - w.writeI32(v.protocol); - w.writeI32(v.guard); - w.writeI32(v.defer); - w.writeI32(v.inout); - w.writeI32(v.internal); - w.writeI32(v.when); - w.writeI32(v.fun); - w.writeI32(v.data); - w.writeI32(v.late); - w.writeI32(v.part); - w.writeI32(v.out); - w.writeI32(v.ref); - w.writeI32(v.event); - w.writeI32(v.base); - w.writeI32(v.lambda); - w.writeI32(v.yield); - w.writeI32(v.nil); - w.writeI32(v.null_); - w.writeI32(v.begin); - w.writeI32(v.rescue); - w.writeI32(v.module); - w.writeI32(v.chan); - w.writeI32(v.go); - w.writeI32(v.let); - w.writeI32(v.export); - w.writeI32(v.delete); - w.writeI32(v.typedef); - w.writeI32(v.sizeof); - w.writeI32(v.static); - w.writeI32(v.super_); -} - -Keywords _unpackKeywords(_BufferReader r) => Keywords( - class_: r.readI32(), - def: r.readI32(), - import: r.readI32(), - from: r.readI32(), - func: r.readI32(), - range: r.readI32(), - register: r.readI32(), - volatile: r.readI32(), - new_: r.readI32(), - default_: r.readI32(), - end: r.readI32(), - in_: r.readI32(), - is_: r.readI32(), - int_: r.readI32(), - var_: r.readI32(), - val: r.readI32(), - self: r.readI32(), - this_: r.readI32(), - object: r.readI32(), - string: r.readI32(), - case_: r.readI32(), - select: r.readI32(), - switch_: r.readI32(), - void_: r.readI32(), - union: r.readI32(), - namespace: r.readI32(), - operator: r.readI32(), - template: r.readI32(), - package: r.readI32(), - protocol: r.readI32(), - guard: r.readI32(), - defer: r.readI32(), - inout: r.readI32(), - internal: r.readI32(), - when: r.readI32(), - fun: r.readI32(), - data: r.readI32(), - late: r.readI32(), - part: r.readI32(), - out: r.readI32(), - ref: r.readI32(), - event: r.readI32(), - base: r.readI32(), - lambda: r.readI32(), - yield: r.readI32(), - nil: r.readI32(), - null_: r.readI32(), - begin: r.readI32(), - rescue: r.readI32(), - module: r.readI32(), - chan: r.readI32(), - go: r.readI32(), - let: r.readI32(), - export: r.readI32(), - delete: r.readI32(), - typedef: r.readI32(), - sizeof: r.readI32(), - static: r.readI32(), - super_: r.readI32(), - ); - -/// Deprecated struct -@Deprecated('Use Outer instead') -final class Retired { - /// Creates a [Retired]. - Retired({required this.id}); - - final int id; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Retired && - id == other.id; - - @override - int get hashCode => Object.hashAll([Retired, id]); - - @override - String toString() => 'Retired(id: $id)'; -} - -void _packRetired(_BufferWriter w, Retired v) { - w.writeI64(v.id); -} - -Retired _unpackRetired(_BufferReader r) => Retired( - id: r.readI64(), - ); - -/// Record carrying objects in every buffered position -final class WidgetHolder { - /// Creates a [WidgetHolder]. - WidgetHolder({required this.primary, this.fallback, required this.extras, required this.byName}); - - final Widget primary; - final Widget? fallback; - final List extras; - final Map byName; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is WidgetHolder && - primary == other.primary && - fallback == other.fallback && - _deepEquals(extras, other.extras) && - _deepEquals(byName, other.byName); - - @override - int get hashCode => Object.hashAll([WidgetHolder, primary, fallback, _deepHash(extras), _deepHash(byName)]); - - @override - String toString() => 'WidgetHolder(primary: $primary, fallback: $fallback, extras: $extras, byName: $byName)'; -} - -void _packWidgetHolder(_BufferWriter w, WidgetHolder v) { - w.writeU64(v.primary._cloneRef().address); - _packOptionalOfWidget(w, v.fallback); - _packListOfWidget(w, v.extras); - _packMapOfStringToWidget(w, v.byName); -} - -WidgetHolder _unpackWidgetHolder(_BufferReader r) => WidgetHolder( - primary: Widget._(Pointer.fromAddress(r.readU64())), - fallback: _unpackOptionalOfWidget(r), - extras: _unpackListOfWidget(r), - byName: _unpackMapOfStringToWidget(r), - ); - -/// Callback interface carrying composite payloads and objects -/// -/// A callback interface: implement it and pass an instance to any -/// function taking a [BatchListener]. The library keeps the instance alive -/// until the producer releases it. -/// -/// Methods that return a value run synchronously and must be called by -/// the producer on this isolate's thread during a call from Dart; a -/// thrown exception fails that call with [NativeException.foreignCode] -/// (or, from a method that throws, the domain exception's code and -/// fields). Void methods may be called from any thread: they're -/// delivered asynchronously on this isolate's event loop, in the zone -/// that passed the instance, where a thrown exception is an uncaught -/// error of that zone. Object arguments are owned by the -/// implementation. -abstract class BatchListener { - /// Composite payloads cross as value buffers - void onBatch(List items, List raw, Outer? first, Mode mode); - - /// Objects cross as strong references the consumer must release - void onWidget(Widget widget, Widget? fallback, List children); - - void onTick(); - - /// Scalar return travels back through the vtable - bool shouldContinue(int class_); - - /// Enum return travels back as its integer discriminant - Mode pickMode(); -} - -typedef _EdgeCasesEdgeCasesBatchListenerOnBatch = Void Function(Pointer, Pointer, Size, Pointer, Size, Pointer, Size, Int32, Pointer<_Error>); -typedef _EdgeCasesEdgeCasesBatchListenerOnWidget = Void Function(Pointer, Pointer, Pointer, Pointer, Size, Pointer<_Error>); -typedef _EdgeCasesEdgeCasesBatchListenerOnTick = Void Function(Pointer, Pointer<_Error>); -typedef _EdgeCasesEdgeCasesBatchListenerShouldContinue = Bool Function(Pointer, Int32, Pointer<_Error>); -typedef _EdgeCasesEdgeCasesBatchListenerPickMode = Int32 Function(Pointer, Pointer<_Error>); - -// The C `edge_cases_edge_cases_BatchListener_vtable` layout. -final class _BatchListenerVtable extends Struct { - @Uint32() - external int size; - @Uint32() - external int flags; - external Pointer)>> free; - external Pointer> onBatch; - external Pointer> onWidget; - external Pointer> onTick; - external Pointer> shouldContinue; - external Pointer> pickMode; -} - -// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.on_batch`: forwarded to the isolate. -void _batchListenerOnBatch(Pointer ctx, Pointer itemsPtr, int itemsLen, Pointer rawPtr, int rawLen, Pointer firstPtr, int firstLen, int mode, Pointer<_Error> outErr) { - _CallbackMessage(ctx, 0, 4) - ..bytes(itemsPtr, itemsLen) - ..bytes(rawPtr, rawLen) - ..bytes(firstPtr, firstLen) - ..int64(mode) - ..send(); -} - -// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.on_widget`: forwarded to the isolate. -void _batchListenerOnWidget(Pointer ctx, Pointer widget, Pointer fallback, Pointer childrenPtr, int childrenLen, Pointer<_Error> outErr) { - _CallbackMessage(ctx, 1, 3) - ..pointer(widget) - ..pointer(fallback) - ..bytes(childrenPtr, childrenLen) - ..send(); -} - -// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.on_tick`: forwarded to the isolate. -void _batchListenerOnTick(Pointer ctx, Pointer<_Error> outErr) { - _CallbackMessage(ctx, 2, 0) - ..send(); -} - -// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.should_continue`. -bool _batchListenerShouldContinue(Pointer ctx, int class_, Pointer<_Error> outErr) { - try { - return (_callbackTarget(ctx) as BatchListener).shouldContinue(class_); - } catch (e) { - _foreignError(outErr, e); - } - return false; -} - -// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.pick_mode`. -int _batchListenerPickMode(Pointer ctx, Pointer<_Error> outErr) { - try { - return (_callbackTarget(ctx) as BatchListener).pickMode().value; - } catch (e) { - _foreignError(outErr, e); - } - return 0; -} - -void _batchListenerDispatch(Object impl, int method, List message) { - final target = impl as BatchListener; - switch (method) { - case 0: { - final a2 = _decode(message[2]! as Uint8List, _unpackListOfOuter); - final a3 = message[3]! as Uint8List; - final a4 = _decode(message[4]! as Uint8List, _unpackOptionalOfOuter); - final a5 = Mode.fromValue(message[5]! as int); - target.onBatch(a2, a3, a4, a5); - } - case 1: { - final a2Address = message[2]! as int; - final a2 = Widget._(Pointer.fromAddress(a2Address)); - final a3Address = message[3]! as int; - final a3 = a3Address == 0 ? null : Widget._(Pointer.fromAddress(a3Address)); - final a4 = _decode(message[4]! as Uint8List, _unpackListOfWidget); - target.onWidget(a2, a3, a4); - } - case 2: { - target.onTick(); - } - } -} - -final Pointer<_BatchListenerVtable> _batchListenerVtable = () { - final vtable = calloc<_BatchListenerVtable>(); - vtable.ref - ..size = sizeOf<_BatchListenerVtable>() - ..flags = 0 - ..free = _callbackFree - ..onBatch = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerOnBatch>.isolateGroupBound(_batchListenerOnBatch)) - ..onWidget = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerOnWidget>.isolateGroupBound(_batchListenerOnWidget)) - ..onTick = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerOnTick>.isolateGroupBound(_batchListenerOnTick)) - ..shouldContinue = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerShouldContinue>.isolateLocal(_batchListenerShouldContinue, exceptionalReturn: false)) - ..pickMode = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerPickMode>.isolateLocal(_batchListenerPickMode, exceptionalReturn: 0)); - return vtable; -}(); - -/// Callback interface whose name and methods are keywords -/// -/// A callback interface: implement it and pass an instance to any -/// function taking a [Keyword]. The library keeps the instance alive -/// until the producer releases it. -/// -/// Methods that return a value run synchronously and must be called by -/// the producer on this isolate's thread during a call from Dart; a -/// thrown exception fails that call with [NativeException.foreignCode] -/// (or, from a method that throws, the domain exception's code and -/// fields). Void methods may be called from any thread: they're -/// delivered asynchronously on this isolate's event loop, in the zone -/// that passed the instance, where a thrown exception is an uncaught -/// error of that zone. Object arguments are owned by the -/// implementation. -abstract class Keyword { - void delete(int self); - - int import(); -} - -typedef _EdgeCasesEdgeCasesKeywordDelete = Void Function(Pointer, Int32, Pointer<_Error>); -typedef _EdgeCasesEdgeCasesKeywordImport = Int32 Function(Pointer, Pointer<_Error>); - -// The C `edge_cases_edge_cases_Keyword_vtable` layout. -final class _KeywordVtable extends Struct { - @Uint32() - external int size; - @Uint32() - external int flags; - external Pointer)>> free; - external Pointer> delete; - external Pointer> import; -} - -// Vtable entry `edge_cases_edge_cases_Keyword_vtable.delete`: forwarded to the isolate. -void _keywordDelete(Pointer ctx, int self, Pointer<_Error> outErr) { - _CallbackMessage(ctx, 0, 1) - ..int64(self) - ..send(); -} - -// Vtable entry `edge_cases_edge_cases_Keyword_vtable.import`. -int _keywordImport(Pointer ctx, Pointer<_Error> outErr) { - try { - return (_callbackTarget(ctx) as Keyword).import(); - } catch (e) { - _foreignError(outErr, e); - } - return 0; -} - -void _keywordDispatch(Object impl, int method, List message) { - final target = impl as Keyword; - switch (method) { - case 0: { - final a2 = message[2]! as int; - target.delete(a2); - } - } -} - -final Pointer<_KeywordVtable> _keywordVtable = () { - final vtable = calloc<_KeywordVtable>(); - vtable.ref - ..size = sizeOf<_KeywordVtable>() - ..flags = 0 - ..free = _callbackFree - ..delete = _pin(NativeCallable<_EdgeCasesEdgeCasesKeywordDelete>.isolateGroupBound(_keywordDelete)) - ..import = _pin(NativeCallable<_EdgeCasesEdgeCasesKeywordImport>.isolateLocal(_keywordImport, exceptionalReturn: 0)); - return vtable; -}(); - -final _edgeCasesEdgeCasesWidgetClone = _lib.lookupFunction< - Pointer Function(Pointer), - Pointer Function(Pointer)>('edge_cases_edge_cases_Widget_clone'); - -final _edgeCasesEdgeCasesWidgetDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('edge_cases_edge_cases_Widget_destroy'); -final _edgeCasesEdgeCasesWidgetDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('edge_cases_edge_cases_Widget_destroy')); - -final _edgeCasesEdgeCasesWidgetRefresh = _lib.lookupFunction< - Void Function(Pointer, Pointer<_Error>), - void Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_refresh'); - -final _edgeCasesEdgeCasesWidgetRename = _lib.lookupFunction< - Pointer Function(Pointer, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_rename'); - -typedef _EdgeCasesEdgeCasesWidgetFetchCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _edgeCasesEdgeCasesWidgetFetch = _lib.lookupFunction< - Void Function(Pointer, Int64, Pointer>, Pointer), - void Function(Pointer, int, Pointer>, Pointer)>('edge_cases_edge_cases_Widget_fetch'); - -final _edgeCasesEdgeCasesWidgetOldName = _lib.lookupFunction< - Pointer Function(Pointer, Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_old_name'); - -final _edgeCasesEdgeCasesWidgetClose = _lib.lookupFunction< - Bool Function(Pointer, Pointer<_Error>), - bool Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_close'); - -final _edgeCasesEdgeCasesWidgetDispose = _lib.lookupFunction< - Void Function(Pointer, Bool, Pointer<_Error>), - void Function(Pointer, bool, Pointer<_Error>)>('edge_cases_edge_cases_Widget_dispose'); - -/// Interface with no constructor; instances come from make_widget -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, when an undisposed instance is -/// collected, by a finalizer. -final class Widget extends _NativeObject { - Widget._(super._ptr); - - @override - NativeFinalizer get _finalizer => _edgeCasesEdgeCasesWidgetDestroyFinalizer; - - @override - void _destroy(Pointer ptr) => _edgeCasesEdgeCasesWidgetDestroy(ptr); - - @override - Pointer _clone(Pointer ptr) => _edgeCasesEdgeCasesWidgetClone(ptr); - - /// Void method that can fail - /// - /// Throws [EdgeErrorsException] on domain errors. - void refresh() { - final _self = _enter(); - try { - _edgeCasesEdgeCasesWidgetRefresh(_self, _err); - _check(_err, _mapEdgeErrorsException); - } finally { - _leave(); - } - } - - String rename(String class_, String? import) { - final _self = _enter(); - final _arena = Arena(); - try { - final _class_Bytes = utf8.encode(class_); - final _importBytes = _encode(import, _packOptionalOfString); - final _result = _edgeCasesEdgeCasesWidgetRename(_self, _stage(_arena, _class_Bytes), _class_Bytes.length, _stage(_arena, _importBytes), _importBytes.length, _outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } finally { - _arena.releaseAll(); - _leave(); - } - } - - /// Async method with a struct result - /// - /// Throws [EdgeErrorsException] on domain errors. - Future fetch(int id) { - final _self = _enter(); - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesWidgetFetchCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesWidgetFetchCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _mapEdgeErrorsException); - _completer.complete(_decode(_takeBytes(value, valueLen), _unpackOuter)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesWidgetFetch(_self, id, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } finally { - _leave(); - } - return _completer.future; - } - - @Deprecated('Use rename instead') - String oldName() { - final _self = _enter(); - try { - final _result = _edgeCasesEdgeCasesWidgetOldName(_self, _outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } finally { - _leave(); - } - } - - /// Spelled like the release method of several wrappers - bool close() { - final _self = _enter(); - try { - final _result = _edgeCasesEdgeCasesWidgetClose(_self, _err); - _check(_err); - return _result; - } finally { - _leave(); - } - } - - /// Spelled like the release method of the Dart and .NET wrappers - void dispose_(bool force) { - final _self = _enter(); - try { - _edgeCasesEdgeCasesWidgetDispose(_self, force, _err); - _check(_err); - } finally { - _leave(); - } - } -} - -final _edgeCasesEdgeCasesVaultClone = _lib.lookupFunction< - Pointer Function(Pointer), - Pointer Function(Pointer)>('edge_cases_edge_cases_Vault_clone'); - -final _edgeCasesEdgeCasesVaultDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('edge_cases_edge_cases_Vault_destroy'); -final _edgeCasesEdgeCasesVaultDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('edge_cases_edge_cases_Vault_destroy')); - -final _edgeCasesEdgeCasesVaultOpen = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer<_Error>)>('edge_cases_edge_cases_Vault_open'); - -final _edgeCasesEdgeCasesVaultPeek = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Vault_peek'); - -final _edgeCasesEdgeCasesVaultHandle = _lib.lookupFunction< - Int64 Function(Pointer, Pointer<_Error>), - int Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Vault_handle'); - -final _edgeCasesEdgeCasesVaultProbe = _lib.lookupFunction< - Bool Function(Pointer<_Error>), - bool Function(Pointer<_Error>)>('edge_cases_edge_cases_Vault_probe'); - -final _edgeCasesEdgeCasesVaultToString = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Vault_to_string'); - -/// Deprecated interface with a throwing constructor -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, when an undisposed instance is -/// collected, by a finalizer. -@Deprecated('Use Widget instead') -final class Vault extends _NativeObject { - Vault._(super._ptr); - - @override - NativeFinalizer get _finalizer => _edgeCasesEdgeCasesVaultDestroyFinalizer; - - @override - void _destroy(Pointer ptr) => _edgeCasesEdgeCasesVaultDestroy(ptr); - - @override - Pointer _clone(Pointer ptr) => _edgeCasesEdgeCasesVaultClone(ptr); - - /// Fails when the vault is busy - /// - /// The returned object is owned by the caller: call its `dispose()` - /// when you're done with it. - /// - /// Throws [EdgeErrorsException] on domain errors. - factory Vault.open(String path) { - final _arena = Arena(); - try { - final _pathBytes = utf8.encode(path); - final _result = _edgeCasesEdgeCasesVaultOpen(_stage(_arena, _pathBytes), _pathBytes.length, _err); - _check(_err, _mapEdgeErrorsException); - return Vault._(_result); - } finally { - _arena.releaseAll(); - } - } - - /// Returns null when the producer reports no object. A returned object - /// is owned by the caller: call its `dispose()` when you're done with it. - Widget? peek() { - final _self = _enter(); - try { - final _result = _edgeCasesEdgeCasesVaultPeek(_self, _err); - _check(_err); - return _result == nullptr ? null : Widget._(_result); - } finally { - _leave(); - } - } - - /// Spelled like the handle accessor of several wrappers - int handle() { - final _self = _enter(); - try { - final _result = _edgeCasesEdgeCasesVaultHandle(_self, _err); - _check(_err); - return _result; - } finally { - _leave(); - } - } - - /// Throwing static - /// - /// Throws [EdgeErrorsException] on domain errors. - static bool probe() { - final _result = _edgeCasesEdgeCasesVaultProbe(_err); - _check(_err, _mapEdgeErrorsException); - return _result; - } - - /// Static spelled like an inherited object member - static String toString_() { - final _result = _edgeCasesEdgeCasesVaultToString(_outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } -} - -final _edgeCasesEdgeCasesNoop = _lib.lookupFunction< - Void Function(Pointer<_Error>), - void Function(Pointer<_Error>)>('edge_cases_edge_cases_noop'); - -/// No parameters and no result -void noop() { - _edgeCasesEdgeCasesNoop(_err); - _check(_err); -} - -final _edgeCasesEdgeCasesSelect = _lib.lookupFunction< - Int32 Function(Int32, Pointer, Size, Pointer, Size, Pointer, Size, Bool, Pointer<_Error>), - int Function(int, Pointer, int, Pointer, int, Pointer, int, bool, Pointer<_Error>)>('edge_cases_edge_cases_select'); - -/// Function named after a keyword whose parameters are all keywords -int select(int class_, String from, List func, int? range, bool type) { - final _arena = Arena(); - try { - final _fromBytes = utf8.encode(from); - final _funcBytes = _encode(func, _packListOfString); - final _rangeBytes = _encode(range, _packOptionalOfI32); - final _result = _edgeCasesEdgeCasesSelect(class_, _stage(_arena, _fromBytes), _fromBytes.length, _stage(_arena, _funcBytes), _funcBytes.length, _stage(_arena, _rangeBytes), _rangeBytes.length, type, _err); - _check(_err); - return _result; - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesNestedOptional = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_nested_optional'); - -/// Optional list of optional structs, the deepest legal nesting -List? nestedOptional(List? items) { - final _arena = Arena(); - try { - final _itemsBytes = _encode(items, _packOptionalOfListOfOptionalOfOuter); - final _result = _edgeCasesEdgeCasesNestedOptional(_stage(_arena, _itemsBytes), _itemsBytes.length, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackOptionalOfListOfOptionalOfOuter); - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesOptionalParams = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_optional_params'); - -Mode? optionalParams(String? text, int? count, Outer? outer, List? tags, List? blob, Mode? mode) { - final _arena = Arena(); - try { - final _textBytes = _encode(text, _packOptionalOfString); - final _countBytes = _encode(count, _packOptionalOfI32); - final _outerBytes = _encode(outer, _packOptionalOfOuter); - final _tagsBytes = _encode(tags, _packOptionalOfListOfString); - final _blobBytes = _encode(blob, _packOptionalOfBytes); - final _modeBytes = _encode(mode, _packOptionalOfMode); - final _result = _edgeCasesEdgeCasesOptionalParams(_stage(_arena, _textBytes), _textBytes.length, _stage(_arena, _countBytes), _countBytes.length, _stage(_arena, _outerBytes), _outerBytes.length, _stage(_arena, _tagsBytes), _tagsBytes.length, _stage(_arena, _blobBytes), _blobBytes.length, _stage(_arena, _modeBytes), _modeBytes.length, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackOptionalOfMode); - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesRegisterBatch = _lib.lookupFunction< - Void Function(Pointer, Pointer, Pointer, Pointer, Pointer<_Error>), - void Function(Pointer, Pointer, Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_register_batch'); - -/// Two callback interface parameters on one function -void registerBatch(BatchListener listener, Keyword keyword) { - final _listenerCtx = _registerCallback(listener, _batchListenerDispatch); - final _keywordCtx = _registerCallback(keyword, _keywordDispatch); - _edgeCasesEdgeCasesRegisterBatch(_listenerCtx, _batchListenerVtable.cast(), _keywordCtx, _keywordVtable.cast(), _err); - _check(_err); -} - -final _edgeCasesEdgeCasesWidgetsEverywhere = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_widgets_everywhere'); - -/// Objects in every buffered position -WidgetHolder widgetsEverywhere(List list, Map byName, List sparse, WidgetHolder holder) { - final _arena = Arena(); - try { - final _listBytes = _encode(list, _packListOfWidget); - final _byNameBytes = _encode(byName, _packMapOfStringToWidget); - final _sparseBytes = _encode(sparse, _packListOfOptionalOfWidget); - final _holderBytes = _encode(holder, _packWidgetHolder); - final _result = _edgeCasesEdgeCasesWidgetsEverywhere(_stage(_arena, _listBytes), _listBytes.length, _stage(_arena, _byNameBytes), _byNameBytes.length, _stage(_arena, _sparseBytes), _sparseBytes.length, _stage(_arena, _holderBytes), _holderBytes.length, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackWidgetHolder); - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesWidgetStream = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_widget_stream'); - -final _edgeCasesEdgeCasesWidgetStreamIteratorNext = _lib.lookupFunction< - Int32 Function(Pointer, Pointer>, Pointer<_Error>), - int Function(Pointer, Pointer>, Pointer<_Error>)>('edge_cases_edge_cases_WidgetStreamIterator_next'); - -final _edgeCasesEdgeCasesWidgetStreamIteratorDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('edge_cases_edge_cases_WidgetStreamIterator_destroy'); -final _edgeCasesEdgeCasesWidgetStreamIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('edge_cases_edge_cases_WidgetStreamIterator_destroy')); - -/// Iterator of objects -/// -/// Returns a lazy [Iterable]: each step pulls one element from a native -/// iterator, and iterating again starts a new one. The native iterator -/// is released when the iteration ends, fails, or is abandoned and -/// collected. -/// -/// Each element is owned by the caller: call its `dispose()` when -/// you're done with it. -Iterable widgetStream() sync* { - final Pointer _iter; - _iter = _edgeCasesEdgeCasesWidgetStream(_err); - _check(_err); - final _anchor = _IteratorAnchor(); - _edgeCasesEdgeCasesWidgetStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); - try { - while (true) { - final _more = _edgeCasesEdgeCasesWidgetStreamIteratorNext(_iter, _outItem.cast(), _err); - _check(_err); - if (_more == 0) break; - yield Widget._(_outItem.cast>().value); - } - } finally { - _edgeCasesEdgeCasesWidgetStreamIteratorDestroyFinalizer.detach(_anchor); - _edgeCasesEdgeCasesWidgetStreamIteratorDestroy(_iter); - } -} - -typedef _EdgeCasesEdgeCasesAsyncWidgetCallback = Void Function(Pointer, Pointer<_Error>, Pointer); - -final _edgeCasesEdgeCasesAsyncWidget = _lib.lookupFunction< - Void Function(Pointer>, Pointer), - void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_widget'); - -/// Async returning an object -/// -/// The returned object is owned by the caller: call its `dispose()` -/// when you're done with it. -/// -/// Throws [EdgeErrorsException] on domain errors. -Future asyncWidget() { - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncWidgetCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncWidgetCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _mapEdgeErrorsException); - _completer.complete(Widget._(value)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncWidget(_callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -final _edgeCasesEdgeCasesMakeWidget = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer<_Error>)>('edge_cases_edge_cases_make_widget'); - -/// Factory for an interface with no constructor -/// -/// The returned object is owned by the caller: call its `dispose()` -/// when you're done with it. -Widget makeWidget(String label) { - final _arena = Arena(); - try { - final _labelBytes = utf8.encode(label); - final _result = _edgeCasesEdgeCasesMakeWidget(_stage(_arena, _labelBytes), _labelBytes.length, _err); - _check(_err); - return Widget._(_result); - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesFindWidget = _lib.lookupFunction< - Pointer Function(Int64, Pointer<_Error>), - Pointer Function(int, Pointer<_Error>)>('edge_cases_edge_cases_find_widget'); - -/// Optional interface return -/// -/// Returns null when the producer reports no object. A returned object -/// is owned by the caller: call its `dispose()` when you're done with it. -/// -/// Throws [EdgeErrorsException] on domain errors. -Widget? findWidget(int id) { - final _result = _edgeCasesEdgeCasesFindWidget(id, _err); - _check(_err, _mapEdgeErrorsException); - return _result == nullptr ? null : Widget._(_result); -} - -final _edgeCasesEdgeCasesInspectWidget = _lib.lookupFunction< - Pointer Function(Pointer, Pointer, Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_inspect_widget'); - -/// Interface parameter, including an optional one -String inspectWidget(Widget widget, Widget? fallback) { - final _arena = Arena(); - try { - final _widgetPtr = _borrow(_arena, widget); - final _fallbackPtr = fallback == null ? nullptr : _borrow(_arena, fallback); - final _result = _edgeCasesEdgeCasesInspectWidget(_widgetPtr, _fallbackPtr, _outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesStringStream = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_string_stream'); - -final _edgeCasesEdgeCasesStringStreamIteratorNext = _lib.lookupFunction< - Int32 Function(Pointer, Pointer>, Pointer, Pointer<_Error>), - int Function(Pointer, Pointer>, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_StringStreamIterator_next'); - -final _edgeCasesEdgeCasesStringStreamIteratorDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('edge_cases_edge_cases_StringStreamIterator_destroy'); -final _edgeCasesEdgeCasesStringStreamIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('edge_cases_edge_cases_StringStreamIterator_destroy')); - -/// Iterator of strings -/// -/// Returns a lazy [Iterable]: each step pulls one element from a native -/// iterator, and iterating again starts a new one. The native iterator -/// is released when the iteration ends, fails, or is abandoned and -/// collected. -Iterable stringStream() sync* { - final Pointer _iter; - _iter = _edgeCasesEdgeCasesStringStream(_err); - _check(_err); - final _anchor = _IteratorAnchor(); - _edgeCasesEdgeCasesStringStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); - try { - while (true) { - final _more = _edgeCasesEdgeCasesStringStreamIteratorNext(_iter, _outItem.cast(), _outLen, _err); - _check(_err); - if (_more == 0) break; - yield _takeString(_outItem.cast>().value, _outLen.value); - } - } finally { - _edgeCasesEdgeCasesStringStreamIteratorDestroyFinalizer.detach(_anchor); - _edgeCasesEdgeCasesStringStreamIteratorDestroy(_iter); - } -} - -final _edgeCasesEdgeCasesScalarStream = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_scalar_stream'); - -final _edgeCasesEdgeCasesScalarStreamIteratorNext = _lib.lookupFunction< - Int32 Function(Pointer, Pointer, Pointer<_Error>), - int Function(Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_ScalarStreamIterator_next'); - -final _edgeCasesEdgeCasesScalarStreamIteratorDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('edge_cases_edge_cases_ScalarStreamIterator_destroy'); -final _edgeCasesEdgeCasesScalarStreamIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('edge_cases_edge_cases_ScalarStreamIterator_destroy')); - -/// Iterator of scalars -/// -/// Returns a lazy [Iterable]: each step pulls one element from a native -/// iterator, and iterating again starts a new one. The native iterator -/// is released when the iteration ends, fails, or is abandoned and -/// collected. -Iterable scalarStream() sync* { - final Pointer _iter; - _iter = _edgeCasesEdgeCasesScalarStream(_err); - _check(_err); - final _anchor = _IteratorAnchor(); - _edgeCasesEdgeCasesScalarStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); - try { - while (true) { - final _more = _edgeCasesEdgeCasesScalarStreamIteratorNext(_iter, _outItem.cast(), _err); - _check(_err); - if (_more == 0) break; - yield _outItem.cast().value; - } - } finally { - _edgeCasesEdgeCasesScalarStreamIteratorDestroyFinalizer.detach(_anchor); - _edgeCasesEdgeCasesScalarStreamIteratorDestroy(_iter); - } -} - -final _edgeCasesEdgeCasesModeStream = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_mode_stream'); - -final _edgeCasesEdgeCasesModeStreamIteratorNext = _lib.lookupFunction< - Int32 Function(Pointer, Pointer, Pointer<_Error>), - int Function(Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_ModeStreamIterator_next'); - -final _edgeCasesEdgeCasesModeStreamIteratorDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('edge_cases_edge_cases_ModeStreamIterator_destroy'); -final _edgeCasesEdgeCasesModeStreamIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('edge_cases_edge_cases_ModeStreamIterator_destroy')); - -/// Iterator of enums -/// -/// Returns a lazy [Iterable]: each step pulls one element from a native -/// iterator, and iterating again starts a new one. The native iterator -/// is released when the iteration ends, fails, or is abandoned and -/// collected. -Iterable modeStream() sync* { - final Pointer _iter; - _iter = _edgeCasesEdgeCasesModeStream(_err); - _check(_err); - final _anchor = _IteratorAnchor(); - _edgeCasesEdgeCasesModeStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); - try { - while (true) { - final _more = _edgeCasesEdgeCasesModeStreamIteratorNext(_iter, _outItem.cast(), _err); - _check(_err); - if (_more == 0) break; - yield Mode.fromValue(_outItem.cast().value); - } - } finally { - _edgeCasesEdgeCasesModeStreamIteratorDestroyFinalizer.detach(_anchor); - _edgeCasesEdgeCasesModeStreamIteratorDestroy(_iter); - } -} - -typedef _EdgeCasesEdgeCasesAsyncVoidCallback = Void Function(Pointer, Pointer<_Error>); - -final _edgeCasesEdgeCasesAsyncVoid = _lib.lookupFunction< - Void Function(Pointer>, Pointer), - void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_void'); - -/// Async with no result -Future asyncVoid() { - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncVoidCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncVoidCallback>.listener(( - Pointer _, Pointer<_Error> error) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncVoid(_callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -typedef _EdgeCasesEdgeCasesAsyncBytesCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _edgeCasesEdgeCasesAsyncBytes = _lib.lookupFunction< - Void Function(Uint32, Pointer>, Pointer), - void Function(int, Pointer>, Pointer)>('edge_cases_edge_cases_async_bytes'); - -Future> asyncBytes(int size) { - final _completer = Completer>(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncBytesCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncBytesCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(_takeBytes(value, valueLen)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncBytes(size, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -typedef _EdgeCasesEdgeCasesAsyncStructCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _edgeCasesEdgeCasesAsyncStruct = _lib.lookupFunction< - Void Function(Pointer>, Pointer), - void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_struct'); - -/// Throws [EdgeErrorsException] on domain errors. -Future asyncStruct() { - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncStructCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncStructCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _mapEdgeErrorsException); - _completer.complete(_decode(_takeBytes(value, valueLen), _unpackOuter)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncStruct(_callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -typedef _EdgeCasesEdgeCasesAsyncListCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _edgeCasesEdgeCasesAsyncList = _lib.lookupFunction< - Void Function(Pointer, Pointer>, Pointer), - void Function(Pointer, Pointer>, Pointer)>('edge_cases_edge_cases_async_list'); - -/// Cancelling [cancelToken] completes the returned future with a -/// [CancelledException]. -/// -/// Throws [EdgeErrorsException] on domain errors. -Future> asyncList({CancelToken? cancelToken}) { - final _completer = Completer>(); - final _cancel = _NativeCancel.bind(cancelToken); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncListCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncListCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - _cancel?.release(); - try { - if (error != nullptr) throw _takeAsyncError(error, _mapEdgeErrorsException); - _completer.complete(_decode(_takeBytes(value, valueLen), _unpackListOfOuter)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncList(_cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - _cancel?.release(); - rethrow; - } - return _completer.future; -} - -typedef _EdgeCasesEdgeCasesAsyncOptionalCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _edgeCasesEdgeCasesAsyncOptional = _lib.lookupFunction< - Void Function(Pointer>, Pointer), - void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_optional'); - -Future asyncOptional() { - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncOptionalCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncOptionalCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(_decode(_takeBytes(value, valueLen), _unpackOptionalOfI64)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncOptional(_callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -typedef _EdgeCasesEdgeCasesAsyncEnumCallback = Void Function(Pointer, Pointer<_Error>, Int32); - -final _edgeCasesEdgeCasesAsyncEnum = _lib.lookupFunction< - Void Function(Pointer>, Pointer), - void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_enum'); - -Future asyncEnum() { - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncEnumCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncEnumCallback>.listener(( - Pointer _, Pointer<_Error> error, int value) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(Mode.fromValue(value)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - _edgeCasesEdgeCasesAsyncEnum(_callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } - return _completer.future; -} - -typedef _EdgeCasesEdgeCasesAsyncKeywordsCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _edgeCasesEdgeCasesAsyncKeywords = _lib.lookupFunction< - Void Function(Pointer, Size, Int32, Pointer>, Pointer), - void Function(Pointer, int, int, Pointer>, Pointer)>('edge_cases_edge_cases_async_keywords'); - -Future asyncKeywords(String class_, int import) { - final _arena = Arena(); - final _completer = Completer(); - late final NativeCallable<_EdgeCasesEdgeCasesAsyncKeywordsCallback> _callback; - _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncKeywordsCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(_takeString(value, valueLen)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - final _class_Bytes = utf8.encode(class_); - _edgeCasesEdgeCasesAsyncKeywords(_stage(_arena, _class_Bytes), _class_Bytes.length, import, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } finally { - _arena.releaseAll(); - } - return _completer.future; -} - -final _edgeCasesEdgeCasesKeywordsRoundtrip = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_keywords_roundtrip'); - -Keywords keywordsRoundtrip(Keywords value) { - final _arena = Arena(); - try { - final _valueBytes = _encode(value, _packKeywords); - final _result = _edgeCasesEdgeCasesKeywordsRoundtrip(_stage(_arena, _valueBytes), _valueBytes.length, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackKeywords); - } finally { - _arena.releaseAll(); - } -} - -final _edgeCasesEdgeCasesLegacyValue = _lib.lookupFunction< - Pointer Function(Int32, Pointer, Pointer<_Error>), - Pointer Function(int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_legacy_value'); - -@Deprecated('Use Mode-based APIs') -Retired legacyValue(Legacy value) { - final _result = _edgeCasesEdgeCasesLegacyValue(value.value, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackRetired); -} - -// END edge_cases.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap deleted file mode 100644 index 7cf53204..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink (Dart) - -`dart:ffi` bindings for the `kitchen_sink` native library. - -## Usage - -Add this package as a dependency (a `path:` dependency works) and import it: - -```dart -import 'package:kitchen_sink/kitchen_sink.dart'; -``` - -The library loads `libkitchen_sink.dylib` (macOS), `libkitchen_sink.so` (Linux and Android), or -`kitchen_sink.dll` (Windows) from the platform's search path on first use. Set -`KITCHEN_SINK_LIBRARY` to a full path to load a specific build. On iOS, and as a macOS -fallback, the bindings look the symbols up in the running executable. - -## Objects and callbacks - -Each interface wrapper holds one native reference: call `dispose()` when -you're done, or let the garbage collector's finalizer release it. - -Callback-interface methods that return a value run synchronously and must -be called by the native library on the isolate's thread during a call from -Dart; void methods may be called from any thread and are delivered on the -event loop. - -## Requirements - -- Dart SDK `>=3.10.0 <4.0.0` -- The `ffi` package - - diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap index 02c7ded1..555106bc 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap @@ -5,7 +5,12 @@ source: crates/weaveffi-cli/tests/snapshots.rs // DO NOT EDIT. Your changes will be overwritten. // To regenerate: weaveffi generate -// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element +/// `dart:ffi` bindings for the `kitchen_sink` native library. +/// +/// The library loads on first use (set `KITCHEN_SINK_LIBRARY` to its path to pick a +/// build); a library that can't be loaded or doesn't match these +/// bindings throws a [NativeLibraryException]. +library; import 'dart:async'; import 'dart:convert'; @@ -16,1824 +21,19 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart'; -// ── Library loading ── -// The native library opens on first use and is verified before any other -// lookup: it must implement the C ABI revision these bindings were generated -// against and carry every declaration they were generated with, unchanged. - -/// The native library every binding in this file calls. -final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); - -/// The native library couldn't be loaded, or it doesn't match these -/// bindings: a different C ABI revision, or a declaration that's missing or -/// changed. Regenerate the bindings from the library you ship. -final class NativeLibraryError extends Error { - /// Creates a load failure described by [message]. - NativeLibraryError(this.message); - - /// What failed, naming the library or declaration. - final String message; - - @override - String toString() => 'NativeLibraryError: $message'; -} - -/// The platform file name of the library on the system search path. -String get _libraryFile => Platform.isMacOS - ? 'libkitchen_sink.dylib' - : Platform.isWindows - ? 'kitchen_sink.dll' - : 'libkitchen_sink.so'; - -DynamicLibrary _openLibrary() { - // An explicit path wins, so a caller can point at any build artifact. - final override = Platform.environment['KITCHEN_SINK_LIBRARY']; - if (override != null && override.isNotEmpty) { - return _open(override); - } - // iOS links native code into the app binary. - if (Platform.isIOS) return DynamicLibrary.process(); - try { - return DynamicLibrary.open(_libraryFile); - } on ArgumentError catch (e) { - // A macOS app may link the library into its executable instead. - if (Platform.isMacOS) return DynamicLibrary.process(); - throw NativeLibraryError("can't open '$_libraryFile' ($e); set " - 'KITCHEN_SINK_LIBRARY to its path'); - } -} - -DynamicLibrary _open(String path) { - try { - return DynamicLibrary.open(path); - } on ArgumentError catch (e) { - throw NativeLibraryError("can't open '$path': $e"); - } -} - -/// The C ABI revision these bindings were generated against. -const int _abiVersion = 4; - -/// One `kitchen_sink_contract_entry` of a module's contract table. -final class _ContractEntry extends Struct { - @Uint64() - external int id; - @Uint64() - external int hash; -} - -DynamicLibrary _verifyLibrary(DynamicLibrary lib) { - final int Function() abiVersion; - try { - abiVersion = lib.lookupFunction( - 'kitchen_sink_abi_version'); - } on ArgumentError { - throw NativeLibraryError( - "the native library 'kitchen_sink' isn't loaded; set KITCHEN_SINK_LIBRARY " - 'to its path'); - } - final found = abiVersion(); - if (found != _abiVersion) { - throw NativeLibraryError('kitchen_sink: these bindings expect C ABI revision ' - '$_abiVersion, but the loaded library implements revision $found'); - } - for (final (symbol, expected) in _contracts) { - _checkContract(lib, symbol, expected); - } - return lib; -} - -/// Checks that every `(id, hash, path)` entry in [expected] is in the table -/// [symbol] returns with an equal hash. Entries only the library has are -/// fine: they're declarations added after these bindings were generated. -void _checkContract( - DynamicLibrary lib, String symbol, List<(int, int, String)> expected) { - final Pointer<_ContractEntry> Function(Pointer) table; - try { - table = lib.lookupFunction Function(Pointer), - Pointer<_ContractEntry> Function(Pointer)>(symbol); - } on ArgumentError { - throw NativeLibraryError( - "kitchen_sink: the loaded library doesn't export '$symbol'"); - } - final hashes = {}; - final len = calloc(); - try { - final entries = table(len); - for (var i = 0; i < len.value; i++) { - hashes[entries[i].id] = entries[i].hash; - } - } finally { - calloc.free(len); - } - for (final (id, hash, path) in expected) { - final found = hashes[id]; - if (found == null) { - throw NativeLibraryError('kitchen_sink: $path is missing from the library'); - } - if (found != hash) { - throw NativeLibraryError( - 'kitchen_sink: $path changed since these bindings were generated'); - } - } -} - -/// Each top-level module's contract-table symbol and the declarations -/// these bindings were generated with: (id, signature hash, path). -const List<(String, List<(int, int, String)>)> _contracts = [ - ('kitchen_sink_shared_contract', [ - (0x709e234caa962446, 0x3894f2ca452e9f98, 'shared.ping'), - (0xcfb306fa5801ee5b, 0x1cbd9e10983cac2c, 'shared.Token'), - ]), - ('kitchen_sink_kitchen_contract', [ - (0x01d4f09bbddc986f, 0x29eb88fecc66544d, 'kitchen.Gadget.new'), - (0x10472d8fe748c2ee, 0xb265832b6a759fc4, 'kitchen.maybe_gadget'), - (0x11368eec07a74078, 0xfa049f78b77e146a, 'kitchen.gadgets'), - (0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, 'kitchen.stream_gadgets'), - (0x2111b54980826dd7, 0x452783f3e6ca6e1d, 'kitchen.do_cancellable'), - (0x29dddbc60bf8d7db, 0x452a3e573d57f6db, 'kitchen.Priority'), - (0x2b227003b74a0d16, 0x12332957470e9782, 'kitchen.maybe_subscribe'), - (0x2dd66cb0064f0494, 0x60197363fefd92dd, 'kitchen.Item'), - (0x33c265bbf079b0e9, 0xc3dcd0927e6c1deb, 'kitchen.u32_id'), - (0x33e6381d01660dfe, 0x077c63c31fd7f90a, 'kitchen.KitchenErrors'), - (0x3f5fc99d73504b76, 0x2df88d710f1ea10e, 'kitchen.Gadget.poke'), - (0x4c5764cf109528d1, 0x2a65529af4338a58, 'kitchen.maybe_item'), - (0x4de17dfb93fd494d, 0xba0ad0dd52ed74af, 'kitchen.i32_id'), - (0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, 'kitchen.stream_items'), - (0x68cdb9a4bd3e3dd5, 0xc678b9164348d497, 'kitchen.bool_id'), - (0x69e7cd703d16eb85, 0xf3b28dd22ae9e689, 'kitchen.do_async'), - (0x70865079ce41be82, 0xbe3b69464672af46, 'kitchen.Gadget.describe'), - (0x71c5a1116cb7c49a, 0xcba1837108a89b84, 'kitchen.echo_bytes'), - (0x735ee1e0049ee91a, 0xca74320bc53eddab, 'kitchen.echo_string'), - (0x76d7fc58f4e41d84, 0x7373a0e47d30e690, 'kitchen.i64_id'), - (0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, 'kitchen.nested.hello'), - (0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, 'kitchen.new_op'), - (0x89a5e664bc25c67b, 0x5764a95e81ecfd31, 'kitchen.f64_id'), - (0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, 'kitchen.Gadget.version'), - (0x974b9f3e823c1738, 0xb81b91e975695f27, 'kitchen.cross_module_token'), - (0xaca0655fac15d230, 0x11c4d2b28abe07bb, 'kitchen.list_items'), - (0xc883b4afae20543e, 0xc40560e5411ff392, 'kitchen.legacy_op'), - (0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, 'kitchen.get_attrs'), - (0xfae25d3391922ead, 0x1c7f95739f3210b8, 'kitchen.subscribe'), - (0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, 'kitchen.Gadget'), - (0xfed9c551fb9fd2d6, 0x59aec980ef3f3291, 'kitchen.ReadyListener'), - ]), -]; - -// ── Errors and memory ── - -/// The C `kitchen_sink_error` slot every fallible call writes. A non-zero -/// `code` carries a producer-owned `message` and, for a domain error with -/// fields, a value-buffer `payload`; `kitchen_sink_error_clear` releases both. -final class _Error extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -final _errorClear = _lib.lookupFunction), - void Function(Pointer<_Error>)>('kitchen_sink_error_clear', isLeaf: true); - -final _freeBytes = _lib.lookupFunction, Size), - void Function(Pointer, int)>('kitchen_sink_free_bytes', isLeaf: true); - -// Synchronous calls on this isolate share one error slot and one length -// slot: each call reads (and clears) them right after it returns, before any -// other call can reuse them, including a nested call from a callback. -final Pointer<_Error> _err = calloc<_Error>(); -final Pointer _outLen = calloc(); - -/// A failure the native library reported to a function declared to throw. -/// Domain errors extend this class with a positive [code]; a negative code is -/// one of the runtime constants below. -class NativeException implements Exception { - /// Creates an exception carrying [code] and [message]. - NativeException(this.code, this.message); - - /// The producer reported an untyped error. - static const int genericCode = -1; - - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - - /// An argument or a callback's return couldn't be lifted by the producer. - static const int marshalCode = -3; - - /// A callback implementation failed; [message] carries its text. - static const int foreignCode = -4; - - /// The call was cancelled (see [CancelledException]). - static const int cancelledCode = -5; - - /// The error code: positive for a domain error, negative for a runtime - /// failure. - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => '$runtimeType($code): $message'; -} - -/// A cancellable call completed because its token was cancelled. -class CancelledException extends NativeException { - /// Creates a cancellation with the producer's [message]. - CancelledException([String message = 'cancelled']) - : super(NativeException.cancelledCode, message); -} - -/// A function that isn't declared to throw failed anyway. That's a bug in -/// the native library (a panic, an argument it couldn't lift, a callback -/// failure it let through), so it's an [Error], not an [Exception]. -final class NativeError extends Error { - /// Creates the error a failed call reported. - NativeError(this.code, this.message); - - /// The runtime code the producer reported (see [NativeException]). - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => 'NativeError($code): $message'; -} - -/// Maps a reported code, message, and payload onto the object to throw. -typedef _ErrorMapper = Object Function( - int code, String message, Uint8List payload); - -/// The mapper of a function that can't fail: every code but cancellation -/// is a [NativeError]. -Object _trap(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeError(code, message); - -/// Maps a runtime code (or one the domain doesn't declare) onto its -/// exception, for a function declared to throw. -NativeException _runtimeException(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeException(code, message); - -/// Builds the error [err] reports, using [map]. -Object _readError(Pointer<_Error> err, _ErrorMapper map) { - final e = err.ref; - final message = e.message == nullptr ? '' : e.message.toDartString(); - return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); -} - -/// Throws the error a synchronous call left in [err], clearing the slot. -/// [map] is the domain's mapper for a function declared to throw. -void _check(Pointer<_Error> err, [_ErrorMapper map = _trap]) { - if (err.ref.code == 0) return; - final error = _readError(err, map); - _errorClear(err); - throw error; -} - -/// Copies a borrowed native byte run into Dart memory. -Uint8List _copyBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -/// Copies an owned native byte run and releases it. -Uint8List _takeBytes(Pointer ptr, int len) { - if (ptr == nullptr) return Uint8List(0); - final bytes = Uint8List.fromList(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return bytes; -} - -/// Decodes an owned UTF-8 run and releases it. -String _takeString(Pointer ptr, int len) { - if (ptr == nullptr) return ''; - final text = utf8.decode(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return text; -} - -/// Decodes a borrowed UTF-8 run. -String _readString(Pointer ptr, int len) => - ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); - -/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. -Pointer _stage(Arena arena, List bytes) { - if (bytes.isEmpty) return nullptr; - final ptr = arena(bytes.length); - ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -// ── Objects ── - -/// The lifetime machinery every interface wrapper shares. A wrapper owns one -/// strong reference to its native object and releases it exactly once: in -/// [dispose], or through a GC finalizer when it's collected undisposed. -/// -/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] -/// that arrives while a call is in flight (from a callback the call made) -/// only marks the wrapper; the reference is released when the last call -/// leaves, so the native object never disappears under a running call. -abstract base class _NativeObject implements Finalizable { - _NativeObject(this._ptr) { - _finalizer.attach(this, _ptr, detach: this); - } - - final Pointer _ptr; - int _calls = 0; - bool _disposed = false; - - NativeFinalizer get _finalizer; - void _destroy(Pointer ptr); - Pointer _clone(Pointer ptr); - - /// Releases this wrapper's native reference. Safe to call more than once; - /// the native object is dropped when its last reference (this one, another - /// wrapper's, or the producer's) goes away. Using the wrapper afterwards - /// throws a [StateError]. - void dispose() { - if (_disposed) return; - _disposed = true; - if (_calls == 0) _release(); - } - - void _release() { - _finalizer.detach(this); - _destroy(_ptr); - } - - Pointer _enter() { - if (_disposed) throw StateError('$runtimeType used after dispose()'); - _calls++; - return _ptr; - } - - void _leave() { - if (--_calls == 0 && _disposed) _release(); - } - - /// A second strong reference, for an object token in a value buffer. - Pointer _cloneRef() { - final ptr = _enter(); - try { - return _clone(ptr); - } finally { - _leave(); - } - } -} - -/// Borrows [object]'s pointer until [arena] is released. -Pointer _borrow(Arena arena, _NativeObject object) { - final ptr = object._enter(); - arena.onReleaseAll(object._leave); - return ptr; -} - -// ── Value buffers ── -// Records, rich enums, optionals, lists, maps, and error payloads cross the -// C ABI serialized in this little-endian, packed format. A malformed buffer -// is a contract violation between producer and bindings, never a domain -// error. - -Never _bufferError(String context) => - throw StateError('malformed value buffer: $context'); - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - late ByteData _data = ByteData.sublistView(_buf); - int _len = 0; - - void _reserve(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - _buf = Uint8List(cap)..setRange(0, _len, _buf); - _data = ByteData.sublistView(_buf); - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeU8(v ? 1 : 0); - - void writeFlag(bool present) => writeU8(present ? 1 : 0); - - void writeI8(int v) { - _reserve(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeU8(int v) { - _reserve(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeI16(int v) { - _reserve(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeU16(int v) { - _reserve(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeI32(int v) { - _reserve(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeU32(int v) { - _reserve(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeI64(int v) { - _reserve(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - // Dart ints are signed 64-bit; a u64 travels as its bit pattern. - void writeU64(int v) => writeI64(v); - - void writeF32(double v) { - _reserve(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeF64(double v) { - _reserve(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeU32(v); - - void writeString(String v) => writeBytes(utf8.encode(v)); - - void writeBytes(List v) { - writeLength(v.length); - _reserve(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } - - void writeOptional(T? v, void Function(T) write) { - writeFlag(v != null); - if (v != null) write(v); - } - - void writeList(List v, void Function(T) write) { - writeLength(v.length); - for (final e in v) { - write(e); - } - } - - void writeMap( - Map v, void Function(K) writeKey, void Function(V) writeValue) { - writeLength(v.length); - for (final e in v.entries) { - writeKey(e.key); - writeValue(e.value); - } - } -} - -final class _BufferReader { - _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); - - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - int _take(int n, String context) { - if (_buf.length - _pos < n) _bufferError('truncated $context'); - final at = _pos; - _pos += n; - return at; - } - - bool readBool() { - final b = _buf[_take(1, 'bool')]; - if (b > 1) _bufferError('bool byte $b'); - return b == 1; - } - - bool readFlag() { - final b = _buf[_take(1, 'option flag')]; - if (b > 1) _bufferError('option flag $b'); - return b == 1; - } - - int readI8() => _data.getInt8(_take(1, 'i8')); - - int readU8() => _buf[_take(1, 'u8')]; - - int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); - - int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); - - int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); - - int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); - - int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); - - // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. - int readU64() => readI64(); - - double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); - - double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); - - int readLength() => readU32(); - - String readString() { - final n = readLength(); - final at = _take(n, 'string'); - return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); - } - - Uint8List readBytes() { - final n = readLength(); - final at = _take(n, 'bytes'); - return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); - } - - T? readOptional(T Function() read) => - readFlag() ? read() : null; - - // Collections grow as they decode, so a corrupt count can't force a huge - // allocation up front. - List readList(T Function() read) => - [for (var i = readLength(); i > 0; i--) read()]; - - Map readMap(K Function() readKey, V Function() readValue) { - final map = {}; - for (var i = readLength(); i > 0; i--) { - final key = readKey(); - if (map.containsKey(key)) _bufferError('duplicate map key $key'); - map[key] = readValue(); - } - return map; - } - - void expectEnd() { - if (_pos != _buf.length) _bufferError('trailing bytes'); - } -} - -/// Encodes one value with [write] into a fresh buffer. -Uint8List _encode(T value, void Function(_BufferWriter, T) write) { - final writer = _BufferWriter(); - write(writer, value); - return writer.takeBytes(); -} - -/// Decodes exactly one value with [read] from [bytes]. -T _decode(Uint8List bytes, T Function(_BufferReader) read) { - final reader = _BufferReader(bytes); - final value = read(reader); - reader.expectEnd(); - return value; -} - -// Records and rich enums compare by value: lists, maps, and byte arrays -// element by element, and everything else with `==` (interface wrappers by -// identity). - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) return true; - if (a is List && b is List) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!_deepEquals(a[i], b[i])) return false; - } - return true; - } - if (a is Map && b is Map) { - if (a.length != b.length) return false; - for (final key in a.keys) { - if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false; - } - return true; - } - return a == b; -} - -int _deepHash(Object? v) => switch (v) { - List() => Object.hashAll(v.map(_deepHash)), - Map() => Object.hashAllUnordered( - v.entries.map((e) => Object.hash(_deepHash(e.key), _deepHash(e.value)))), - _ => v.hashCode, - }; - -// ── Async completion ── -// An async call completes through a `NativeCallable.listener` the producer -// invokes exactly once, from any thread. Everything the completion carries is -// owned by these bindings, so it's decoded later on the event loop and then -// released. - -final _errorFree = - _lib.lookupFunction), void Function(Pointer<_Error>)>( - 'kitchen_sink_error_free'); - -/// Builds the error a completion reports and frees its boxed error. -Object _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { - final error = _readError(err, map); - _errorFree(err); - return error; -} - -// ── Cancellation ── - -/// Cancels the cancellable calls it's passed to. One token can serve any -/// number of calls; once cancelled it stays cancelled, and a call that -/// receives a cancelled token completes with [CancelledException] without -/// waiting for its work. -final class CancelToken { - bool _cancelled = false; - final List _listeners = []; - - /// Whether [cancel] has been called. - bool get isCancelled => _cancelled; - - /// Requests cancellation of every call this token was passed to. Calls - /// that already completed are unaffected. Idempotent. - void cancel() { - if (_cancelled) return; - _cancelled = true; - final listeners = List.of(_listeners); - _listeners.clear(); - for (final listener in listeners) { - listener(); - } - } -} - -final _cancelTokenCreate = - _lib.lookupFunction Function(), Pointer Function()>( - 'kitchen_sink_cancel_token_create'); -final _cancelTokenCancel = _lib.lookupFunction), - void Function(Pointer)>('kitchen_sink_cancel_token_cancel'); -final _cancelTokenDestroy = _lib.lookupFunction), - void Function(Pointer)>('kitchen_sink_cancel_token_destroy'); - -/// The native token behind one launch: created for the call, cancelled when -/// the [CancelToken] is, and destroyed once the call completes. The producer -/// holds its own reference, so destroying ours never races the call. -final class _NativeCancel { - _NativeCancel._(this._token) : pointer = _cancelTokenCreate() { - if (_token.isCancelled) { - _cancelTokenCancel(pointer); - } else { - _token._listeners.add(_cancel); - } - } - - /// Binds a native token to [token], or returns null for no token. - static _NativeCancel? bind(CancelToken? token) => - token == null ? null : _NativeCancel._(token); - - final CancelToken _token; - - /// The `kitchen_sink_cancel_token*` passed to the launcher. - final Pointer pointer; - - void _cancel() => _cancelTokenCancel(pointer); - - /// Detaches from the [CancelToken] and drops the native reference. - void release() { - _token._listeners.remove(_cancel); - _cancelTokenDestroy(pointer); - } -} - -// ── Callback interfaces ── -// An implementation passed to the producer is parked in [_callbacks] under -// an integer key. The `ctx` the producer receives points at a small native -// [_CallbackContext] holding that key, so the producer never holds a Dart -// object and the GC never sees a raw pointer. The entry lives until the -// producer calls the vtable's `free`. -// -// Methods that return a value are `NativeCallable.isolateLocal` trampolines: -// they run synchronously, so the producer must call them on this isolate's -// thread while a call from Dart is in progress (the VM aborts the process -// otherwise, and an entry point that may run on any thread can't re-enter -// the isolate to run the method). Void methods and `free` are -// `NativeCallable.isolateGroupBound` forwarders, which may run on any -// thread: they copy their arguments into a message for this isolate -// (borrowed bytes are copied before the forwarder returns) and the -// implementation runs later on the event loop, in the zone that passed the -// callback. - -/// Where forwarders send messages: the allocator they build a message with -/// and the isolate port that receives it. Forwarders run outside the -/// isolate and can't read Dart globals, so everything they need is native. -final class _CallbackPost extends Struct { - external Pointer Function(Size)>> alloc; - external Pointer)>> release; - external Pointer)>> post; - @Int64() - external int port; -} - -/// The native `ctx` of one registered implementation. -final class _CallbackContext extends Struct { - @Int64() - external int key; - external Pointer<_CallbackPost> post; -} - -// The `Dart_CObject` layout `Dart_PostCObject` reads. -final class _CArray extends Struct { - @IntPtr() - external int length; - external Pointer> values; -} - -final class _CTypedData extends Struct { - @Int32() - external int type; - @IntPtr() - external int length; - external Pointer values; -} - -final class _CValue extends Union { - @Bool() - external bool asBool; - @Int64() - external int asInt64; - @Double() - external double asDouble; - external _CArray asArray; - external _CTypedData asTypedData; -} - -final class _CObject extends Struct { - @Int32() - external int type; - external _CValue value; -} - -typedef _CallbackDispatch = void Function( - Object impl, int method, List message); - -final class _CallbackEntry { - _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; - - final Object impl; - final _CallbackDispatch dispatch; - final Pointer<_CallbackContext> context; - final Zone zone; -} - -final Map _callbacks = {}; -int _nextCallbackKey = 1; - -/// Registers [impl] and returns the `ctx` the producer receives. -Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { - final key = _nextCallbackKey++; - final context = calloc<_CallbackContext>(); - context.ref - ..key = key - ..post = _callbackPost; - _callbacks[key] = _CallbackEntry(impl, dispatch, context); - return context.cast(); -} - -/// The implementation registered under [ctx]. -Object _callbackTarget(Pointer ctx) { - final key = ctx.cast<_CallbackContext>().ref.key; - final entry = _callbacks[key]; - if (entry == null) { - throw StateError('callback context $key is not registered'); - } - return entry.impl; -} - -final Pointer<_CallbackPost> _callbackPost = () { - final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); - port.keepIsolateAlive = false; - _callbackPort = port; - final allocator = Platform.isWindows - ? DynamicLibrary.open('ole32.dll') - : DynamicLibrary.process(); - final post = calloc<_CallbackPost>(); - post.ref - ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') - ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') - ..post = NativeApi.postCObject.cast() - ..port = port.sendPort.nativePort; - return post; -}(); - -RawReceivePort? _callbackPort; - -void _receiveCallbackMessage(Object? message) { - final fields = message! as List; - final key = fields[0]! as int; - final method = fields[1]! as int; - if (method == _CallbackMessage.free) { - final entry = _callbacks.remove(key); - if (entry != null) calloc.free(entry.context); - return; - } - final entry = _callbacks[key]; - if (entry == null) return; - entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); -} - -/// One message from a forwarder: `[key, method, arguments...]`, built in -/// native memory and posted with `Dart_PostCObject`, which copies it. Runs -/// outside the isolate, so it touches no Dart globals. -final class _CallbackMessage { - _CallbackMessage(Pointer ctx, int method, int arguments) - : _context = ctx.cast<_CallbackContext>().ref, - _capacity = arguments + 2 { - final post = _context.post.ref; - final alloc = post.alloc.asFunction Function(int)>(); - _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); - _slots = alloc(sizeOf>() * _capacity).cast(); - int64(_context.key); - int64(method); - } - - static const int free = -1; - static const int _kBool = 1; - static const int _kInt64 = 3; - static const int _kDouble = 4; - static const int _kArray = 6; - static const int _kTypedData = 7; - static const int _kUint8 = 2; - - final _CallbackContext _context; - final int _capacity; - late final Pointer<_CObject> _objects; - late final Pointer> _slots; - int _count = 0; - - _CObject _next(int type) { - final object = _objects + (_count + 1); - _slots[_count++] = object; - return object.ref..type = type; - } - - void boolean(bool v) => _next(_kBool).value.asBool = v; - - void int64(int v) => _next(_kInt64).value.asInt64 = v; - - void float64(double v) => _next(_kDouble).value.asDouble = v; - - void pointer(Pointer v) => int64(v.address); - - /// A borrowed byte run; posting copies it. - void bytes(Pointer ptr, int len) { - final data = _next(_kTypedData).value.asTypedData; - data - ..type = _kUint8 - ..length = ptr == nullptr ? 0 : len - ..values = ptr; - } - - void send() { - final post = _context.post.ref; - _objects.ref.type = _kArray; - _objects.ref.value.asArray - ..length = _count - ..values = _slots; - post.post.asFunction)>()( - post.port, _objects); - final release = post.release.asFunction)>(); - release(_slots.cast()); - release(_objects.cast()); - } -} - -// The vtable `free` entry every callback interface shares. -void _forwardCallbackFree(Pointer ctx) => - _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); - -final Pointer)>> _callbackFree = - _pin(NativeCallable)>.isolateGroupBound( - _forwardCallbackFree)); - -// Vtable entries live for the process: they're never closed, so they're -// anchored here, and they don't keep an idle isolate alive. -final List> _pinned = >[]; - -Pointer> _pin(NativeCallable callable) { - callable.keepIsolateAlive = false; - _pinned.add(callable); - return callable.nativeFunction; -} - -final _errorSet = _lib.lookupFunction< - Void Function(Pointer<_Error>, Int32, Pointer), - void Function(Pointer<_Error>, int, Pointer)>('kitchen_sink_error_set'); - -final _errorSetPayload = _lib.lookupFunction< - Void Function(Pointer<_Error>, Pointer, Size), - void Function( - Pointer<_Error>, Pointer, int)>('kitchen_sink_error_set_payload'); - -final _alloc = _lib.lookupFunction Function(Size), - Pointer Function(int)>('kitchen_sink_alloc', isLeaf: true); - -/// Reports a failed callback method to the producer through [outErr]: -/// `kitchen_sink_error_set` copies [message], and `kitchen_sink_error_set_payload` -/// copies a domain code's encoded fields. Never throws: it runs in the catch -/// path of a trampoline an exception must not unwind through. -void _failCallback( - Pointer<_Error> outErr, int code, String message, Uint8List? payload) { - if (outErr == nullptr) return; - final text = message.toNativeUtf8(allocator: calloc); - try { - _errorSet(outErr, code, text); - } finally { - calloc.free(text); - } - if (payload == null || payload.isEmpty) return; - final bytes = calloc(payload.length); - try { - bytes.asTypedList(payload.length).setAll(0, payload); - _errorSetPayload(outErr, bytes, payload.length); - } finally { - calloc.free(bytes); - } -} - -/// Reports an implementation's exception that isn't a domain error of the -/// method with the foreign code (-4) and the exception's text. -void _foreignError(Pointer<_Error> outErr, Object error) { - try { - _failCallback(outErr, NativeException.foreignCode, '$error', null); - } catch (_) { - _errorSet(outErr, NativeException.foreignCode, nullptr); - } -} - -/// Hands [bytes] to the producer through a method's `out_ptr`/`out_len` -/// slots as a run allocated with `kitchen_sink_alloc`, which the producer -/// adopts and frees. An empty run is null with length 0. -void _handOver( - List bytes, Pointer> outPtr, Pointer outLen) { - final run = bytes.isEmpty ? nullptr : _alloc(bytes.length); - if (run != nullptr) run.asTypedList(bytes.length).setAll(0, bytes); - outPtr.value = run; - outLen.value = bytes.length; -} - -// ── Iterators ── - -/// Anchors one live native iterator for its GC-finalizer backstop. A -/// suspended `sync*` body keeps its anchor reachable; abandoning the -/// iteration drops the body, and the finalizer destroys the native iterator. -/// An iteration that runs to completion (or fails) detaches and destroys it -/// eagerly instead, so the iterator is destroyed exactly once either way. -final class _IteratorAnchor implements Finalizable {} - -/// The `out_item` slot iterator steps write, wide enough for any element. -final Pointer _outItem = calloc(); - -void _packListOfGadget(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeU64(e._cloneRef().address)); - -List _unpackListOfGadget(_BufferReader r) => - r.readList(() => Gadget._(Pointer.fromAddress(r.readU64()))); - -void _packListOfItem(_BufferWriter w, List v) => - w.writeList(v, (e) => _packItem(w, e)); - -List _unpackListOfItem(_BufferReader r) => - r.readList(() => _unpackItem(r)); - -void _packListOfString(_BufferWriter w, List v) => - w.writeList(v, (e) => w.writeString(e)); - -List _unpackListOfString(_BufferReader r) => - r.readList(() => r.readString()); - -void _packMapOfStringToI32(_BufferWriter w, Map v) => - w.writeMap(v, (k) => w.writeString(k), (e) => w.writeI32(e)); - -Map _unpackMapOfStringToI32(_BufferReader r) => - r.readMap(() => r.readString(), () => r.readI32()); - -void _packMapOfStringToString(_BufferWriter w, Map v) => - w.writeMap(v, (k) => w.writeString(k), (e) => w.writeString(e)); - -Map _unpackMapOfStringToString(_BufferReader r) => - r.readMap(() => r.readString(), () => r.readString()); - -void _packOptionalOfGadget(_BufferWriter w, Gadget? v) => - w.writeOptional(v, (e) => w.writeU64(e._cloneRef().address)); - -Gadget? _unpackOptionalOfGadget(_BufferReader r) => - r.readOptional(() => Gadget._(Pointer.fromAddress(r.readU64()))); - -void _packOptionalOfI64(_BufferWriter w, int? v) => - w.writeOptional(v, (e) => w.writeI64(e)); - -int? _unpackOptionalOfI64(_BufferReader r) => - r.readOptional(() => r.readI64()); - -void _packOptionalOfItem(_BufferWriter w, Item? v) => - w.writeOptional(v, (e) => _packItem(w, e)); - -Item? _unpackOptionalOfItem(_BufferReader r) => - r.readOptional(() => _unpackItem(r)); - -/// A reusable token referenced across modules -final class Token { - /// Creates a [Token]. - Token({required this.id, required this.label}); - - final int id; - final String label; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Token && - id == other.id && - label == other.label; - - @override - int get hashCode => Object.hashAll([Token, id, label]); - - @override - String toString() => 'Token(id: $id, label: $label)'; -} - -void _packToken(_BufferWriter w, Token v) { - w.writeI64(v.id); - w.writeString(v.label); -} - -Token _unpackToken(_BufferReader r) => Token( - id: r.readI64(), - label: r.readString(), - ); - -final _kitchenSinkSharedPing = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_shared_ping'); - -/// Trivial cross-module helper -String ping() { - final _result = _kitchenSinkSharedPing(_outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); -} - -/// The `KitchenErrors` error domain of module `kitchen`: catch it to handle any of -/// its codes, or a subclass for one code. -sealed class KitchenErrorsException extends NativeException { - /// Creates a domain error carrying [code] and [message]. - KitchenErrorsException(super.code, super.message); -} - -/// Item not found -final class NotFoundException extends KitchenErrorsException { - /// Creates a [NotFoundException] (code 1). - NotFoundException([String message = 'Item not found']) : super(1, message); -} - -/// Invalid input -final class InvalidInputException extends KitchenErrorsException { - /// Creates an [InvalidInputException] (code 2). - InvalidInputException([String message = 'Invalid input']) : super(2, message); -} - -NativeException _mapKitchenErrorsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return NotFoundException(message); - case 2: - return InvalidInputException(message); - default: - return _runtimeException(code, message, payload); - } -} - -/// The value buffer of [e]'s fields, or null for a code without any. -Uint8List? _payloadOfKitchenErrorsException(KitchenErrorsException e) { - switch (e) { - case NotFoundException(): - return null; - case InvalidInputException(): - return null; - } -} - -/// Task priority levels -enum Priority { - /// Low priority - low(0), - /// Normal priority - normal(1), - /// High priority - high(2); - - const Priority(this.value); - - /// The C discriminant. - final int value; - - /// The variant whose discriminant is [value]. - static Priority fromValue(int value) => values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError.value(value, 'value', 'not a Priority discriminant')); -} - -/// Kitchen sink struct exercising every field feature -final class Item { - /// Creates an [Item]. - Item({required this.id, required this.name, required this.count, required this.enabled, required this.ratio, required this.nick, required this.payload, required this.tags, required this.attrs, this.parent, required this.token, required this.priority, this.gadget}); - - /// Stable identifier - final int id; - /// Display name - final String name; - /// Initial count - final int count; - /// Whether the item is enabled - final bool enabled; - /// Scaling ratio - final double ratio; - final int nick; - final List payload; - final List tags; - final Map attrs; - final int? parent; - /// Cross-module struct reference - final Token token; - final Priority priority; - /// An optional object inside a record - final Gadget? gadget; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Item && - id == other.id && - name == other.name && - count == other.count && - enabled == other.enabled && - ratio == other.ratio && - nick == other.nick && - _deepEquals(payload, other.payload) && - _deepEquals(tags, other.tags) && - _deepEquals(attrs, other.attrs) && - parent == other.parent && - token == other.token && - priority == other.priority && - gadget == other.gadget; - - @override - int get hashCode => Object.hashAll([Item, id, name, count, enabled, ratio, nick, _deepHash(payload), _deepHash(tags), _deepHash(attrs), parent, token, priority, gadget]); - - @override - String toString() => 'Item(id: $id, name: $name, count: $count, enabled: $enabled, ratio: $ratio, nick: $nick, payload: $payload, tags: $tags, attrs: $attrs, parent: $parent, token: $token, priority: $priority, gadget: $gadget)'; -} - -void _packItem(_BufferWriter w, Item v) { - w.writeI64(v.id); - w.writeString(v.name); - w.writeI32(v.count); - w.writeBool(v.enabled); - w.writeF64(v.ratio); - w.writeU32(v.nick); - w.writeBytes(v.payload); - _packListOfString(w, v.tags); - _packMapOfStringToString(w, v.attrs); - _packOptionalOfI64(w, v.parent); - _packToken(w, v.token); - w.writeI32(v.priority.value); - _packOptionalOfGadget(w, v.gadget); -} - -Item _unpackItem(_BufferReader r) => Item( - id: r.readI64(), - name: r.readString(), - count: r.readI32(), - enabled: r.readBool(), - ratio: r.readF64(), - nick: r.readU32(), - payload: r.readBytes(), - tags: _unpackListOfString(r), - attrs: _unpackMapOfStringToString(r), - parent: _unpackOptionalOfI64(r), - token: _unpackToken(r), - priority: Priority.fromValue(r.readI32()), - gadget: _unpackOptionalOfGadget(r), - ); - -/// Consumer-implemented observer of ready items -/// -/// A callback interface: implement it and pass an instance to any -/// function taking a [ReadyListener]. The library keeps the instance alive -/// until the producer releases it. -/// -/// Methods that return a value run synchronously and must be called by -/// the producer on this isolate's thread during a call from Dart; a -/// thrown exception fails that call with [NativeException.foreignCode] -/// (or, from a method that throws, the domain exception's code and -/// fields). Void methods may be called from any thread: they're -/// delivered asynchronously on this isolate's event loop, in the zone -/// that passed the instance, where a thrown exception is an uncaught -/// error of that zone. Object arguments are owned by the -/// implementation. -abstract class ReadyListener { - /// Fires when an item is ready - void onReady(int code, String msg); - - /// Receives the item itself and says whether to keep listening - bool onItem(Item item, Gadget gadget); - - /// A display label, failing with a kitchen error - /// - /// Throw a [KitchenErrorsException] to fail the producer's call with it. - String label(); - - /// The most recent item the listener kept, if any - Item? latest(); - - /// The listener's favorite gadget - Gadget favorite(); -} - -typedef _KitchenSinkKitchenReadyListenerOnReady = Void Function(Pointer, Int32, Pointer, Size, Pointer<_Error>); -typedef _KitchenSinkKitchenReadyListenerOnItem = Bool Function(Pointer, Pointer, Size, Pointer, Pointer<_Error>); -typedef _KitchenSinkKitchenReadyListenerLabel = Void Function(Pointer, Pointer>, Pointer, Pointer<_Error>); -typedef _KitchenSinkKitchenReadyListenerLatest = Void Function(Pointer, Pointer>, Pointer, Pointer<_Error>); -typedef _KitchenSinkKitchenReadyListenerFavorite = Pointer Function(Pointer, Pointer<_Error>); - -// The C `kitchen_sink_kitchen_ReadyListener_vtable` layout. -final class _ReadyListenerVtable extends Struct { - @Uint32() - external int size; - @Uint32() - external int flags; - external Pointer)>> free; - external Pointer> onReady; - external Pointer> onItem; - external Pointer> label; - external Pointer> latest; - external Pointer> favorite; -} - -// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.on_ready`: forwarded to the isolate. -void _readyListenerOnReady(Pointer ctx, int code, Pointer msgPtr, int msgLen, Pointer<_Error> outErr) { - _CallbackMessage(ctx, 0, 2) - ..int64(code) - ..bytes(msgPtr, msgLen) - ..send(); -} - -// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.on_item`. -bool _readyListenerOnItem(Pointer ctx, Pointer itemPtr, int itemLen, Pointer gadget, Pointer<_Error> outErr) { - try { - final _gadget = Gadget._(gadget); - final _item = _decode(_copyBytes(itemPtr, itemLen), _unpackItem); - return (_callbackTarget(ctx) as ReadyListener).onItem(_item, _gadget); - } catch (e) { - _foreignError(outErr, e); - } - return false; -} - -// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.label`. -void _readyListenerLabel(Pointer ctx, Pointer> outPtr, Pointer outLen, Pointer<_Error> outErr) { - try { - final result = (_callbackTarget(ctx) as ReadyListener).label(); - _handOver(utf8.encode(result), outPtr, outLen); - } on KitchenErrorsException catch (e) { - _failCallback(outErr, e.code, e.message, _payloadOfKitchenErrorsException(e)); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.latest`. -void _readyListenerLatest(Pointer ctx, Pointer> outPtr, Pointer outLen, Pointer<_Error> outErr) { - try { - final result = (_callbackTarget(ctx) as ReadyListener).latest(); - _handOver(_encode(result, _packOptionalOfItem), outPtr, outLen); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.favorite`. -Pointer _readyListenerFavorite(Pointer ctx, Pointer<_Error> outErr) { - try { - final result = (_callbackTarget(ctx) as ReadyListener).favorite(); - return result._cloneRef(); - } catch (e) { - _foreignError(outErr, e); - } - return nullptr; -} - -void _readyListenerDispatch(Object impl, int method, List message) { - final target = impl as ReadyListener; - switch (method) { - case 0: { - final a2 = message[2]! as int; - final a3 = utf8.decode(message[3]! as Uint8List); - target.onReady(a2, a3); - } - } -} - -final Pointer<_ReadyListenerVtable> _readyListenerVtable = () { - final vtable = calloc<_ReadyListenerVtable>(); - vtable.ref - ..size = sizeOf<_ReadyListenerVtable>() - ..flags = 0 - ..free = _callbackFree - ..onReady = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerOnReady>.isolateGroupBound(_readyListenerOnReady)) - ..onItem = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerOnItem>.isolateLocal(_readyListenerOnItem, exceptionalReturn: false)) - ..label = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerLabel>.isolateLocal(_readyListenerLabel)) - ..latest = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerLatest>.isolateLocal(_readyListenerLatest)) - ..favorite = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerFavorite>.isolateLocal(_readyListenerFavorite)); - return vtable; -}(); - -final _kitchenSinkKitchenGadgetClone = _lib.lookupFunction< - Pointer Function(Pointer), - Pointer Function(Pointer)>('kitchen_sink_kitchen_Gadget_clone'); - -final _kitchenSinkKitchenGadgetDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('kitchen_sink_kitchen_Gadget_destroy'); -final _kitchenSinkKitchenGadgetDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('kitchen_sink_kitchen_Gadget_destroy')); - -final _kitchenSinkKitchenGadgetNew = _lib.lookupFunction< - Pointer Function(Int64, Pointer<_Error>), - Pointer Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_new'); - -final _kitchenSinkKitchenGadgetDescribe = _lib.lookupFunction< - Pointer Function(Pointer, Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_describe'); - -final _kitchenSinkKitchenGadgetPoke = _lib.lookupFunction< - Int32 Function(Pointer, Int32, Pointer<_Error>), - int Function(Pointer, int, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_poke'); - -final _kitchenSinkKitchenGadgetVersion = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_version'); - -/// A pocket-sized interface exercising the object surface -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, when an undisposed instance is -/// collected, by a finalizer. -final class Gadget extends _NativeObject { - Gadget._(super._ptr); - - @override - NativeFinalizer get _finalizer => _kitchenSinkKitchenGadgetDestroyFinalizer; - - @override - void _destroy(Pointer ptr) => _kitchenSinkKitchenGadgetDestroy(ptr); - - @override - Pointer _clone(Pointer ptr) => _kitchenSinkKitchenGadgetClone(ptr); - - /// Create a gadget with the given id - /// - /// The returned object is owned by the caller: call its `dispose()` - /// when you're done with it. - factory Gadget(int id) { - final _result = _kitchenSinkKitchenGadgetNew(id, _err); - _check(_err); - return Gadget._(_result); - } - - /// Render the gadget as a human-readable string - String describe() { - final _self = _enter(); - try { - final _result = _kitchenSinkKitchenGadgetDescribe(_self, _outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } finally { - _leave(); - } - } - - /// Poke the gadget a number of times, failing on a negative count - /// - /// Throws [KitchenErrorsException] on domain errors. - int poke(int times) { - final _self = _enter(); - try { - final _result = _kitchenSinkKitchenGadgetPoke(_self, times, _err); - _check(_err, _mapKitchenErrorsException); - return _result; - } finally { - _leave(); - } - } - - /// The gadget subsystem version string - static String version() { - final _result = _kitchenSinkKitchenGadgetVersion(_outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } -} - -final _kitchenSinkKitchenBoolId = _lib.lookupFunction< - Bool Function(Bool, Pointer<_Error>), - bool Function(bool, Pointer<_Error>)>('kitchen_sink_kitchen_bool_id'); - -/// Identity for bool -bool boolId(bool x) { - final _result = _kitchenSinkKitchenBoolId(x, _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenI32Id = _lib.lookupFunction< - Int32 Function(Int32, Pointer<_Error>), - int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_i32_id'); - -int i32Id(int x) { - final _result = _kitchenSinkKitchenI32Id(x, _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenU32Id = _lib.lookupFunction< - Uint32 Function(Uint32, Pointer<_Error>), - int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_u32_id'); - -int u32Id(int x) { - final _result = _kitchenSinkKitchenU32Id(x, _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenI64Id = _lib.lookupFunction< - Int64 Function(Int64, Pointer<_Error>), - int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_i64_id'); - -int i64Id(int x) { - final _result = _kitchenSinkKitchenI64Id(x, _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenF64Id = _lib.lookupFunction< - Double Function(Double, Pointer<_Error>), - double Function(double, Pointer<_Error>)>('kitchen_sink_kitchen_f64_id'); - -double f64Id(double x) { - final _result = _kitchenSinkKitchenF64Id(x, _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenEchoString = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_echo_string'); - -String echoString(String s) { - final _arena = Arena(); - try { - final _sBytes = utf8.encode(s); - final _result = _kitchenSinkKitchenEchoString(_stage(_arena, _sBytes), _sBytes.length, _outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } finally { - _arena.releaseAll(); - } -} - -final _kitchenSinkKitchenEchoBytes = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_echo_bytes'); - -List echoBytes(List b) { - final _arena = Arena(); - try { - final _result = _kitchenSinkKitchenEchoBytes(_stage(_arena, b), b.length, _outLen, _err); - _check(_err); - return _takeBytes(_result, _outLen.value); - } finally { - _arena.releaseAll(); - } -} - -final _kitchenSinkKitchenSubscribe = _lib.lookupFunction< - Int32 Function(Pointer, Pointer, Pointer<_Error>), - int Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_subscribe'); - -/// Register a listener, returning how many are attached -int subscribe(ReadyListener listener) { - final _listenerCtx = _registerCallback(listener, _readyListenerDispatch); - final _result = _kitchenSinkKitchenSubscribe(_listenerCtx, _readyListenerVtable.cast(), _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenMaybeSubscribe = _lib.lookupFunction< - Bool Function(Pointer, Pointer, Pointer<_Error>), - bool Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_subscribe'); - -/// Register a listener if one is given -bool maybeSubscribe(ReadyListener? listener) { - final _listenerCtx = listener == null ? nullptr : _registerCallback(listener, _readyListenerDispatch); - final _result = _kitchenSinkKitchenMaybeSubscribe(_listenerCtx, listener == null ? nullptr : _readyListenerVtable.cast(), _err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenMaybeGadget = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_gadget'); - -/// Optional object in both directions -/// -/// Returns null when the producer reports no object. A returned object -/// is owned by the caller: call its `dispose()` when you're done with it. -Gadget? maybeGadget(Gadget? fallback) { - final _arena = Arena(); - try { - final _fallbackPtr = fallback == null ? nullptr : _borrow(_arena, fallback); - final _result = _kitchenSinkKitchenMaybeGadget(_fallbackPtr, _err); - _check(_err); - return _result == nullptr ? null : Gadget._(_result); - } finally { - _arena.releaseAll(); - } -} - -final _kitchenSinkKitchenGadgets = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_gadgets'); - -/// A list of objects -List gadgets() { - final _result = _kitchenSinkKitchenGadgets(_outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackListOfGadget); -} - -final _kitchenSinkKitchenStreamGadgets = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_gadgets'); - -final _kitchenSinkKitchenStreamGadgetsIteratorNext = _lib.lookupFunction< - Int32 Function(Pointer, Pointer>, Pointer<_Error>), - int Function(Pointer, Pointer>, Pointer<_Error>)>('kitchen_sink_kitchen_StreamGadgetsIterator_next'); - -final _kitchenSinkKitchenStreamGadgetsIteratorDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('kitchen_sink_kitchen_StreamGadgetsIterator_destroy'); -final _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('kitchen_sink_kitchen_StreamGadgetsIterator_destroy')); - -/// An iterator over objects -/// -/// Returns a lazy [Iterable]: each step pulls one element from a native -/// iterator, and iterating again starts a new one. The native iterator -/// is released when the iteration ends, fails, or is abandoned and -/// collected. -/// -/// Each element is owned by the caller: call its `dispose()` when -/// you're done with it. -Iterable streamGadgets() sync* { - final Pointer _iter; - _iter = _kitchenSinkKitchenStreamGadgets(_err); - _check(_err); - final _anchor = _IteratorAnchor(); - _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); - try { - while (true) { - final _more = _kitchenSinkKitchenStreamGadgetsIteratorNext(_iter, _outItem.cast(), _err); - _check(_err); - if (_more == 0) break; - yield Gadget._(_outItem.cast>().value); - } - } finally { - _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer.detach(_anchor); - _kitchenSinkKitchenStreamGadgetsIteratorDestroy(_iter); - } -} - -final _kitchenSinkKitchenMaybeItem = _lib.lookupFunction< - Pointer Function(Int64, Pointer, Pointer<_Error>), - Pointer Function(int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_item'); - -/// Optional struct return -/// -/// Throws [KitchenErrorsException] on domain errors. -Item? maybeItem(int id) { - final _result = _kitchenSinkKitchenMaybeItem(id, _outLen, _err); - _check(_err, _mapKitchenErrorsException); - return _decode(_takeBytes(_result, _outLen.value), _unpackOptionalOfItem); -} - -final _kitchenSinkKitchenListItems = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_list_items'); - -/// List of structs -List listItems() { - final _result = _kitchenSinkKitchenListItems(_outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackListOfItem); -} - -final _kitchenSinkKitchenGetAttrs = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_get_attrs'); - -/// Map return type -Map getAttrs() { - final _result = _kitchenSinkKitchenGetAttrs(_outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackMapOfStringToI32); -} - -final _kitchenSinkKitchenStreamItems = _lib.lookupFunction< - Pointer Function(Pointer<_Error>), - Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_items'); - -final _kitchenSinkKitchenStreamItemsIteratorNext = _lib.lookupFunction< - Int32 Function(Pointer, Pointer>, Pointer, Pointer<_Error>), - int Function(Pointer, Pointer>, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_StreamItemsIterator_next'); - -final _kitchenSinkKitchenStreamItemsIteratorDestroy = _lib.lookupFunction< - Void Function(Pointer), - void Function(Pointer)>('kitchen_sink_kitchen_StreamItemsIterator_destroy'); -final _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('kitchen_sink_kitchen_StreamItemsIterator_destroy')); - -/// Iterator return type -/// -/// Returns a lazy [Iterable]: each step pulls one element from a native -/// iterator, and iterating again starts a new one. The native iterator -/// is released when the iteration ends, fails, or is abandoned and -/// collected. -Iterable streamItems() sync* { - final Pointer _iter; - _iter = _kitchenSinkKitchenStreamItems(_err); - _check(_err); - final _anchor = _IteratorAnchor(); - _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); - try { - while (true) { - final _more = _kitchenSinkKitchenStreamItemsIteratorNext(_iter, _outItem.cast(), _outLen, _err); - _check(_err); - if (_more == 0) break; - yield _decode(_takeBytes(_outItem.cast>().value, _outLen.value), _unpackItem); - } - } finally { - _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer.detach(_anchor); - _kitchenSinkKitchenStreamItemsIteratorDestroy(_iter); - } -} - -final _kitchenSinkKitchenCrossModuleToken = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_cross_module_token'); - -/// Returns the shared Token type from another module -Token crossModuleToken() { - final _result = _kitchenSinkKitchenCrossModuleToken(_outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackToken); -} - -typedef _KitchenSinkKitchenDoAsyncCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _kitchenSinkKitchenDoAsync = _lib.lookupFunction< - Void Function(Pointer, Size, Pointer>, Pointer), - void Function(Pointer, int, Pointer>, Pointer)>('kitchen_sink_kitchen_do_async'); - -/// Async operation -Future doAsync(String input) { - final _arena = Arena(); - final _completer = Completer(); - late final NativeCallable<_KitchenSinkKitchenDoAsyncCallback> _callback; - _callback = NativeCallable<_KitchenSinkKitchenDoAsyncCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(_takeString(value, valueLen)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - final _inputBytes = utf8.encode(input); - _kitchenSinkKitchenDoAsync(_stage(_arena, _inputBytes), _inputBytes.length, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - rethrow; - } finally { - _arena.releaseAll(); - } - return _completer.future; -} - -typedef _KitchenSinkKitchenDoCancellableCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); - -final _kitchenSinkKitchenDoCancellable = _lib.lookupFunction< - Void Function(Pointer, Size, Pointer, Pointer>, Pointer), - void Function(Pointer, int, Pointer, Pointer>, Pointer)>('kitchen_sink_kitchen_do_cancellable'); - -/// Cancellable async operation -/// -/// Cancelling [cancelToken] completes the returned future with a -/// [CancelledException]. -Future doCancellable(String input, {CancelToken? cancelToken}) { - final _arena = Arena(); - final _completer = Completer(); - final _cancel = _NativeCancel.bind(cancelToken); - late final NativeCallable<_KitchenSinkKitchenDoCancellableCallback> _callback; - _callback = NativeCallable<_KitchenSinkKitchenDoCancellableCallback>.listener(( - Pointer _, Pointer<_Error> error, Pointer value, int valueLen) { - _callback.close(); - _cancel?.release(); - try { - if (error != nullptr) throw _takeAsyncError(error, _trap); - _completer.complete(_takeString(value, valueLen)); - } catch (e, s) { - _completer.completeError(e, s); - } - }); - try { - final _inputBytes = utf8.encode(input); - _kitchenSinkKitchenDoCancellable(_stage(_arena, _inputBytes), _inputBytes.length, _cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr); - } catch (_) { - _callback.close(); - _cancel?.release(); - rethrow; - } finally { - _arena.releaseAll(); - } - return _completer.future; -} - -final _kitchenSinkKitchenLegacyOp = _lib.lookupFunction< - Int32 Function(Pointer<_Error>), - int Function(Pointer<_Error>)>('kitchen_sink_kitchen_legacy_op'); - -/// Legacy operation kept for compatibility -@Deprecated('Use new_op instead') -int legacyOp() { - final _result = _kitchenSinkKitchenLegacyOp(_err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenNewOp = _lib.lookupFunction< - Int32 Function(Pointer<_Error>), - int Function(Pointer<_Error>)>('kitchen_sink_kitchen_new_op'); - -/// Replacement for legacy_op -int newOp() { - final _result = _kitchenSinkKitchenNewOp(_err); - _check(_err); - return _result; -} - -final _kitchenSinkKitchenNestedHello = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_nested_hello'); - -/// Trivial nested-module function -String hello() { - final _result = _kitchenSinkKitchenNestedHello(_outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); -} +part 'src/runtime/loader.dart'; +part 'src/runtime/contracts.dart'; +part 'src/runtime/core.dart'; +part 'src/runtime/object.dart'; +part 'src/runtime/codec.dart'; +part 'src/runtime/arrays.dart'; +part 'src/runtime/async.dart'; +part 'src/runtime/cancel.dart'; +part 'src/runtime/callbacks.dart'; +part 'src/runtime/iterator.dart'; +part 'src/runtime/composites.dart'; +part 'src/shared.dart'; +part 'src/kitchen.dart'; +part 'src/kitchen/nested.dart'; // END kitchen_sink.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_kitchen_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_kitchen_dart.snap new file mode 100644 index 00000000..a3d6ca88 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_kitchen_dart.snap @@ -0,0 +1,1352 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field +part of '../kitchen_sink.dart'; + +// ── Module `kitchen` ── + +/// The `KitchenErrors` error domain of module `kitchen`: catch it to handle any of +/// its codes, or a subclass for one code. The domain is open: a code +/// these bindings don't declare (from a newer library) arrives as this +/// class itself, with its [code] and [message]. +class KitchenException extends NativeException { + /// Creates a domain error carrying [code] and [message]. + KitchenException(super.code, super.message); +} + +/// Item not found +final class NotFoundException extends KitchenException { + /// Creates a [NotFoundException] (code 1). + NotFoundException([String message = 'Item not found']) : super(1, message); +} + +/// Invalid input +final class InvalidInputException extends KitchenException { + /// Creates an [InvalidInputException] (code 2). + InvalidInputException([String message = 'Invalid input']) : super(2, message); +} + +NativeException _mapKitchenException(int code, String message, Uint8List payload) => + switch (code) { + 1 => NotFoundException(message), + 2 => InvalidInputException(message), + > 0 => KitchenException(code, message), + _ => _runtimeException(code, message, payload), + }; + +/// The `PantryError` error domain of module `kitchen`: catch it to handle any of +/// its codes, or a subclass for one code. The domain is open: a code +/// these bindings don't declare (from a newer library) arrives as this +/// class itself, with its [code] and [message]. +class PantryException extends NativeException { + /// Creates a domain error carrying [code] and [message]. + PantryException(super.code, super.message); +} + +/// Out of stock +final class OutOfStockException extends PantryException { + /// Creates an [OutOfStockException] (code 1). + OutOfStockException(this.item, [String message = 'Out of stock']) + : super(1, message); + + final String item; +} + +/// Spoiled +final class SpoiledException extends PantryException { + /// Creates a [SpoiledException] (code 2). + SpoiledException([String message = 'Spoiled']) : super(2, message); +} + +NativeException _mapPantryException(int code, String message, Uint8List payload) => + switch (code) { + 1 => _decode(payload, (r) => OutOfStockException(r.readString(), message)), + 2 => SpoiledException(message), + > 0 => PantryException(code, message), + _ => _runtimeException(code, message, payload), + }; + +/// The value buffer of [e]'s fields, or null for a code without any. +Uint8List? _payloadOfPantryException(PantryException e) { + final w = _BufferWriter(); + switch (e) { + case OutOfStockException(): + w.writeString(e.item); + default: + return null; + } + return w.takeBytes(); +} + +/// Task priority levels +enum Priority { + /// Low priority + low(0), + /// Normal priority + normal(1), + /// High priority + high(2); + + const Priority(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Priority fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Priority discriminant')); +} + +/// Kitchen sink struct exercising every field feature +final class Item { + /// Creates an [Item]. + Item({required this.id, required this.name, required this.count, required this.enabled, required this.ratio, required this.nick, required this.payload, required this.tags, required this.attrs, this.parent, required this.token, required this.priority, this.gadget}); + + /// Stable identifier + final int id; + /// Display name + final String name; + /// Initial count + final int count; + /// Whether the item is enabled + final bool enabled; + /// Scaling ratio + final double ratio; + final int nick; + final Uint8List payload; + final List tags; + final Map attrs; + final int? parent; + /// Cross-module struct reference + final Token token; + final Priority priority; + /// An optional object inside a record + final Gadget? gadget; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Item && + id == other.id && + name == other.name && + count == other.count && + enabled == other.enabled && + ratio == other.ratio && + nick == other.nick && + _deepEquals(payload, other.payload) && + _deepEquals(tags, other.tags) && + _deepEquals(attrs, other.attrs) && + parent == other.parent && + token == other.token && + priority == other.priority && + gadget == other.gadget; + + @override + int get hashCode => Object.hashAll([Item, id, name, count, enabled, ratio, nick, _deepHash(payload), _deepHash(tags), _deepHash(attrs), parent, token, priority, gadget]); + + @override + String toString() => 'Item(id: $id, name: $name, count: $count, enabled: $enabled, ratio: $ratio, nick: $nick, payload: $payload, tags: $tags, attrs: $attrs, parent: $parent, token: $token, priority: $priority, gadget: $gadget)'; +} + +void _pack_Item(_BufferWriter w, Item v) { + w.writeI64(v.id); + w.writeString(v.name); + w.writeI32(v.count); + w.writeBool(v.enabled); + w.writeF64(v.ratio); + w.writeU32(v.nick); + w.writeBytes(v.payload); + _pack_list_string(w, v.tags); + _pack_map_string_string(w, v.attrs); + _pack_opt_i64(w, v.parent); + _pack_Token(w, v.token); + w.writeI32(v.priority.value); + _pack_opt_Gadget(w, v.gadget); +} + +Item _unpack_Item(_BufferReader r) => Item( + id: r.readI64(), + name: r.readString(), + count: r.readI32(), + enabled: r.readBool(), + ratio: r.readF64(), + nick: r.readU32(), + payload: r.readBytes(), + tags: _unpack_list_string(r), + attrs: _unpack_map_string_string(r), + parent: _unpack_opt_i64(r), + token: _unpack_Token(r), + priority: Priority.fromValue(r.readI32()), + gadget: _unpack_opt_Gadget(r), + ); + +/// Consumer-implemented observer of ready items +/// +/// A callback interface: implement it and pass an instance to any +/// function taking a [ReadyListener]. The library keeps the instance alive +/// until the producer releases it. +/// +/// Methods that return a value run synchronously on this isolate's +/// thread, during the call from Dart that led the producer to call them. +/// The producer refuses to call one from any other thread: the method +/// doesn't run, and the producer sees it fail with +/// [NativeException.foreignCode]. An exception a method throws fails the +/// producer's call (see each method). Void methods may be called from any +/// thread: they're delivered asynchronously on this isolate's event loop, +/// in the zone that passed the instance, where a thrown exception is an +/// uncaught error of that zone. Object arguments are owned by the +/// implementation. +abstract class ReadyListener { + /// Fires when an item is ready + void onReady(int code, String msg); + + /// Receives the item itself and says whether to keep listening + /// + /// An exception fails the producer's call with [NativeException.foreignCode]. + bool onItem(Item item, Gadget gadget); + + /// A display label, failing with a kitchen error + /// + /// Throw a [KitchenException] to fail the producer's call with its code and fields. + String label(); + + /// The most recent item the listener kept, if any + /// + /// An exception fails the producer's call with [NativeException.foreignCode]. + Item? latest(); + + /// The listener's favorite gadget + /// + /// An exception fails the producer's call with [NativeException.foreignCode]. + Gadget favorite(); +} + +typedef _KitchenSinkKitchenReadyListenerOnReady = Void Function(Pointer, Int32, Pointer, Size, Pointer<_Error>); +typedef _KitchenSinkKitchenReadyListenerOnItem = Bool Function(Pointer, Pointer, Size, Pointer, Pointer<_Error>); +typedef _KitchenSinkKitchenReadyListenerLabel = Void Function(Pointer, Pointer>, Pointer, Pointer<_Error>); +typedef _KitchenSinkKitchenReadyListenerLatest = Void Function(Pointer, Pointer>, Pointer, Pointer<_Error>); +typedef _KitchenSinkKitchenReadyListenerFavorite = Pointer Function(Pointer, Pointer<_Error>); + +// The C `kitchen_sink_kitchen_ReadyListener_vtable` layout. +final class _ReadyListenerVtable extends Struct { + @Uint32() + external int size; + @Uint32() + external int flags; + external Pointer)>> free; + external Pointer> onReady; + external Pointer> onItem; + external Pointer> label; + external Pointer> latest; + external Pointer> favorite; +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.on_ready`: forwarded to the isolate. +void _readyListenerOnReady(Pointer ctx, int code, Pointer msgPtr, int msgLen, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 0, 2) + ..int64(code) + ..bytes(msgPtr, msgLen) + ..send(); +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.on_item`. +bool _readyListenerOnItem(Pointer ctx, Pointer itemPtr, int itemLen, Pointer gadget, Pointer<_Error> outErr) { + try { + final _item = _decode(_copyBytes(itemPtr, itemLen), _unpack_Item); + final _gadget = Gadget._(gadget); + return (_callbackTarget(ctx) as ReadyListener).onItem(_item, _gadget); + } catch (e) { + _reportCallbackError(outErr, NativeException.foreignCode, e); + } + return false; +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.label`. +void _readyListenerLabel(Pointer ctx, Pointer> outPtr, Pointer outLen, Pointer<_Error> outErr) { + try { + final _result = (_callbackTarget(ctx) as ReadyListener).label(); + _handOver(utf8.encode(_result), outPtr, outLen); + } on KitchenException catch (e) { + _failCallback(outErr, e.code, e.message, null); + } catch (e) { + _reportCallbackError(outErr, NativeException.genericCode, e); + } +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.latest`. +void _readyListenerLatest(Pointer ctx, Pointer> outPtr, Pointer outLen, Pointer<_Error> outErr) { + try { + final _result = (_callbackTarget(ctx) as ReadyListener).latest(); + _handOver(_encode(_result, _pack_opt_Item), outPtr, outLen); + } catch (e) { + _reportCallbackError(outErr, NativeException.foreignCode, e); + } +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.favorite`. +Pointer _readyListenerFavorite(Pointer ctx, Pointer<_Error> outErr) { + try { + final _result = (_callbackTarget(ctx) as ReadyListener).favorite(); + return _result._cloneRef(); + } catch (e) { + _reportCallbackError(outErr, NativeException.foreignCode, e); + } + return nullptr; +} + +void _readyListenerDispatch(Object impl, int method, List message) { + final target = impl as ReadyListener; + switch (method) { + case 0: { + final a2 = message[2]! as int; + final a3 = utf8.decode(message[3]! as Uint8List); + target.onReady(a2, a3); + } + } +} + +final Pointer<_ReadyListenerVtable> _readyListenerVtable = () { + final vtable = calloc<_ReadyListenerVtable>(); + vtable.ref + ..size = sizeOf<_ReadyListenerVtable>() + ..flags = _vtableThreadAffine + ..free = _callbackFree + ..onReady = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerOnReady>.isolateGroupBound(_readyListenerOnReady)) + ..onItem = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerOnItem>.isolateLocal(_readyListenerOnItem, exceptionalReturn: false)) + ..label = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerLabel>.isolateLocal(_readyListenerLabel)) + ..latest = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerLatest>.isolateLocal(_readyListenerLatest)) + ..favorite = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerFavorite>.isolateLocal(_readyListenerFavorite)); + return vtable; +}(); + +/// Consumer-implemented source of scalar samples +/// +/// A callback interface: implement it and pass an instance to any +/// function taking a [Sampler]. The library keeps the instance alive +/// until the producer releases it. +/// +/// Methods that return a value run synchronously on this isolate's +/// thread, during the call from Dart that led the producer to call them. +/// The producer refuses to call one from any other thread: the method +/// doesn't run, and the producer sees it fail with +/// [NativeException.foreignCode]. An exception a method throws fails the +/// producer's call (see each method). Void methods may be called from any +/// thread: they're delivered asynchronously on this isolate's event loop, +/// in the zone that passed the instance, where a thrown exception is an +/// uncaught error of that zone. Object arguments are owned by the +/// implementation. +abstract class Sampler { + /// The cutoff for a level, if the sampler has one + /// + /// An exception fails the producer's call with [NativeException.foreignCode]. + double? threshold(int? level); + + /// Up to `limit` samples, failing when the pantry is empty + /// + /// Throw a [PantryException] to fail the producer's call with its code and fields. + List samples(int? limit, List weights); + + /// A free-form description, failing with an untyped error + /// + /// Throw any exception to fail the producer's call with its message. + String describe(); +} + +typedef _KitchenSinkKitchenSamplerThreshold = Bool Function(Pointer, Bool, Uint32, Pointer, Pointer<_Error>); +typedef _KitchenSinkKitchenSamplerSamples = Void Function(Pointer, Bool, Uint32, Pointer, Size, Pointer>, Pointer, Pointer<_Error>); +typedef _KitchenSinkKitchenSamplerDescribe = Void Function(Pointer, Pointer>, Pointer, Pointer<_Error>); + +// The C `kitchen_sink_kitchen_Sampler_vtable` layout. +final class _SamplerVtable extends Struct { + @Uint32() + external int size; + @Uint32() + external int flags; + external Pointer)>> free; + external Pointer> threshold; + external Pointer> samples; + external Pointer> describe; +} + +// Vtable entry `kitchen_sink_kitchen_Sampler_vtable.threshold`. +bool _samplerThreshold(Pointer ctx, bool hasLevel, int level, Pointer outValue, Pointer<_Error> outErr) { + try { + final _result = (_callbackTarget(ctx) as Sampler).threshold(hasLevel ? level : null); + if (_result == null) return false; + outValue.value = _result; + return true; + } catch (e) { + _reportCallbackError(outErr, NativeException.foreignCode, e); + } + return false; +} + +// Vtable entry `kitchen_sink_kitchen_Sampler_vtable.samples`. +void _samplerSamples(Pointer ctx, bool hasLimit, int limit, Pointer weightsPtr, int weightsLen, Pointer> outPtr, Pointer outLen, Pointer<_Error> outErr) { + try { + final _result = (_callbackTarget(ctx) as Sampler).samples(hasLimit ? limit : null, _copyF64s(weightsPtr, weightsLen)); + _handOverI16s(_result, outPtr, outLen); + } on PantryException catch (e) { + _failCallback(outErr, e.code, e.message, _payloadOfPantryException(e)); + } catch (e) { + _reportCallbackError(outErr, NativeException.genericCode, e); + } +} + +// Vtable entry `kitchen_sink_kitchen_Sampler_vtable.describe`. +void _samplerDescribe(Pointer ctx, Pointer> outPtr, Pointer outLen, Pointer<_Error> outErr) { + try { + final _result = (_callbackTarget(ctx) as Sampler).describe(); + _handOver(utf8.encode(_result), outPtr, outLen); + } catch (e) { + _reportCallbackError(outErr, NativeException.genericCode, e); + } +} + +void _samplerDispatch(Object impl, int method, List message) { + // Every Sampler method returns a value: nothing is forwarded. +} + +final Pointer<_SamplerVtable> _samplerVtable = () { + final vtable = calloc<_SamplerVtable>(); + vtable.ref + ..size = sizeOf<_SamplerVtable>() + ..flags = _vtableThreadAffine + ..free = _callbackFree + ..threshold = _pin(NativeCallable<_KitchenSinkKitchenSamplerThreshold>.isolateLocal(_samplerThreshold, exceptionalReturn: false)) + ..samples = _pin(NativeCallable<_KitchenSinkKitchenSamplerSamples>.isolateLocal(_samplerSamples)) + ..describe = _pin(NativeCallable<_KitchenSinkKitchenSamplerDescribe>.isolateLocal(_samplerDescribe)); + return vtable; +}(); + +final _kitchenSinkKitchenGadgetClone = _lib.lookupFunction< + Pointer Function(Pointer), + Pointer Function(Pointer)>('kitchen_sink_kitchen_Gadget_clone'); + +final _kitchenSinkKitchenGadgetDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_Gadget_destroy'); +final _kitchenSinkKitchenGadgetDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_Gadget_destroy')); + +final _kitchenSinkKitchenGadgetNew = _lib.lookupFunction< + Pointer Function(Int64, Pointer<_Error>), + Pointer Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_new'); + +final _kitchenSinkKitchenGadgetDescribe = _lib.lookupFunction< + Pointer Function(Pointer, Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_describe'); + +final _kitchenSinkKitchenGadgetPoke = _lib.lookupFunction< + Int32 Function(Pointer, Int32, Pointer<_Error>), + int Function(Pointer, int, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_poke'); + +final _kitchenSinkKitchenGadgetReadings = _lib.lookupFunction< + Pointer Function(Pointer, Bool, Uint32, Pointer, Pointer<_Error>), + Pointer Function(Pointer, bool, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_readings'); + +final _kitchenSinkKitchenGadgetVersion = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_version'); + +/// A pocket-sized interface exercising the object surface +/// +/// A reference-counted native object. Each instance holds one strong +/// reference, released by [dispose] or, when an undisposed instance is +/// collected, by a finalizer. +final class Gadget extends _NativeObject { + Gadget._(super._ptr); + + @override + NativeFinalizer get _finalizer => _kitchenSinkKitchenGadgetDestroyFinalizer; + + @override + void _destroy(Pointer ptr) => _kitchenSinkKitchenGadgetDestroy(ptr); + + @override + Pointer _clone(Pointer ptr) => _kitchenSinkKitchenGadgetClone(ptr); + + /// Create a gadget with the given id + /// + /// The returned object is owned by the caller: call its `dispose()` + /// when you're done with it. + factory Gadget(int id) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGadgetNew(id, _f.err); + _f.check(); + return Gadget._(_result); + } finally { + _f.release(); + } + } + + /// Render the gadget as a human-readable string + String describe() { + final _self = _enter(); + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGadgetDescribe(_self, _f.slot0.cast(), _f.err); + _f.check(); + return _takeString(_result, _f.slot0.cast().value); + } finally { + _f.release(); + _leave(); + } + } + + /// Poke the gadget a number of times, failing on a negative count + /// + /// Throws [KitchenException] (or a subclass for one of its codes) on failure. + int poke(int times) { + final _self = _enter(); + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGadgetPoke(_self, times, _f.err); + _f.check(_mapKitchenException); + return _result; + } finally { + _f.release(); + _leave(); + } + } + + /// The gadget's sensor readings, optionally limited + /// + /// Throws a [NativeException] with [NativeException.genericCode] on failure. + List readings(int? limit) { + final _self = _enter(); + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGadgetReadings(_self, limit != null, limit ?? 0, _f.slot0.cast(), _f.err); + _f.check(_runtimeException); + return _takeF64s(_result, _f.slot0.cast().value); + } finally { + _f.release(); + _leave(); + } + } + + /// The gadget subsystem version string + static String version() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGadgetVersion(_f.slot0.cast(), _f.err); + _f.check(); + return _takeString(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } + } +} + +final _kitchenSinkKitchenBoolId = _lib.lookupFunction< + Bool Function(Bool, Pointer<_Error>), + bool Function(bool, Pointer<_Error>)>('kitchen_sink_kitchen_bool_id'); + +/// Identity for bool +bool boolId(bool x) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenBoolId(x, _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenI32Id = _lib.lookupFunction< + Int32 Function(Int32, Pointer<_Error>), + int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_i32_id'); + +int i32Id(int x) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenI32Id(x, _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenU32Id = _lib.lookupFunction< + Uint32 Function(Uint32, Pointer<_Error>), + int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_u32_id'); + +int u32Id(int x) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenU32Id(x, _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenI64Id = _lib.lookupFunction< + Int64 Function(Int64, Pointer<_Error>), + int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_i64_id'); + +int i64Id(int x) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenI64Id(x, _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenF64Id = _lib.lookupFunction< + Double Function(Double, Pointer<_Error>), + double Function(double, Pointer<_Error>)>('kitchen_sink_kitchen_f64_id'); + +double f64Id(double x) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenF64Id(x, _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenEchoString = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_echo_string'); + +String echoString(String s) { + final _f = _Frame.take(); + try { + final _sBytes = utf8.encode(s); + final _result = _kitchenSinkKitchenEchoString(_stage(_f.arena, _sBytes), _sBytes.length, _f.slot0.cast(), _f.err); + _f.check(); + return _takeString(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenEchoBytes = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_echo_bytes'); + +Uint8List echoBytes(List b) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenEchoBytes(_stage(_f.arena, b), b.length, _f.slot0.cast(), _f.err); + _f.check(); + return _takeBytes(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenSubscribe = _lib.lookupFunction< + Int32 Function(Pointer, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_subscribe'); + +/// Register a listener, returning how many are attached +int subscribe(ReadyListener listener) { + final _f = _Frame.take(); + try { + final _listenerCtx = _registerCallback(listener, _readyListenerDispatch); + final _result = _kitchenSinkKitchenSubscribe(_listenerCtx, _readyListenerVtable.cast(), _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenMaybeSubscribe = _lib.lookupFunction< + Bool Function(Pointer, Pointer, Pointer<_Error>), + bool Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_subscribe'); + +/// Register a listener if one is given +bool maybeSubscribe(ReadyListener? listener) { + final _f = _Frame.take(); + try { + final _listenerCtx = listener == null ? nullptr : _registerCallback(listener, _readyListenerDispatch); + final _result = _kitchenSinkKitchenMaybeSubscribe(_listenerCtx, listener == null ? nullptr : _readyListenerVtable.cast(), _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenMaybeGadget = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_gadget'); + +/// Optional object in both directions +/// +/// Returns null when the producer reports no object. A returned object +/// is owned by the caller: call its `dispose()` when you're done with it. +Gadget? maybeGadget(Gadget? fallback) { + final _f = _Frame.take(); + try { + final _fallbackPtr = fallback == null ? nullptr : _borrow(_f.arena, fallback); + final _result = _kitchenSinkKitchenMaybeGadget(_fallbackPtr, _f.err); + _f.check(); + return _result == nullptr ? null : Gadget._(_result); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenGadgets = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_gadgets'); + +/// A list of objects +List gadgets() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGadgets(_f.slot0.cast(), _f.err); + _f.check(); + return _decode(_takeBytes(_result, _f.slot0.cast().value), _unpack_list_Gadget); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenStreamGadgets = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_gadgets'); + +final _kitchenSinkKitchenStreamGadgetsIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer<_Error>)>('kitchen_sink_kitchen_StreamGadgetsIterator_next'); + +final _kitchenSinkKitchenStreamGadgetsIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_StreamGadgetsIterator_destroy'); +final _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_StreamGadgetsIterator_destroy')); + +/// An iterator over objects +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +/// +/// Each element is owned by the caller: call its `dispose()` when +/// you're done with it. +Iterable streamGadgets() sync* { + final Pointer _iter; + final _f = _Frame.take(); + try { + _iter = _kitchenSinkKitchenStreamGadgets(_f.err); + _f.check(); + } finally { + _f.release(); + } + final _anchor = _IteratorAnchor(); + _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _step = _Frame.take(); + final Gadget _item; + try { + final _more = _kitchenSinkKitchenStreamGadgetsIteratorNext(_iter, _step.slot0.cast(), _step.err); + _step.check(); + if (_more == 0) break; + _item = Gadget._(_step.slot0.cast>().value); + } finally { + _step.release(); + } + yield _item; + } + } finally { + _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer.detach(_anchor); + _kitchenSinkKitchenStreamGadgetsIteratorDestroy(_iter); + } +} + +final _kitchenSinkKitchenMaybeItem = _lib.lookupFunction< + Pointer Function(Int64, Pointer, Pointer<_Error>), + Pointer Function(int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_item'); + +/// Optional struct return +/// +/// Throws [KitchenException] (or a subclass for one of its codes) on failure. +Item? maybeItem(int id) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenMaybeItem(id, _f.slot0.cast(), _f.err); + _f.check(_mapKitchenException); + return _decode(_takeBytes(_result, _f.slot0.cast().value), _unpack_opt_Item); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenListItems = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_list_items'); + +/// List of structs +List listItems() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenListItems(_f.slot0.cast(), _f.err); + _f.check(); + return _decode(_takeBytes(_result, _f.slot0.cast().value), _unpack_list_Item); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenGetAttrs = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_get_attrs'); + +/// Map return type +Map getAttrs() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenGetAttrs(_f.slot0.cast(), _f.err); + _f.check(); + return _decode(_takeBytes(_result, _f.slot0.cast().value), _unpack_map_string_i32); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenStreamItems = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_items'); + +final _kitchenSinkKitchenStreamItemsIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_StreamItemsIterator_next'); + +final _kitchenSinkKitchenStreamItemsIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_StreamItemsIterator_destroy'); +final _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_StreamItemsIterator_destroy')); + +/// Iterator return type +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable streamItems() sync* { + final Pointer _iter; + final _f = _Frame.take(); + try { + _iter = _kitchenSinkKitchenStreamItems(_f.err); + _f.check(); + } finally { + _f.release(); + } + final _anchor = _IteratorAnchor(); + _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _step = _Frame.take(); + final Item _item; + try { + final _more = _kitchenSinkKitchenStreamItemsIteratorNext(_iter, _step.slot0.cast(), _step.slot1.cast(), _step.err); + _step.check(); + if (_more == 0) break; + _item = _decode(_takeBytes(_step.slot0.cast>().value, _step.slot1.cast().value), _unpack_Item); + } finally { + _step.release(); + } + yield _item; + } + } finally { + _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer.detach(_anchor); + _kitchenSinkKitchenStreamItemsIteratorDestroy(_iter); + } +} + +final _kitchenSinkKitchenCrossModuleToken = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_cross_module_token'); + +/// Returns the shared Token type from another module +Token crossModuleToken() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenCrossModuleToken(_f.slot0.cast(), _f.err); + _f.check(); + return _decode(_takeBytes(_result, _f.slot0.cast().value), _unpack_Token); + } finally { + _f.release(); + } +} + +typedef _KitchenSinkKitchenDoAsyncCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _kitchenSinkKitchenDoAsync = _lib.lookupFunction< + Void Function(Pointer, Size, Pointer>, Pointer), + void Function(Pointer, int, Pointer>, Pointer)>('kitchen_sink_kitchen_do_async'); + +/// Async operation +Future doAsync(String input) { + final _completer = Completer(); + late final NativeCallable<_KitchenSinkKitchenDoAsyncCallback> _callback; + _callback = NativeCallable<_KitchenSinkKitchenDoAsyncCallback>.listener(( + Pointer context, Pointer<_Error> err, Pointer resultPtr, int resultLen) { + _callback.close(); + try { + if (err != nullptr) throw _takeAsyncError(err, _trap); + _completer.complete(_takeString(resultPtr, resultLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + final _f = _Frame.take(); + try { + final _inputBytes = utf8.encode(input); + _kitchenSinkKitchenDoAsync(_stage(_f.arena, _inputBytes), _inputBytes.length, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } finally { + _f.release(); + } + return _completer.future; +} + +typedef _KitchenSinkKitchenDoCancellableCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _kitchenSinkKitchenDoCancellable = _lib.lookupFunction< + Void Function(Pointer, Size, Pointer, Pointer>, Pointer), + void Function(Pointer, int, Pointer, Pointer>, Pointer)>('kitchen_sink_kitchen_do_cancellable'); + +/// Cancellable async operation +/// +/// Cancelling [cancelToken] completes the returned future with a +/// [CancelledException]. +Future doCancellable(String input, {CancelToken? cancelToken}) { + final _completer = Completer(); + final _cancel = _NativeCancel.bind(cancelToken); + late final NativeCallable<_KitchenSinkKitchenDoCancellableCallback> _callback; + _callback = NativeCallable<_KitchenSinkKitchenDoCancellableCallback>.listener(( + Pointer context, Pointer<_Error> err, Pointer resultPtr, int resultLen) { + _callback.close(); + _cancel?.release(); + try { + if (err != nullptr) throw _takeAsyncError(err, _trap); + _completer.complete(_takeString(resultPtr, resultLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + final _f = _Frame.take(); + try { + final _inputBytes = utf8.encode(input); + _kitchenSinkKitchenDoCancellable(_stage(_f.arena, _inputBytes), _inputBytes.length, _cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + _cancel?.release(); + rethrow; + } finally { + _f.release(); + } + return _completer.future; +} + +final _kitchenSinkKitchenLegacyOp = _lib.lookupFunction< + Int32 Function(Pointer<_Error>), + int Function(Pointer<_Error>)>('kitchen_sink_kitchen_legacy_op'); + +/// Legacy operation kept for compatibility +@Deprecated('Use `newOp` instead') +int legacyOp() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenLegacyOp(_f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenNewOp = _lib.lookupFunction< + Int32 Function(Pointer<_Error>), + int Function(Pointer<_Error>)>('kitchen_sink_kitchen_new_op'); + +/// Replacement for [legacyOp] +int newOp() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenNewOp(_f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenOptI32Id = _lib.lookupFunction< + Bool Function(Bool, Int32, Pointer, Pointer<_Error>), + bool Function(bool, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_opt_i32_id'); + +/// Optional scalar in both directions +int? optI32Id(int? x) { + final _f = _Frame.take(); + try { + final _present = _kitchenSinkKitchenOptI32Id(x != null, x ?? 0, _f.slot0.cast(), _f.err); + _f.check(); + return _present ? _f.slot0.cast().value : null; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenOptBoolId = _lib.lookupFunction< + Bool Function(Bool, Bool, Pointer, Pointer<_Error>), + bool Function(bool, bool, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_opt_bool_id'); + +bool? optBoolId(bool? x) { + final _f = _Frame.take(); + try { + final _present = _kitchenSinkKitchenOptBoolId(x != null, x ?? false, _f.slot0.cast(), _f.err); + _f.check(); + return _present ? _f.slot0.cast().value : null; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenOptPriority = _lib.lookupFunction< + Bool Function(Bool, Int32, Pointer, Pointer<_Error>), + bool Function(bool, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_opt_priority'); + +/// Optional C-style enum in both directions +Priority? optPriority(Priority? p) { + final _f = _Frame.take(); + try { + final _present = _kitchenSinkKitchenOptPriority(p != null, p?.value ?? 0, _f.slot0.cast(), _f.err); + _f.check(); + return _present ? Priority.fromValue(_f.slot0.cast().value) : null; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenOptMixed = _lib.lookupFunction< + Bool Function(Double, Bool, Float, Bool, Int64, Pointer, Size, Pointer, Pointer<_Error>), + bool Function(double, bool, double, bool, int, Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_opt_mixed'); + +/// Optional scalars next to ordinary parameters +int? optMixed(double base, double? scale, int? offset, String? label) { + final _f = _Frame.take(); + try { + final _labelBytes = _encode(label, _pack_opt_string); + final _present = _kitchenSinkKitchenOptMixed(base, scale != null, scale ?? 0.0, offset != null, offset ?? 0, _stage(_f.arena, _labelBytes), _labelBytes.length, _f.slot0.cast(), _f.err); + _f.check(); + return _present ? _f.slot0.cast().value : null; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenSumF64 = _lib.lookupFunction< + Double Function(Pointer, Size, Pointer<_Error>), + double Function(Pointer, int, Pointer<_Error>)>('kitchen_sink_kitchen_sum_f64'); + +/// A typed-array parameter +double sumF64(List xs) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenSumF64(_stageF64s(_f.arena, xs), xs.length, _f.err); + _f.check(); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenScaleI32 = _lib.lookupFunction< + Pointer Function(Pointer, Size, Int32, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_scale_i32'); + +/// Typed arrays in both directions +List scaleI32(List xs, int factor) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenScaleI32(_stageI32s(_f.arena, xs), xs.length, factor, _f.slot0.cast(), _f.err); + _f.check(); + return _takeI32s(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenIdsU64 = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_ids_u64'); + +List idsU64(List ids) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenIdsU64(_stageU64s(_f.arena, ids), ids.length, _f.slot0.cast(), _f.err); + _f.check(); + return _takeU64s(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenMixedSlices = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_mixed_slices'); + +/// Every slice element type +List mixedSlices(List a, List b, List c, List d, List e, List f) { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenMixedSlices(_stageI8s(_f.arena, a), a.length, _stageI16s(_f.arena, b), b.length, _stageU16s(_f.arena, c), c.length, _stageU32s(_f.arena, d), d.length, _stageI64s(_f.arena, e), e.length, _stageF32s(_f.arena, f), f.length, _f.slot0.cast(), _f.err); + _f.check(); + return _takeU32s(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +typedef _KitchenSinkKitchenAsyncMaybeCountCallback = Void Function(Pointer, Pointer<_Error>, Bool, Int64); + +final _kitchenSinkKitchenAsyncMaybeCount = _lib.lookupFunction< + Void Function(Pointer>, Pointer), + void Function(Pointer>, Pointer)>('kitchen_sink_kitchen_async_maybe_count'); + +/// Async returning an optional scalar +Future asyncMaybeCount() { + final _completer = Completer(); + late final NativeCallable<_KitchenSinkKitchenAsyncMaybeCountCallback> _callback; + _callback = NativeCallable<_KitchenSinkKitchenAsyncMaybeCountCallback>.listener(( + Pointer context, Pointer<_Error> err, bool hasResult, int result) { + _callback.close(); + try { + if (err != nullptr) throw _takeAsyncError(err, _trap); + _completer.complete(hasResult ? result : null); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _kitchenSinkKitchenAsyncMaybeCount(_callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +typedef _KitchenSinkKitchenAsyncSamplesCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _kitchenSinkKitchenAsyncSamples = _lib.lookupFunction< + Void Function(Uint32, Pointer, Pointer>, Pointer), + void Function(int, Pointer, Pointer>, Pointer)>('kitchen_sink_kitchen_async_samples'); + +/// Async returning a typed array +/// +/// Cancelling [cancelToken] completes the returned future with a +/// [CancelledException]. +Future> asyncSamples(int n, {CancelToken? cancelToken}) { + final _completer = Completer>(); + final _cancel = _NativeCancel.bind(cancelToken); + late final NativeCallable<_KitchenSinkKitchenAsyncSamplesCallback> _callback; + _callback = NativeCallable<_KitchenSinkKitchenAsyncSamplesCallback>.listener(( + Pointer context, Pointer<_Error> err, Pointer resultPtr, int resultLen) { + _callback.close(); + _cancel?.release(); + try { + if (err != nullptr) throw _takeAsyncError(err, _trap); + _completer.complete(_takeF32s(resultPtr, resultLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _kitchenSinkKitchenAsyncSamples(n, _cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + _cancel?.release(); + rethrow; + } + return _completer.future; +} + +final _kitchenSinkKitchenStreamMaybe = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_maybe'); + +final _kitchenSinkKitchenStreamMaybeIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_StreamMaybeIterator_next'); + +final _kitchenSinkKitchenStreamMaybeIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_StreamMaybeIterator_destroy'); +final _kitchenSinkKitchenStreamMaybeIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_StreamMaybeIterator_destroy')); + +/// Iterator of optional scalars +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable streamMaybe() sync* { + final Pointer _iter; + final _f = _Frame.take(); + try { + _iter = _kitchenSinkKitchenStreamMaybe(_f.err); + _f.check(); + } finally { + _f.release(); + } + final _anchor = _IteratorAnchor(); + _kitchenSinkKitchenStreamMaybeIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _step = _Frame.take(); + final int? _item; + try { + final _more = _kitchenSinkKitchenStreamMaybeIteratorNext(_iter, _step.slot0.cast(), _step.slot1.cast(), _step.err); + _step.check(); + if (_more == 0) break; + _item = _step.slot0.cast().value ? _step.slot1.cast().value : null; + } finally { + _step.release(); + } + yield _item; + } + } finally { + _kitchenSinkKitchenStreamMaybeIteratorDestroyFinalizer.detach(_anchor); + _kitchenSinkKitchenStreamMaybeIteratorDestroy(_iter); + } +} + +final _kitchenSinkKitchenStreamChunks = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_chunks'); + +final _kitchenSinkKitchenStreamChunksIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_StreamChunksIterator_next'); + +final _kitchenSinkKitchenStreamChunksIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_StreamChunksIterator_destroy'); +final _kitchenSinkKitchenStreamChunksIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_StreamChunksIterator_destroy')); + +/// Iterator of typed arrays +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable> streamChunks() sync* { + final Pointer _iter; + final _f = _Frame.take(); + try { + _iter = _kitchenSinkKitchenStreamChunks(_f.err); + _f.check(); + } finally { + _f.release(); + } + final _anchor = _IteratorAnchor(); + _kitchenSinkKitchenStreamChunksIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _step = _Frame.take(); + final List _item; + try { + final _more = _kitchenSinkKitchenStreamChunksIteratorNext(_iter, _step.slot0.cast(), _step.slot1.cast(), _step.err); + _step.check(); + if (_more == 0) break; + _item = _takeI32s(_step.slot0.cast>().value, _step.slot1.cast().value); + } finally { + _step.release(); + } + yield _item; + } + } finally { + _kitchenSinkKitchenStreamChunksIteratorDestroyFinalizer.detach(_anchor); + _kitchenSinkKitchenStreamChunksIteratorDestroy(_iter); + } +} + +final _kitchenSinkKitchenSampleWith = _lib.lookupFunction< + Pointer Function(Pointer, Pointer, Bool, Uint32, Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer, bool, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_sample_with'); + +/// Drive a consumer-implemented sampler +/// +/// Throws [PantryException] (or a subclass for one of its codes) on failure. +List sampleWith(Sampler sampler, int? level) { + final _f = _Frame.take(); + try { + final _samplerCtx = _registerCallback(sampler, _samplerDispatch); + final _result = _kitchenSinkKitchenSampleWith(_samplerCtx, _samplerVtable.cast(), level != null, level ?? 0, _f.slot0.cast(), _f.err); + _f.check(_mapPantryException); + return _takeI16s(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenTakeFromPantry = _lib.lookupFunction< + Int32 Function(Pointer, Size, Pointer<_Error>), + int Function(Pointer, int, Pointer<_Error>)>('kitchen_sink_kitchen_take_from_pantry'); + +/// Fails with the second domain +/// +/// Throws [PantryException] (or a subclass for one of its codes) on failure. +int takeFromPantry(String item) { + final _f = _Frame.take(); + try { + final _itemBytes = utf8.encode(item); + final _result = _kitchenSinkKitchenTakeFromPantry(_stage(_f.arena, _itemBytes), _itemBytes.length, _f.err); + _f.check(_mapPantryException); + return _result; + } finally { + _f.release(); + } +} + +final _kitchenSinkKitchenParseAny = _lib.lookupFunction< + Int64 Function(Pointer, Size, Pointer<_Error>), + int Function(Pointer, int, Pointer<_Error>)>('kitchen_sink_kitchen_parse_any'); + +/// Fails with an untyped error +/// +/// Throws a [NativeException] with [NativeException.genericCode] on failure. +int parseAny(String text) { + final _f = _Frame.take(); + try { + final _textBytes = utf8.encode(text); + final _result = _kitchenSinkKitchenParseAny(_stage(_f.arena, _textBytes), _textBytes.length, _f.err); + _f.check(_runtimeException); + return _result; + } finally { + _f.release(); + } +} + +// END kitchen.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_kitchen_nested_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_kitchen_nested_dart.snap new file mode 100644 index 00000000..460a9faa --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_kitchen_nested_dart.snap @@ -0,0 +1,29 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field +part of '../../kitchen_sink.dart'; + +// ── Module `kitchen.nested` ── + +final _kitchenSinkKitchenNestedHello = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_nested_hello'); + +/// Trivial nested-module function +String hello() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkKitchenNestedHello(_f.slot0.cast(), _f.err); + _f.check(); + return _takeString(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +// END nested.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_runtime_composites_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_runtime_composites_dart.snap new file mode 100644 index 00000000..6b1719f8 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_runtime_composites_dart.snap @@ -0,0 +1,69 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field +part of '../../kitchen_sink.dart'; + +// ── Composite codecs ── +// One writer and reader per optional, list, or map type that crosses +// inside a value buffer. + +void _pack_list_string(_BufferWriter w, List v) => + w.writeList(v, (e) => w.writeString(e)); + +List _unpack_list_string(_BufferReader r) => + r.readList(() => r.readString()); + +void _pack_map_string_string(_BufferWriter w, Map v) => + w.writeMap(v, (k) => w.writeString(k), (e) => w.writeString(e)); + +Map _unpack_map_string_string(_BufferReader r) => + r.readMap(() => r.readString(), () => r.readString()); + +void _pack_opt_i64(_BufferWriter w, int? v) => + w.writeOptional(v, (e) => w.writeI64(e)); + +int? _unpack_opt_i64(_BufferReader r) => + r.readOptional(() => r.readI64()); + +void _pack_opt_Gadget(_BufferWriter w, Gadget? v) => + w.writeOptional(v, (e) => w.writeU64(e._cloneRef().address)); + +Gadget? _unpack_opt_Gadget(_BufferReader r) => + r.readOptional(() => Gadget._(Pointer.fromAddress(r.readU64()))); + +void _pack_list_Gadget(_BufferWriter w, List v) => + w.writeList(v, (e) => w.writeU64(e._cloneRef().address)); + +List _unpack_list_Gadget(_BufferReader r) => + r.readList(() => Gadget._(Pointer.fromAddress(r.readU64()))); + +void _pack_opt_Item(_BufferWriter w, Item? v) => + w.writeOptional(v, (e) => _pack_Item(w, e)); + +Item? _unpack_opt_Item(_BufferReader r) => + r.readOptional(() => _unpack_Item(r)); + +void _pack_list_Item(_BufferWriter w, List v) => + w.writeList(v, (e) => _pack_Item(w, e)); + +List _unpack_list_Item(_BufferReader r) => + r.readList(() => _unpack_Item(r)); + +void _pack_map_string_i32(_BufferWriter w, Map v) => + w.writeMap(v, (k) => w.writeString(k), (e) => w.writeI32(e)); + +Map _unpack_map_string_i32(_BufferReader r) => + r.readMap(() => r.readString(), () => r.readI32()); + +void _pack_opt_string(_BufferWriter w, String? v) => + w.writeOptional(v, (e) => w.writeString(e)); + +String? _unpack_opt_string(_BufferReader r) => + r.readOptional(() => r.readString()); + +// END composites.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_runtime_contracts_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_runtime_contracts_dart.snap new file mode 100644 index 00000000..c5b60e89 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_runtime_contracts_dart.snap @@ -0,0 +1,148 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field +part of '../../kitchen_sink.dart'; + +// ── Contract tables ── + +/// Each top-level module's contract-table symbol and the declarations +/// these bindings were generated with: (id, signature hash, path). +const List<(String, List<(int, int, String)>)> _contracts = [ + ('kitchen_sink_shared_contract', [ + // function ping() -> string + (0x709e234caa962446, 0x3894f2ca452e9f98, 'shared.ping'), + // record Token {i64, string} + (0xcfb306fa5801ee5b, 0x18e9fc204a13df6f, 'shared.Token'), + ]), + ('kitchen_sink_kitchen_contract', [ + // constructor new(i64) -> Gadget + (0x01d4f09bbddc986f, 0xffcf2bd3b04aa002, 'kitchen.Gadget.new'), + // callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + (0x02bbd5f31c953704, 0x2cfa679ea1373e09, 'kitchen.Sampler.samples'), + // function maybe_gadget(Gadget?) -> Gadget? + (0x10472d8fe748c2ee, 0xcc570a2649bbf7dc, 'kitchen.maybe_gadget'), + // function gadgets() -> [Gadget] + (0x11368eec07a74078, 0xfa049f78b77e146a, 'kitchen.gadgets'), + // function stream_gadgets() -> iter + (0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, 'kitchen.stream_gadgets'), + // function ids_u64([u64]) -> [u64] + (0x184bfc4ab0e3fe45, 0xb64587477022696b, 'kitchen.ids_u64'), + // function do_cancellable(string) -> string async cancellable + (0x2111b54980826dd7, 0x627bf01ab365b209, 'kitchen.do_cancellable'), + // callback_method #4 favorite() -> Gadget + (0x21c70f9371374d74, 0x25d5d88a30fe0a79, 'kitchen.ReadyListener.favorite'), + // function stream_chunks() -> iter<[i32]> + (0x23de5bd597372f46, 0xcf1bc6bb37872798, 'kitchen.stream_chunks'), + // function take_from_pantry(string) -> i32 throws PantryError + (0x249f1ddaec6a6cbe, 0x0710d6c9a4de93da, 'kitchen.take_from_pantry'), + // enum Priority {Low = 0, Normal = 1, High = 2} + (0x29dddbc60bf8d7db, 0x452a3e573d57f6db, 'kitchen.Priority'), + // function maybe_subscribe(ReadyListener?) -> bool + (0x2b227003b74a0d16, 0x5dae036b8a6aed30, 'kitchen.maybe_subscribe'), + // record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + (0x2dd66cb0064f0494, 0x74b131724dd0d867, 'kitchen.Item'), + // function u32_id(u32) -> u32 + (0x33c265bbf079b0e9, 0x97c32b764693f2cb, 'kitchen.u32_id'), + // errors KitchenErrors + (0x33e6381d01660dfe, 0x05e35a563089bf7d, 'kitchen.KitchenErrors'), + // callback_method #3 latest() -> Item? + (0x3d969ae76db6cb23, 0x65a325cfc772296b, 'kitchen.ReadyListener.latest'), + // method poke(i32) -> i32 throws KitchenErrors + (0x3f5fc99d73504b76, 0x417e4a2acd3210b3, 'kitchen.Gadget.poke'), + // callback Sampler + (0x4082f0085db28781, 0x18848e701962912e, 'kitchen.Sampler'), + // callback_method #2 describe() -> string throws any + (0x4340a6b1e7ec8c50, 0xa59d0f32a66dc7a2, 'kitchen.Sampler.describe'), + // function maybe_item(i64) -> Item? throws KitchenErrors + (0x4c5764cf109528d1, 0xb374accf14676d90, 'kitchen.maybe_item'), + // function i32_id(i32) -> i32 + (0x4de17dfb93fd494d, 0x78be3203bcd10727, 'kitchen.i32_id'), + // function stream_items() -> iter + (0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, 'kitchen.stream_items'), + // code Spoiled = 2 + (0x5dafabb7775fcb2d, 0x6c4b78c7d3f858c1, 'kitchen.PantryError.Spoiled'), + // callback_method #0 threshold(u32?) -> f64? + (0x5e0a8f6be4b00600, 0x18ea1a42036c96a0, 'kitchen.Sampler.threshold'), + // function async_samples(u32) -> [f32] async cancellable + (0x5ec0af1d4ecc6fed, 0x6540bdb74246420c, 'kitchen.async_samples'), + // function async_maybe_count() -> i64? async + (0x61a358b0571e0a4e, 0x11a24c4b50d11996, 'kitchen.async_maybe_count'), + // function bool_id(bool) -> bool + (0x68cdb9a4bd3e3dd5, 0xcde022fe6bc3b1bf, 'kitchen.bool_id'), + // function do_async(string) -> string async + (0x69e7cd703d16eb85, 0xb13cb8d39e702c79, 'kitchen.do_async'), + // method describe() -> string + (0x70865079ce41be82, 0xbe3b69464672af46, 'kitchen.Gadget.describe'), + // function echo_bytes(bytes) -> bytes + (0x71c5a1116cb7c49a, 0x1506cfe6f548fac4, 'kitchen.echo_bytes'), + // function echo_string(string) -> string + (0x735ee1e0049ee91a, 0xa713dbd1be707752, 'kitchen.echo_string'), + // function i64_id(i64) -> i64 + (0x76d7fc58f4e41d84, 0x6bdb4ccea2841d5e, 'kitchen.i64_id'), + // function stream_maybe() -> iter + (0x78d91fd617e162b8, 0xd88e2753503b36df, 'kitchen.stream_maybe'), + // function hello() -> string + (0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, 'kitchen.nested.hello'), + // function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + (0x7b3865b536da998c, 0x1c4e79d60a20e74f, 'kitchen.mixed_slices'), + // code InvalidInput = 2 + (0x7ba266dc26a58989, 0x0f86001cac0c21e8, 'kitchen.KitchenErrors.InvalidInput'), + // function new_op() -> i32 + (0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, 'kitchen.new_op'), + // function scale_i32([i32], i32) -> [i32] + (0x81ded69adee0abf2, 0x8c71141a931906f6, 'kitchen.scale_i32'), + // callback_method #2 label() -> string throws KitchenErrors + (0x81fa72769330fed6, 0x89681e305b55a4f4, 'kitchen.ReadyListener.label'), + // function f64_id(f64) -> f64 + (0x89a5e664bc25c67b, 0x357fd9b65ad879a9, 'kitchen.f64_id'), + // static version() -> string + (0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, 'kitchen.Gadget.version'), + // function opt_priority(Priority?) -> Priority? + (0x8dc42397e47614b9, 0xcddf6fede09b2e7f, 'kitchen.opt_priority'), + // callback_method #1 on_item(Item, Gadget) -> bool + (0x92f060ef68961fc1, 0x4af6ce5b4f9fa508, 'kitchen.ReadyListener.on_item'), + // function cross_module_token() -> Token + (0x974b9f3e823c1738, 0xb81b91e975695f27, 'kitchen.cross_module_token'), + // function sum_f64([f64]) -> f64 + (0x9f0f001aa85f97d5, 0x55815247f895bd45, 'kitchen.sum_f64'), + // code NotFound = 1 + (0xab2a29fd1ef5458b, 0x471a34aa4c09ae2b, 'kitchen.KitchenErrors.NotFound'), + // function list_items() -> [Item] + (0xaca0655fac15d230, 0x11c4d2b28abe07bb, 'kitchen.list_items'), + // errors PantryError + (0xb57c8e6ae3fb4383, 0x3f857851ab75366c, 'kitchen.PantryError'), + // function opt_mixed(f64, f32?, i64?, string?) -> u16? + (0xbdc795b195edd428, 0x36127462f8b0969f, 'kitchen.opt_mixed'), + // callback_method #0 on_ready(i32, string) -> void + (0xbe4821ab3c340e71, 0xe1d32e38b766e623, 'kitchen.ReadyListener.on_ready'), + // function opt_i32_id(i32?) -> i32? + (0xc2ad53f2a6a36323, 0x071e29abee823f17, 'kitchen.opt_i32_id'), + // function parse_any(string) -> i64 throws any + (0xc7ab409554fd3d7b, 0xfea12b6bc219fc46, 'kitchen.parse_any'), + // function sample_with(Sampler, u32?) -> [i16] throws PantryError + (0xc82a7f2d85f99f4e, 0xa9bcb8bacedb036c, 'kitchen.sample_with'), + // function legacy_op() -> i32 + (0xc883b4afae20543e, 0xc40560e5411ff392, 'kitchen.legacy_op'), + // code OutOfStock = 1 {string} + (0xd180bf646bff59e2, 0xa4515a6d00d7c600, 'kitchen.PantryError.OutOfStock'), + // function opt_bool_id(bool?) -> bool? + (0xe6778df0f5c1b82b, 0x38eaafdb350559e1, 'kitchen.opt_bool_id'), + // function get_attrs() -> {string:i32} + (0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, 'kitchen.get_attrs'), + // function subscribe(ReadyListener) -> i32 + (0xfae25d3391922ead, 0xcc034cf506cae182, 'kitchen.subscribe'), + // interface Gadget + (0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, 'kitchen.Gadget'), + // callback ReadyListener + (0xfed9c551fb9fd2d6, 0xe1020573f084dc1d, 'kitchen.ReadyListener'), + // method readings(u32?) -> [f64] throws any + (0xff3a99b52597dee2, 0x14c4ecac97bdfc9b, 'kitchen.Gadget.readings'), + ]), +]; + +// END contracts.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_shared_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_shared_dart.snap new file mode 100644 index 00000000..9118b6c1 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_src_shared_dart.snap @@ -0,0 +1,61 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field +part of '../kitchen_sink.dart'; + +// ── Module `shared` ── + +/// A reusable token referenced across modules +final class Token { + /// Creates a [Token]. + Token({required this.id, required this.label}); + + final int id; + final String label; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Token && + id == other.id && + label == other.label; + + @override + int get hashCode => Object.hashAll([Token, id, label]); + + @override + String toString() => 'Token(id: $id, label: $label)'; +} + +void _pack_Token(_BufferWriter w, Token v) { + w.writeI64(v.id); + w.writeString(v.label); +} + +Token _unpack_Token(_BufferReader r) => Token( + id: r.readI64(), + label: r.readString(), + ); + +final _kitchenSinkSharedPing = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_shared_ping'); + +/// Trivial cross-module helper +String ping() { + final _f = _Frame.take(); + try { + final _result = _kitchenSinkSharedPing(_f.slot0.cast(), _f.err); + _f.check(); + return _takeString(_result, _f.slot0.cast().value); + } finally { + _f.release(); + } +} + +// END shared.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap deleted file mode 100644 index 6fd0da3f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -name: kitchen_sink -version: 0.1.0 -description: 'Native bindings for kitchen_sink' - -environment: - sdk: '>=3.10.0 <4.0.0' - -dependencies: - ffi: ^2.1.0 - -# END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_nested_modules_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_nested_modules_dart.snap deleted file mode 100644 index 9cf72fa1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_nested_modules_dart.snap +++ /dev/null @@ -1,742 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element - -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -// ── Library loading ── -// The native library opens on first use and is verified before any other -// lookup: it must implement the C ABI revision these bindings were generated -// against and carry every declaration they were generated with, unchanged. - -/// The native library every binding in this file calls. -final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); - -/// The native library couldn't be loaded, or it doesn't match these -/// bindings: a different C ABI revision, or a declaration that's missing or -/// changed. Regenerate the bindings from the library you ship. -final class NativeLibraryError extends Error { - /// Creates a load failure described by [message]. - NativeLibraryError(this.message); - - /// What failed, naming the library or declaration. - final String message; - - @override - String toString() => 'NativeLibraryError: $message'; -} - -/// The platform file name of the library on the system search path. -String get _libraryFile => Platform.isMacOS - ? 'libnested_modules.dylib' - : Platform.isWindows - ? 'nested_modules.dll' - : 'libnested_modules.so'; - -DynamicLibrary _openLibrary() { - // An explicit path wins, so a caller can point at any build artifact. - final override = Platform.environment['NESTED_MODULES_LIBRARY']; - if (override != null && override.isNotEmpty) { - return _open(override); - } - // iOS links native code into the app binary. - if (Platform.isIOS) return DynamicLibrary.process(); - try { - return DynamicLibrary.open(_libraryFile); - } on ArgumentError catch (e) { - // A macOS app may link the library into its executable instead. - if (Platform.isMacOS) return DynamicLibrary.process(); - throw NativeLibraryError("can't open '$_libraryFile' ($e); set " - 'NESTED_MODULES_LIBRARY to its path'); - } -} - -DynamicLibrary _open(String path) { - try { - return DynamicLibrary.open(path); - } on ArgumentError catch (e) { - throw NativeLibraryError("can't open '$path': $e"); - } -} - -/// The C ABI revision these bindings were generated against. -const int _abiVersion = 4; - -/// One `nested_modules_contract_entry` of a module's contract table. -final class _ContractEntry extends Struct { - @Uint64() - external int id; - @Uint64() - external int hash; -} - -DynamicLibrary _verifyLibrary(DynamicLibrary lib) { - final int Function() abiVersion; - try { - abiVersion = lib.lookupFunction( - 'nested_modules_abi_version'); - } on ArgumentError { - throw NativeLibraryError( - "the native library 'nested_modules' isn't loaded; set NESTED_MODULES_LIBRARY " - 'to its path'); - } - final found = abiVersion(); - if (found != _abiVersion) { - throw NativeLibraryError('nested_modules: these bindings expect C ABI revision ' - '$_abiVersion, but the loaded library implements revision $found'); - } - for (final (symbol, expected) in _contracts) { - _checkContract(lib, symbol, expected); - } - return lib; -} - -/// Checks that every `(id, hash, path)` entry in [expected] is in the table -/// [symbol] returns with an equal hash. Entries only the library has are -/// fine: they're declarations added after these bindings were generated. -void _checkContract( - DynamicLibrary lib, String symbol, List<(int, int, String)> expected) { - final Pointer<_ContractEntry> Function(Pointer) table; - try { - table = lib.lookupFunction Function(Pointer), - Pointer<_ContractEntry> Function(Pointer)>(symbol); - } on ArgumentError { - throw NativeLibraryError( - "nested_modules: the loaded library doesn't export '$symbol'"); - } - final hashes = {}; - final len = calloc(); - try { - final entries = table(len); - for (var i = 0; i < len.value; i++) { - hashes[entries[i].id] = entries[i].hash; - } - } finally { - calloc.free(len); - } - for (final (id, hash, path) in expected) { - final found = hashes[id]; - if (found == null) { - throw NativeLibraryError('nested_modules: $path is missing from the library'); - } - if (found != hash) { - throw NativeLibraryError( - 'nested_modules: $path changed since these bindings were generated'); - } - } -} - -/// Each top-level module's contract-table symbol and the declarations -/// these bindings were generated with: (id, signature hash, path). -const List<(String, List<(int, int, String)>)> _contracts = [ - ('nested_modules_graphics_contract', [ - (0x2c29eef9327a6bd4, 0x12983d5271651389, 'graphics.Unit'), - (0x304088bbc28f0015, 0x97e6e809913be6b6, 'graphics.new_canvas'), - (0x49f8837532df9ac6, 0x32e5b27e81ee0957, 'graphics.Canvas'), - (0x55717fa027336b47, 0xb2bd3a6bdcce48c8, 'graphics.shapes.describe'), - (0x7626b91b8f1f53cd, 0x867bbf7cf0f41c74, 'graphics.shapes.geometry.compute'), - (0xb3efcb50d9a61ab7, 0x3cb0207dfad2a149, 'graphics.GraphicsError'), - (0xc62b16a9591b744b, 0x7895610c8487eb1b, 'graphics.shapes.Shape'), - (0xe13001f4983ccd35, 0x16e7de070b4a4cfc, 'graphics.shapes.geometry.Detail'), - (0xf3f658bcfc5dba62, 0x2665ccb47356e746, 'graphics.shapes.make_shape'), - ]), -]; - -// ── Errors and memory ── - -/// The C `nested_modules_error` slot every fallible call writes. A non-zero -/// `code` carries a producer-owned `message` and, for a domain error with -/// fields, a value-buffer `payload`; `nested_modules_error_clear` releases both. -final class _Error extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -final _errorClear = _lib.lookupFunction), - void Function(Pointer<_Error>)>('nested_modules_error_clear', isLeaf: true); - -final _freeBytes = _lib.lookupFunction, Size), - void Function(Pointer, int)>('nested_modules_free_bytes', isLeaf: true); - -// Synchronous calls on this isolate share one error slot and one length -// slot: each call reads (and clears) them right after it returns, before any -// other call can reuse them, including a nested call from a callback. -final Pointer<_Error> _err = calloc<_Error>(); -final Pointer _outLen = calloc(); - -/// A failure the native library reported to a function declared to throw. -/// Domain errors extend this class with a positive [code]; a negative code is -/// one of the runtime constants below. -class NativeException implements Exception { - /// Creates an exception carrying [code] and [message]. - NativeException(this.code, this.message); - - /// The producer reported an untyped error. - static const int genericCode = -1; - - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - - /// An argument or a callback's return couldn't be lifted by the producer. - static const int marshalCode = -3; - - /// A callback implementation failed; [message] carries its text. - static const int foreignCode = -4; - - /// The call was cancelled (see [CancelledException]). - static const int cancelledCode = -5; - - /// The error code: positive for a domain error, negative for a runtime - /// failure. - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => '$runtimeType($code): $message'; -} - -/// A cancellable call completed because its token was cancelled. -class CancelledException extends NativeException { - /// Creates a cancellation with the producer's [message]. - CancelledException([String message = 'cancelled']) - : super(NativeException.cancelledCode, message); -} - -/// A function that isn't declared to throw failed anyway. That's a bug in -/// the native library (a panic, an argument it couldn't lift, a callback -/// failure it let through), so it's an [Error], not an [Exception]. -final class NativeError extends Error { - /// Creates the error a failed call reported. - NativeError(this.code, this.message); - - /// The runtime code the producer reported (see [NativeException]). - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => 'NativeError($code): $message'; -} - -/// Maps a reported code, message, and payload onto the object to throw. -typedef _ErrorMapper = Object Function( - int code, String message, Uint8List payload); - -/// The mapper of a function that can't fail: every code but cancellation -/// is a [NativeError]. -Object _trap(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeError(code, message); - -/// Maps a runtime code (or one the domain doesn't declare) onto its -/// exception, for a function declared to throw. -NativeException _runtimeException(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeException(code, message); - -/// Builds the error [err] reports, using [map]. -Object _readError(Pointer<_Error> err, _ErrorMapper map) { - final e = err.ref; - final message = e.message == nullptr ? '' : e.message.toDartString(); - return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); -} - -/// Throws the error a synchronous call left in [err], clearing the slot. -/// [map] is the domain's mapper for a function declared to throw. -void _check(Pointer<_Error> err, [_ErrorMapper map = _trap]) { - if (err.ref.code == 0) return; - final error = _readError(err, map); - _errorClear(err); - throw error; -} - -/// Copies a borrowed native byte run into Dart memory. -Uint8List _copyBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -/// Copies an owned native byte run and releases it. -Uint8List _takeBytes(Pointer ptr, int len) { - if (ptr == nullptr) return Uint8List(0); - final bytes = Uint8List.fromList(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return bytes; -} - -/// Decodes an owned UTF-8 run and releases it. -String _takeString(Pointer ptr, int len) { - if (ptr == nullptr) return ''; - final text = utf8.decode(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return text; -} - -/// Decodes a borrowed UTF-8 run. -String _readString(Pointer ptr, int len) => - ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); - -/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. -Pointer _stage(Arena arena, List bytes) { - if (bytes.isEmpty) return nullptr; - final ptr = arena(bytes.length); - ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -// ── Value buffers ── -// Records, rich enums, optionals, lists, maps, and error payloads cross the -// C ABI serialized in this little-endian, packed format. A malformed buffer -// is a contract violation between producer and bindings, never a domain -// error. - -Never _bufferError(String context) => - throw StateError('malformed value buffer: $context'); - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - late ByteData _data = ByteData.sublistView(_buf); - int _len = 0; - - void _reserve(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - _buf = Uint8List(cap)..setRange(0, _len, _buf); - _data = ByteData.sublistView(_buf); - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeU8(v ? 1 : 0); - - void writeFlag(bool present) => writeU8(present ? 1 : 0); - - void writeI8(int v) { - _reserve(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeU8(int v) { - _reserve(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeI16(int v) { - _reserve(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeU16(int v) { - _reserve(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeI32(int v) { - _reserve(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeU32(int v) { - _reserve(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeI64(int v) { - _reserve(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - // Dart ints are signed 64-bit; a u64 travels as its bit pattern. - void writeU64(int v) => writeI64(v); - - void writeF32(double v) { - _reserve(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeF64(double v) { - _reserve(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeU32(v); - - void writeString(String v) => writeBytes(utf8.encode(v)); - - void writeBytes(List v) { - writeLength(v.length); - _reserve(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } - - void writeOptional(T? v, void Function(T) write) { - writeFlag(v != null); - if (v != null) write(v); - } - - void writeList(List v, void Function(T) write) { - writeLength(v.length); - for (final e in v) { - write(e); - } - } - - void writeMap( - Map v, void Function(K) writeKey, void Function(V) writeValue) { - writeLength(v.length); - for (final e in v.entries) { - writeKey(e.key); - writeValue(e.value); - } - } -} - -final class _BufferReader { - _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); - - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - int _take(int n, String context) { - if (_buf.length - _pos < n) _bufferError('truncated $context'); - final at = _pos; - _pos += n; - return at; - } - - bool readBool() { - final b = _buf[_take(1, 'bool')]; - if (b > 1) _bufferError('bool byte $b'); - return b == 1; - } - - bool readFlag() { - final b = _buf[_take(1, 'option flag')]; - if (b > 1) _bufferError('option flag $b'); - return b == 1; - } - - int readI8() => _data.getInt8(_take(1, 'i8')); - - int readU8() => _buf[_take(1, 'u8')]; - - int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); - - int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); - - int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); - - int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); - - int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); - - // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. - int readU64() => readI64(); - - double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); - - double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); - - int readLength() => readU32(); - - String readString() { - final n = readLength(); - final at = _take(n, 'string'); - return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); - } - - Uint8List readBytes() { - final n = readLength(); - final at = _take(n, 'bytes'); - return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); - } - - T? readOptional(T Function() read) => - readFlag() ? read() : null; - - // Collections grow as they decode, so a corrupt count can't force a huge - // allocation up front. - List readList(T Function() read) => - [for (var i = readLength(); i > 0; i--) read()]; - - Map readMap(K Function() readKey, V Function() readValue) { - final map = {}; - for (var i = readLength(); i > 0; i--) { - final key = readKey(); - if (map.containsKey(key)) _bufferError('duplicate map key $key'); - map[key] = readValue(); - } - return map; - } - - void expectEnd() { - if (_pos != _buf.length) _bufferError('trailing bytes'); - } -} - -/// Encodes one value with [write] into a fresh buffer. -Uint8List _encode(T value, void Function(_BufferWriter, T) write) { - final writer = _BufferWriter(); - write(writer, value); - return writer.takeBytes(); -} - -/// Decodes exactly one value with [read] from [bytes]. -T _decode(Uint8List bytes, T Function(_BufferReader) read) { - final reader = _BufferReader(bytes); - final value = read(reader); - reader.expectEnd(); - return value; -} - -// Records and rich enums compare by value: lists, maps, and byte arrays -// element by element, and everything else with `==` (interface wrappers by -// identity). - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) return true; - if (a is List && b is List) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!_deepEquals(a[i], b[i])) return false; - } - return true; - } - if (a is Map && b is Map) { - if (a.length != b.length) return false; - for (final key in a.keys) { - if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false; - } - return true; - } - return a == b; -} - -int _deepHash(Object? v) => switch (v) { - List() => Object.hashAll(v.map(_deepHash)), - Map() => Object.hashAllUnordered( - v.entries.map((e) => Object.hash(_deepHash(e.key), _deepHash(e.value)))), - _ => v.hashCode, - }; - -/// The `GraphicsError` error domain of module `graphics`: catch it to handle any of -/// its codes, or a subclass for one code. -sealed class GraphicsException extends NativeException { - /// Creates a domain error carrying [code] and [message]. - GraphicsException(super.code, super.message); -} - -/// coordinate out of bounds -final class OutOfBoundsException extends GraphicsException { - /// Creates an [OutOfBoundsException] (code 1). - OutOfBoundsException([String message = 'coordinate out of bounds']) : super(1, message); -} - -NativeException _mapGraphicsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return OutOfBoundsException(message); - default: - return _runtimeException(code, message, payload); - } -} - -/// A measurement unit shared with nested submodules. -enum Unit { - pixels(0), - points(1); - - const Unit(this.value); - - /// The C discriminant. - final int value; - - /// The variant whose discriminant is [value]. - static Unit fromValue(int value) => values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError.value(value, 'value', 'not an Unit discriminant')); -} - -/// Top-level struct that references a struct defined one level deeper. -final class Canvas { - /// Creates a [Canvas]. - Canvas({required this.width, required this.root}); - - final int width; - /// Reference down into a nested module - final Shape root; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Canvas && - width == other.width && - root == other.root; - - @override - int get hashCode => Object.hashAll([Canvas, width, root]); - - @override - String toString() => 'Canvas(width: $width, root: $root)'; -} - -void _packCanvas(_BufferWriter w, Canvas v) { - w.writeI32(v.width); - _packShape(w, v.root); -} - -Canvas _unpackCanvas(_BufferReader r) => Canvas( - width: r.readI32(), - root: _unpackShape(r), - ); - -final _nestedModulesGraphicsNewCanvas = _lib.lookupFunction< - Pointer Function(Int32, Pointer, Pointer<_Error>), - Pointer Function(int, Pointer, Pointer<_Error>)>('nested_modules_graphics_new_canvas', isLeaf: true); - -/// Construct a Canvas. -Canvas newCanvas(int width) { - final _result = _nestedModulesGraphicsNewCanvas(width, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackCanvas); -} - -/// A struct one level deep that references both an ancestor enum and a deeper struct. -final class Shape { - /// Creates a [Shape]. - Shape({required this.sides, required this.unit, required this.detail}); - - final int sides; - /// Reference up to the parent module's enum - final Unit unit; - /// Reference down into a deeper module - final Detail detail; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Shape && - sides == other.sides && - unit == other.unit && - detail == other.detail; - - @override - int get hashCode => Object.hashAll([Shape, sides, unit, detail]); - - @override - String toString() => 'Shape(sides: $sides, unit: $unit, detail: $detail)'; -} - -void _packShape(_BufferWriter w, Shape v) { - w.writeI32(v.sides); - w.writeI32(v.unit.value); - _packDetail(w, v.detail); -} - -Shape _unpackShape(_BufferReader r) => Shape( - sides: r.readI32(), - unit: Unit.fromValue(r.readI32()), - detail: _unpackDetail(r), - ); - -final _nestedModulesGraphicsShapesMakeShape = _lib.lookupFunction< - Pointer Function(Int32, Pointer, Pointer<_Error>), - Pointer Function(int, Pointer, Pointer<_Error>)>('nested_modules_graphics_shapes_make_shape', isLeaf: true); - -/// Construct a Shape. -Shape makeShape(int sides) { - final _result = _nestedModulesGraphicsShapesMakeShape(sides, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackShape); -} - -final _nestedModulesGraphicsShapesDescribe = _lib.lookupFunction< - Pointer Function(Pointer, Pointer<_Error>), - Pointer Function(Pointer, Pointer<_Error>)>('nested_modules_graphics_shapes_describe', isLeaf: true); - -/// Nested function returning a struct owned by an ancestor module. -Canvas describe() { - final _result = _nestedModulesGraphicsShapesDescribe(_outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackCanvas); -} - -/// A struct two levels deep. -final class Detail { - /// Creates a [Detail]. - Detail({required this.area}); - - final double area; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Detail && - area == other.area; - - @override - int get hashCode => Object.hashAll([Detail, area]); - - @override - String toString() => 'Detail(area: $area)'; -} - -void _packDetail(_BufferWriter w, Detail v) { - w.writeF64(v.area); -} - -Detail _unpackDetail(_BufferReader r) => Detail( - area: r.readF64(), - ); - -final _nestedModulesGraphicsShapesGeometryCompute = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('nested_modules_graphics_shapes_geometry_compute', isLeaf: true); - -/// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. -/// -/// Throws [GraphicsException] on domain errors. -Detail compute(Shape shape) { - final _arena = Arena(); - try { - final _shapeBytes = _encode(shape, _packShape); - final _result = _nestedModulesGraphicsShapesGeometryCompute(_stage(_arena, _shapeBytes), _shapeBytes.length, _outLen, _err); - _check(_err, _mapGraphicsException); - return _decode(_takeBytes(_result, _outLen.value), _unpackDetail); - } finally { - _arena.releaseAll(); - } -} - -// END nested_modules.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_shapes_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_shapes_dart.snap deleted file mode 100644 index 7fec7356..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_shapes_dart.snap +++ /dev/null @@ -1,843 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element - -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -// ── Library loading ── -// The native library opens on first use and is verified before any other -// lookup: it must implement the C ABI revision these bindings were generated -// against and carry every declaration they were generated with, unchanged. - -/// The native library every binding in this file calls. -final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); - -/// The native library couldn't be loaded, or it doesn't match these -/// bindings: a different C ABI revision, or a declaration that's missing or -/// changed. Regenerate the bindings from the library you ship. -final class NativeLibraryError extends Error { - /// Creates a load failure described by [message]. - NativeLibraryError(this.message); - - /// What failed, naming the library or declaration. - final String message; - - @override - String toString() => 'NativeLibraryError: $message'; -} - -/// The platform file name of the library on the system search path. -String get _libraryFile => Platform.isMacOS - ? 'libshapes.dylib' - : Platform.isWindows - ? 'shapes.dll' - : 'libshapes.so'; - -DynamicLibrary _openLibrary() { - // An explicit path wins, so a caller can point at any build artifact. - final override = Platform.environment['SHAPES_LIBRARY']; - if (override != null && override.isNotEmpty) { - return _open(override); - } - // iOS links native code into the app binary. - if (Platform.isIOS) return DynamicLibrary.process(); - try { - return DynamicLibrary.open(_libraryFile); - } on ArgumentError catch (e) { - // A macOS app may link the library into its executable instead. - if (Platform.isMacOS) return DynamicLibrary.process(); - throw NativeLibraryError("can't open '$_libraryFile' ($e); set " - 'SHAPES_LIBRARY to its path'); - } -} - -DynamicLibrary _open(String path) { - try { - return DynamicLibrary.open(path); - } on ArgumentError catch (e) { - throw NativeLibraryError("can't open '$path': $e"); - } -} - -/// The C ABI revision these bindings were generated against. -const int _abiVersion = 4; - -/// One `shapes_contract_entry` of a module's contract table. -final class _ContractEntry extends Struct { - @Uint64() - external int id; - @Uint64() - external int hash; -} - -DynamicLibrary _verifyLibrary(DynamicLibrary lib) { - final int Function() abiVersion; - try { - abiVersion = lib.lookupFunction( - 'shapes_abi_version'); - } on ArgumentError { - throw NativeLibraryError( - "the native library 'shapes' isn't loaded; set SHAPES_LIBRARY " - 'to its path'); - } - final found = abiVersion(); - if (found != _abiVersion) { - throw NativeLibraryError('shapes: these bindings expect C ABI revision ' - '$_abiVersion, but the loaded library implements revision $found'); - } - for (final (symbol, expected) in _contracts) { - _checkContract(lib, symbol, expected); - } - return lib; -} - -/// Checks that every `(id, hash, path)` entry in [expected] is in the table -/// [symbol] returns with an equal hash. Entries only the library has are -/// fine: they're declarations added after these bindings were generated. -void _checkContract( - DynamicLibrary lib, String symbol, List<(int, int, String)> expected) { - final Pointer<_ContractEntry> Function(Pointer) table; - try { - table = lib.lookupFunction Function(Pointer), - Pointer<_ContractEntry> Function(Pointer)>(symbol); - } on ArgumentError { - throw NativeLibraryError( - "shapes: the loaded library doesn't export '$symbol'"); - } - final hashes = {}; - final len = calloc(); - try { - final entries = table(len); - for (var i = 0; i < len.value; i++) { - hashes[entries[i].id] = entries[i].hash; - } - } finally { - calloc.free(len); - } - for (final (id, hash, path) in expected) { - final found = hashes[id]; - if (found == null) { - throw NativeLibraryError('shapes: $path is missing from the library'); - } - if (found != hash) { - throw NativeLibraryError( - 'shapes: $path changed since these bindings were generated'); - } - } -} - -/// Each top-level module's contract-table symbol and the declarations -/// these bindings were generated with: (id, signature hash, path). -const List<(String, List<(int, int, String)>)> _contracts = [ - ('shapes_shapes_contract', [ - (0x0d7b3eec49243e7a, 0x5f232311808e1b1f, 'shapes.Channel'), - (0x183fa8198100860e, 0xe40c6993cb31d359, 'shapes.describe'), - (0x2d1395eb837b848e, 0x42202a0c12f3a16e, 'shapes.sum_bytes'), - (0x7f9a5b87ba09b86b, 0xc5b32fb0efbbaa17, 'shapes.scale'), - (0xc8edd779240fda17, 0x68e1be78fddb20e4, 'shapes.Numerics'), - (0xd3d3881b2fd18a62, 0xf24d0eb76a99d918, 'shapes.make_numerics'), - (0xf010844afff5ac3c, 0x50de28ae9b41ff15, 'shapes.Shape'), - ]), -]; - -// ── Errors and memory ── - -/// The C `shapes_error` slot every fallible call writes. A non-zero -/// `code` carries a producer-owned `message` and, for a domain error with -/// fields, a value-buffer `payload`; `shapes_error_clear` releases both. -final class _Error extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -final _errorClear = _lib.lookupFunction), - void Function(Pointer<_Error>)>('shapes_error_clear', isLeaf: true); - -final _freeBytes = _lib.lookupFunction, Size), - void Function(Pointer, int)>('shapes_free_bytes', isLeaf: true); - -// Synchronous calls on this isolate share one error slot and one length -// slot: each call reads (and clears) them right after it returns, before any -// other call can reuse them, including a nested call from a callback. -final Pointer<_Error> _err = calloc<_Error>(); -final Pointer _outLen = calloc(); - -/// A failure the native library reported to a function declared to throw. -/// Domain errors extend this class with a positive [code]; a negative code is -/// one of the runtime constants below. -class NativeException implements Exception { - /// Creates an exception carrying [code] and [message]. - NativeException(this.code, this.message); - - /// The producer reported an untyped error. - static const int genericCode = -1; - - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - - /// An argument or a callback's return couldn't be lifted by the producer. - static const int marshalCode = -3; - - /// A callback implementation failed; [message] carries its text. - static const int foreignCode = -4; - - /// The call was cancelled (see [CancelledException]). - static const int cancelledCode = -5; - - /// The error code: positive for a domain error, negative for a runtime - /// failure. - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => '$runtimeType($code): $message'; -} - -/// A cancellable call completed because its token was cancelled. -class CancelledException extends NativeException { - /// Creates a cancellation with the producer's [message]. - CancelledException([String message = 'cancelled']) - : super(NativeException.cancelledCode, message); -} - -/// A function that isn't declared to throw failed anyway. That's a bug in -/// the native library (a panic, an argument it couldn't lift, a callback -/// failure it let through), so it's an [Error], not an [Exception]. -final class NativeError extends Error { - /// Creates the error a failed call reported. - NativeError(this.code, this.message); - - /// The runtime code the producer reported (see [NativeException]). - final int code; - - /// The producer's message. - final String message; - - @override - String toString() => 'NativeError($code): $message'; -} - -/// Maps a reported code, message, and payload onto the object to throw. -typedef _ErrorMapper = Object Function( - int code, String message, Uint8List payload); - -/// The mapper of a function that can't fail: every code but cancellation -/// is a [NativeError]. -Object _trap(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeError(code, message); - -/// Maps a runtime code (or one the domain doesn't declare) onto its -/// exception, for a function declared to throw. -NativeException _runtimeException(int code, String message, Uint8List payload) => - code == NativeException.cancelledCode - ? CancelledException(message) - : NativeException(code, message); - -/// Builds the error [err] reports, using [map]. -Object _readError(Pointer<_Error> err, _ErrorMapper map) { - final e = err.ref; - final message = e.message == nullptr ? '' : e.message.toDartString(); - return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); -} - -/// Throws the error a synchronous call left in [err], clearing the slot. -/// [map] is the domain's mapper for a function declared to throw. -void _check(Pointer<_Error> err, [_ErrorMapper map = _trap]) { - if (err.ref.code == 0) return; - final error = _readError(err, map); - _errorClear(err); - throw error; -} - -/// Copies a borrowed native byte run into Dart memory. -Uint8List _copyBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -/// Copies an owned native byte run and releases it. -Uint8List _takeBytes(Pointer ptr, int len) { - if (ptr == nullptr) return Uint8List(0); - final bytes = Uint8List.fromList(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return bytes; -} - -/// Decodes an owned UTF-8 run and releases it. -String _takeString(Pointer ptr, int len) { - if (ptr == nullptr) return ''; - final text = utf8.decode(ptr.asTypedList(len)); - _freeBytes(ptr, len); - return text; -} - -/// Decodes a borrowed UTF-8 run. -String _readString(Pointer ptr, int len) => - ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); - -/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. -Pointer _stage(Arena arena, List bytes) { - if (bytes.isEmpty) return nullptr; - final ptr = arena(bytes.length); - ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -// ── Value buffers ── -// Records, rich enums, optionals, lists, maps, and error payloads cross the -// C ABI serialized in this little-endian, packed format. A malformed buffer -// is a contract violation between producer and bindings, never a domain -// error. - -Never _bufferError(String context) => - throw StateError('malformed value buffer: $context'); - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - late ByteData _data = ByteData.sublistView(_buf); - int _len = 0; - - void _reserve(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - _buf = Uint8List(cap)..setRange(0, _len, _buf); - _data = ByteData.sublistView(_buf); - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeU8(v ? 1 : 0); - - void writeFlag(bool present) => writeU8(present ? 1 : 0); - - void writeI8(int v) { - _reserve(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeU8(int v) { - _reserve(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeI16(int v) { - _reserve(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeU16(int v) { - _reserve(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeI32(int v) { - _reserve(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeU32(int v) { - _reserve(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeI64(int v) { - _reserve(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - // Dart ints are signed 64-bit; a u64 travels as its bit pattern. - void writeU64(int v) => writeI64(v); - - void writeF32(double v) { - _reserve(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeF64(double v) { - _reserve(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeU32(v); - - void writeString(String v) => writeBytes(utf8.encode(v)); - - void writeBytes(List v) { - writeLength(v.length); - _reserve(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } - - void writeOptional(T? v, void Function(T) write) { - writeFlag(v != null); - if (v != null) write(v); - } - - void writeList(List v, void Function(T) write) { - writeLength(v.length); - for (final e in v) { - write(e); - } - } - - void writeMap( - Map v, void Function(K) writeKey, void Function(V) writeValue) { - writeLength(v.length); - for (final e in v.entries) { - writeKey(e.key); - writeValue(e.value); - } - } -} - -final class _BufferReader { - _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); - - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - int _take(int n, String context) { - if (_buf.length - _pos < n) _bufferError('truncated $context'); - final at = _pos; - _pos += n; - return at; - } - - bool readBool() { - final b = _buf[_take(1, 'bool')]; - if (b > 1) _bufferError('bool byte $b'); - return b == 1; - } - - bool readFlag() { - final b = _buf[_take(1, 'option flag')]; - if (b > 1) _bufferError('option flag $b'); - return b == 1; - } - - int readI8() => _data.getInt8(_take(1, 'i8')); - - int readU8() => _buf[_take(1, 'u8')]; - - int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); - - int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); - - int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); - - int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); - - int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); - - // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. - int readU64() => readI64(); - - double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); - - double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); - - int readLength() => readU32(); - - String readString() { - final n = readLength(); - final at = _take(n, 'string'); - return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); - } - - Uint8List readBytes() { - final n = readLength(); - final at = _take(n, 'bytes'); - return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); - } - - T? readOptional(T Function() read) => - readFlag() ? read() : null; - - // Collections grow as they decode, so a corrupt count can't force a huge - // allocation up front. - List readList(T Function() read) => - [for (var i = readLength(); i > 0; i--) read()]; - - Map readMap(K Function() readKey, V Function() readValue) { - final map = {}; - for (var i = readLength(); i > 0; i--) { - final key = readKey(); - if (map.containsKey(key)) _bufferError('duplicate map key $key'); - map[key] = readValue(); - } - return map; - } - - void expectEnd() { - if (_pos != _buf.length) _bufferError('trailing bytes'); - } -} - -/// Encodes one value with [write] into a fresh buffer. -Uint8List _encode(T value, void Function(_BufferWriter, T) write) { - final writer = _BufferWriter(); - write(writer, value); - return writer.takeBytes(); -} - -/// Decodes exactly one value with [read] from [bytes]. -T _decode(Uint8List bytes, T Function(_BufferReader) read) { - final reader = _BufferReader(bytes); - final value = read(reader); - reader.expectEnd(); - return value; -} - -// Records and rich enums compare by value: lists, maps, and byte arrays -// element by element, and everything else with `==` (interface wrappers by -// identity). - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) return true; - if (a is List && b is List) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!_deepEquals(a[i], b[i])) return false; - } - return true; - } - if (a is Map && b is Map) { - if (a.length != b.length) return false; - for (final key in a.keys) { - if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false; - } - return true; - } - return a == b; -} - -int _deepHash(Object? v) => switch (v) { - List() => Object.hashAll(v.map(_deepHash)), - Map() => Object.hashAllUnordered( - v.entries.map((e) => Object.hash(_deepHash(e.key), _deepHash(e.value)))), - _ => v.hashCode, - }; - -/// An algebraic shape (sum type with associated data) -sealed class Shape { - const Shape(); -} - -/// The empty shape (a unit variant) -final class ShapeEmpty extends Shape { - /// Creates a [ShapeEmpty]. - const ShapeEmpty(); - - @override - bool operator ==(Object other) => other is ShapeEmpty; - - @override - int get hashCode => (ShapeEmpty).hashCode; - - @override - String toString() => 'ShapeEmpty()'; -} - -/// A circle with a radius -final class ShapeCircle extends Shape { - /// Creates a [ShapeCircle]. - ShapeCircle(this.radius); - - /// Radius in points - final double radius; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ShapeCircle && - radius == other.radius; - - @override - int get hashCode => Object.hashAll([ShapeCircle, radius]); - - @override - String toString() => 'ShapeCircle(radius: $radius)'; -} - -/// An axis-aligned rectangle -final class ShapeRectangle extends Shape { - /// Creates a [ShapeRectangle]. - ShapeRectangle(this.width, this.height); - - final double width; - final double height; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ShapeRectangle && - width == other.width && - height == other.height; - - @override - int get hashCode => Object.hashAll([ShapeRectangle, width, height]); - - @override - String toString() => 'ShapeRectangle(width: $width, height: $height)'; -} - -/// An integer-bounded shape (exercises narrow + wide ints) -final class ShapeBounded extends Shape { - /// Creates a [ShapeBounded]. - ShapeBounded(this.min, this.max, this.weight); - - final int min; - final int max; - final int weight; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ShapeBounded && - min == other.min && - max == other.max && - weight == other.weight; - - @override - int get hashCode => Object.hashAll([ShapeBounded, min, max, weight]); - - @override - String toString() => 'ShapeBounded(min: $min, max: $max, weight: $weight)'; -} - -/// A labeled shape with a small count -final class ShapeLabeled extends Shape { - /// Creates a [ShapeLabeled]. - ShapeLabeled(this.label, this.count); - - final String label; - final int count; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ShapeLabeled && - label == other.label && - count == other.count; - - @override - int get hashCode => Object.hashAll([ShapeLabeled, label, count]); - - @override - String toString() => 'ShapeLabeled(label: $label, count: $count)'; -} - -void _packShape(_BufferWriter w, Shape v) { - switch (v) { - case ShapeEmpty(): - w.writeI32(0); - case ShapeCircle(): - w.writeI32(1); - w.writeF64(v.radius); - case ShapeRectangle(): - w.writeI32(2); - w.writeF32(v.width); - w.writeF32(v.height); - case ShapeBounded(): - w.writeI32(3); - w.writeI16(v.min); - w.writeI16(v.max); - w.writeU64(v.weight); - case ShapeLabeled(): - w.writeI32(4); - w.writeString(v.label); - w.writeU8(v.count); - } -} - -Shape _unpackShape(_BufferReader r) { - final tag = r.readI32(); - switch (tag) { - case 0: - return const ShapeEmpty(); - case 1: - return ShapeCircle(r.readF64()); - case 2: - return ShapeRectangle(r.readF32(), r.readF32()); - case 3: - return ShapeBounded(r.readI16(), r.readI16(), r.readU64()); - case 4: - return ShapeLabeled(r.readString(), r.readU8()); - default: - _bufferError('unknown Shape tag $tag'); - } -} - -/// A plain C-style enum (no payloads) -enum Channel { - red(0), - green(1), - blue(2); - - const Channel(this.value); - - /// The C discriminant. - final int value; - - /// The variant whose discriminant is [value]. - static Channel fromValue(int value) => values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError.value(value, 'value', 'not a Channel discriminant')); -} - -/// One field per supported numeric primitive -final class Numerics { - /// Creates a [Numerics]. - Numerics({required this.a, required this.b, required this.c, required this.d, required this.e, required this.f, required this.g, required this.h, required this.i, required this.j}); - - final int a; - final int b; - final int c; - final int d; - final int e; - final int f; - final int g; - final int h; - final double i; - final double j; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Numerics && - a == other.a && - b == other.b && - c == other.c && - d == other.d && - e == other.e && - f == other.f && - g == other.g && - h == other.h && - i == other.i && - j == other.j; - - @override - int get hashCode => Object.hashAll([Numerics, a, b, c, d, e, f, g, h, i, j]); - - @override - String toString() => 'Numerics(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f, g: $g, h: $h, i: $i, j: $j)'; -} - -void _packNumerics(_BufferWriter w, Numerics v) { - w.writeI8(v.a); - w.writeI16(v.b); - w.writeI32(v.c); - w.writeI64(v.d); - w.writeU8(v.e); - w.writeU16(v.f); - w.writeU32(v.g); - w.writeU64(v.h); - w.writeF32(v.i); - w.writeF64(v.j); -} - -Numerics _unpackNumerics(_BufferReader r) => Numerics( - a: r.readI8(), - b: r.readI16(), - c: r.readI32(), - d: r.readI64(), - e: r.readU8(), - f: r.readU16(), - g: r.readU32(), - h: r.readU64(), - i: r.readF32(), - j: r.readF64(), - ); - -final _shapesShapesDescribe = _lib.lookupFunction< - Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('shapes_shapes_describe', isLeaf: true); - -/// Render a shape to a string -String describe(Shape shape) { - final _arena = Arena(); - try { - final _shapeBytes = _encode(shape, _packShape); - final _result = _shapesShapesDescribe(_stage(_arena, _shapeBytes), _shapeBytes.length, _outLen, _err); - _check(_err); - return _takeString(_result, _outLen.value); - } finally { - _arena.releaseAll(); - } -} - -final _shapesShapesScale = _lib.lookupFunction< - Pointer Function(Pointer, Size, Double, Pointer, Pointer<_Error>), - Pointer Function(Pointer, int, double, Pointer, Pointer<_Error>)>('shapes_shapes_scale', isLeaf: true); - -/// Scale a shape by a factor, returning a new shape -Shape scale(Shape shape, double factor) { - final _arena = Arena(); - try { - final _shapeBytes = _encode(shape, _packShape); - final _result = _shapesShapesScale(_stage(_arena, _shapeBytes), _shapeBytes.length, factor, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackShape); - } finally { - _arena.releaseAll(); - } -} - -final _shapesShapesSumBytes = _lib.lookupFunction< - Uint64 Function(Pointer, Size, Pointer<_Error>), - int Function(Pointer, int, Pointer<_Error>)>('shapes_shapes_sum_bytes', isLeaf: true); - -/// Sum a list of bytes into a wide integer (numerics smoke) -int sumBytes(List values) { - final _arena = Arena(); - try { - final _result = _shapesShapesSumBytes(_stage(_arena, values), values.length, _err); - _check(_err); - return _result; - } finally { - _arena.releaseAll(); - } -} - -final _shapesShapesMakeNumerics = _lib.lookupFunction< - Pointer Function(Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float, Double, Pointer, Pointer<_Error>), - Pointer Function(int, int, int, int, int, int, int, int, double, double, Pointer, Pointer<_Error>)>('shapes_shapes_make_numerics', isLeaf: true); - -/// Build a Numerics struct from every numeric primitive -Numerics makeNumerics(int a, int b, int c, int d, int e, int f, int g, int h, double i, double j) { - final _result = _shapesShapesMakeNumerics(a, b, c, d, e, f, g, h, i, j, _outLen, _err); - _check(_err); - return _decode(_takeBytes(_result, _outLen.value), _unpackNumerics); -} - -// END shapes.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_cs.snap deleted file mode 100644 index a29040ab..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_cs.snap +++ /dev/null @@ -1,546 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace DocsEverywhere; - -/// The errors of the DocsErrors domain, declared by module docs. -/// Catch a nested class for one code, or this class for any of them. -public abstract class DocsErrorsException : NativeException -{ - private protected DocsErrorsException(int code, string message) : base(code, message) - { - } - - /// Writes this code's fields, the error payload a callback - /// reports with it. - internal virtual void WritePayload(FfiBufferWriter writer) - { - } - - /// Returned when no document matches the requested id - public sealed class NotFound : DocsErrorsException - { - /// The domain code, 1. - public const int ErrorCode = 1; - - /// Creates the error with , or the - /// default message "Document not found" when it's null. - public NotFound(string? message = null) : base(ErrorCode, message ?? "Document not found") - { - } - } - - /// Raised when attempting to move into a forbidden state - public sealed class InvalidStatus : DocsErrorsException - { - /// The domain code, 2. - public const int ErrorCode = 2; - - /// Creates the error with , or the - /// default message "Invalid status transition" when it's null. - public InvalidStatus(string? message = null) : base(ErrorCode, message ?? "Invalid status transition") - { - } - } - - /// The exception for a raw code and its serialized payload. - internal static new Exception FromError(int code, string message, byte[]? payload) - { - var given = message.Length == 0 ? null : message; - switch (code) - { - case NotFound.ErrorCode: - return new NotFound(given); - case InvalidStatus.ErrorCode: - return new InvalidStatus(given); - default: - return global::DocsEverywhere.NativeException.FromError(code, message, payload); - } - } -} - -/// -/// Lifecycle status of a document. -/// -/// Used to drive UI badges and filters. -/// -public enum Status -{ - /// Document is being edited - Draft = 0, - /// Document is publicly visible - Published = 1, - /// Document is hidden from listings - Archived = 2, -} - -/// -/// Represents a single document tracked by the system. -/// -/// Documents are persisted to disk and exposed via the public API. -/// -public sealed class Document -{ - /// Stable opaque identifier - public long Id { get; } - - /// Human-readable title shown in lists - public string Title { get; } - - /// Current lifecycle status - public Status Status { get; } - - /// Free-form tags for grouping - public string[] Tags { get; } - - /// Creates a from every field. - public Document(long id, string title, Status status, string[] tags) - { - Id = id; - Title = title; - Status = status; - Tags = tags; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Title); - writer.WriteI32((int)Status); - FfiCodecs.WriteListOfString(writer, Tags); - } - - internal static Document ReadFrom(FfiBufferReader reader) - { - return new Document( - reader.ReadI64(), - reader.ReadString(), - (global::DocsEverywhere.Status)reader.ReadI32(), - FfiCodecs.ReadListOfString(reader)); - } -} - -/// -/// Consumer-implemented observer of document-saved notifications. -/// -/// Implementations must be safe to call from any thread. -/// -/// The native library may call any method from any thread until -/// it releases its last reference to the implementation. Object arguments -/// belong to the implementation. An exception thrown by a method fails the -/// native library's call in progress: a method documented to throw a domain -/// exception reports it with its code and fields, and any other exception -/// reaches the library as . -public interface ISavedListener -{ - /// Fires after a document has been persisted successfully - /// Identifier of the saved document - /// Monotonically increasing revision - void OnSaved(long docId, int revision); -} - -/// The process-wide docs_everywhere_docs_SavedListener_vtable and the trampolines behind it, -/// adapting an to the C ABI. -internal static unsafe class FfiVtable_docs_SavedListener -{ - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public uint Size; - public uint Flags; - public delegate* unmanaged[Cdecl] Free; - public delegate* unmanaged[Cdecl] on_saved; - } - - /// The vtable's address, valid for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var vtable = (Layout*)NativeMemory.AllocZeroed((nuint)sizeof(Layout)); - vtable->Size = (uint)sizeof(Layout); - vtable->Flags = 0; - vtable->Free = &FreeTrampoline; - vtable->on_saved = &OnSavedTrampoline; - return (IntPtr)vtable; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - Ffi.Unregister(ctx); - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnSavedTrampoline(IntPtr ctx, long doc_id, int revision, FfiError* out_err) - { - try - { - Ffi.Target(ctx).OnSaved(doc_id, revision); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - } - } -} - -/// -/// An ordered collection of documents. -/// -/// Notebooks own their documents and release them when destroyed. -/// -public sealed unsafe class Notebook : IDisposable, IEquatable -{ - /// The native object; borrowed by every call. - internal NativeHandle Handle { get; } - - private Notebook(NativeHandle handle) - { - Handle = handle; - } - - /// Wraps one strong reference the caller transfers. - internal static Notebook Adopt(IntPtr ptr) - { - return new Notebook(new NativeHandle(ptr)); - } - - /// Mints a second strong reference the caller owns, as an - /// object token in a value buffer requires. - internal IntPtr CloneHandle() - { - return NativeMethods.docs_everywhere_docs_Notebook_clone(Handle); - } - - /// Create an empty notebook - public Notebook() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_new(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - Handle = new NativeHandle(ffiResult); - } - - /// Add a document, returning its position in the notebook - /// The document to add - public int Insert(Document document) - { - var documentWriter = new FfiBufferWriter(); - document.WriteTo(documentWriter); - var ffiErr = default(FfiError); - fixed (byte* documentPtr = documentWriter.Written) - { - var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_insert(Handle, documentPtr, (nuint)documentWriter.Length, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - return ffiResult; - } - } - - /// Look up a document by identifier, failing when none matches - /// Identifier of the document to find - /// The call reported a DocsErrors code. - public Document Find(long id) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_find(Handle, id, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.DocsErrorsException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::DocsEverywhere.Document.ReadFrom(r)); - } - - /// The number of documents currently held - public int Size() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_size(Handle, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - return ffiResult; - } - - /// The largest number of documents one notebook will hold - public static int MaxDocuments() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_max_documents(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - return ffiResult; - } - - /// Releases this wrapper's reference. The native object is - /// dropped once its last reference is released, and an in-flight - /// call finishes first. - public void Dispose() - { - Handle.Dispose(); - } - - /// True when both wrappers reference the same native object. - public bool Equals(Notebook? other) - { - return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); - } - - /// - public override bool Equals(object? obj) - { - return obj is Notebook other && Equals(other); - } - - /// - public override int GetHashCode() - { - return Handle.DangerousGetHandle().GetHashCode(); - } - - /// One strong reference, released through - /// docs_everywhere_docs_Notebook_destroy. - internal sealed class NativeHandle : SafeHandle - { - /// A null handle, passed for an absent optional object. - internal static readonly NativeHandle Null = new NativeHandle(); - - public NativeHandle() : base(IntPtr.Zero, true) - { - } - - internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) - { - SetHandle(ptr); - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - NativeMethods.docs_everywhere_docs_Notebook_destroy(handle); - return true; - } - } -} - -/// Document storage: create, publish, and observe documents. -public static unsafe class Docs -{ - /// - /// Create a brand new document. - /// - /// The document starts in `Draft` status; callers must publish - /// it explicitly to make it visible. - /// - /// Human-readable title for the new document - /// Initial set of tags (may be empty) - public static Document CreateDocument(string title, string[] tags) - { - var titleBytes = Ffi.Utf8(title); - var tagsWriter = new FfiBufferWriter(); - FfiCodecs.WriteListOfString(tagsWriter, tags); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* titlePtr = titleBytes, tagsPtr = tagsWriter.Written) - { - var ffiResult = NativeMethods.docs_everywhere_docs_create_document(titlePtr, (nuint)titleBytes.Length, tagsPtr, (nuint)tagsWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::DocsEverywhere.Document.ReadFrom(r)); - } - } - - /// Look up a document by identifier - /// Identifier returned from create_document - public static Document? GetDocument(long id) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.docs_everywhere_docs_get_document(id, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadOptionalOfDocument(r)); - } - - /// Publish a document in the background - /// Identifier of the document to publish - public static Task PublishAsync(long id) - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.docs_everywhere_docs_publish_async(id, &CompletePublishAsync, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompletePublishAsync(IntPtr context, FfiError* err, byte result) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::DocsEverywhere.NativeBugException.FromError); - return; - } - ffiCall.SetResult(result != 0); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - /// Register a listener for saved notifications - /// Receiver of saved notifications - public static void Watch(ISavedListener listener) - { - var listenerCtx = Ffi.Register(listener); - var ffiErr = default(FfiError); - try - { - NativeMethods.docs_everywhere_docs_watch(listenerCtx, FfiVtable_docs_SavedListener.Pointer, &ffiErr); - } - catch - { - Ffi.Unregister(listenerCtx); - throw; - } - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeBugException.FromError); - } -} - -/// The value-buffer codec of every composite type the API -/// carries. -internal static class FfiCodecs -{ - /// Reads one value that must fill the whole buffer. - internal static T Decode(FfiBufferReader reader, Func read) - { - var value = read(reader); - reader.ExpectEnd(); - return value; - } - - internal static void WriteListOfString(FfiBufferWriter writer, string[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - writer.WriteString(item); - } - } - - internal static string[] ReadListOfString(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(reader.ReadString()); - } - return list.ToArray(); - } - - internal static void WriteOptionalOfDocument(FfiBufferWriter writer, Document? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - present.WriteTo(writer); - } - } - - internal static Document? ReadOptionalOfDocument(FfiBufferReader reader) - { - return reader.ReadBool() ? global::DocsEverywhere.Document.ReadFrom(reader) : null; - } -} - -internal static unsafe partial class NativeMethods -{ - static partial void VerifyContracts() - { - VerifyContract("docs_everywhere_docs_contract", &docs_everywhere_docs_contract, new (ulong, ulong, string)[] - { - (0x00b2187fd75634ecUL, 0x2263c6f787bf6081UL, "docs.publish_async"), - (0x12fc242b9dc6444cUL, 0x96405f484a642b31UL, "docs.Notebook.insert"), - (0x2213822ed05cd5bdUL, 0x26c5833392aa63ceUL, "docs.Notebook.new"), - (0x2bd762bcdaa207ffUL, 0x1b0ebab2d04c49b0UL, "docs.SavedListener"), - (0x2e086c45e9d617deUL, 0x4d43bf32358c01ffUL, "docs.Notebook.find"), - (0x4817dc98197ad960UL, 0xf8017b5a7d58b2c7UL, "docs.Notebook.size"), - (0x614de47452d5a55cUL, 0xd9aae1472494ffefUL, "docs.get_document"), - (0x67c5cb9fc119bcd5UL, 0x08d5b25a1a22a7faUL, "docs.Document"), - (0x8d484bf2d78c9b70UL, 0xe9093cf96912194cUL, "docs.Notebook.max_documents"), - (0xaea61d6451691c67UL, 0xe4704128b3524b3dUL, "docs.watch"), - (0xd68d6ad1540cdec5UL, 0x01afeb40b0a8bfe1UL, "docs.Notebook"), - (0xdb0601444a4760ccUL, 0xe56c69629b444797UL, "docs.create_document"), - (0xe12d6f4838347b3eUL, 0xa9bab86646d84f51UL, "docs.Status"), - (0xee2f8d25b3313b50UL, 0x9691232f35439a2bUL, "docs.DocsErrors"), - }); - } - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_contract")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial FfiContractEntry* docs_everywhere_docs_contract(nuint* out_len); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_clone")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr docs_everywhere_docs_Notebook_clone(Notebook.NativeHandle self); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void docs_everywhere_docs_Notebook_destroy(IntPtr self); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_new")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr docs_everywhere_docs_Notebook_new(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_insert")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int docs_everywhere_docs_Notebook_insert(Notebook.NativeHandle self, byte* document_ptr, nuint document_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_find")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* docs_everywhere_docs_Notebook_find(Notebook.NativeHandle self, long id, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_size")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int docs_everywhere_docs_Notebook_size(Notebook.NativeHandle self, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_max_documents")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int docs_everywhere_docs_Notebook_max_documents(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_create_document")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* docs_everywhere_docs_create_document(byte* title_ptr, nuint title_len, byte* tags_ptr, nuint tags_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_get_document")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* docs_everywhere_docs_get_document(long id, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_publish_async")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void docs_everywhere_docs_publish_async(long id, delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_watch")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void docs_everywhere_docs_watch(IntPtr listener_ctx, IntPtr listener_vtable, FfiError* out_err); -} - -// END DocsEverywhere.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_cs.snap deleted file mode 100644 index 761df884..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_cs.snap +++ /dev/null @@ -1,2304 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace EdgeCases; - -/// The errors of the EdgeErrors domain, declared by module edge_cases. -/// Catch a nested class for one code, or this class for any of them. -public abstract class EdgeErrorsException : NativeException -{ - private protected EdgeErrorsException(int code, string message) : base(code, message) - { - } - - /// Writes this code's fields, the error payload a callback - /// reports with it. - internal virtual void WritePayload(FfiBufferWriter writer) - { - } - - /// Resource is busy - public sealed class Busy : EdgeErrorsException - { - /// The domain code, 7. - public const int ErrorCode = 7; - - /// Creates the error with , or the - /// default message "Resource is busy" when it's null. - public Busy(string? message = null) : base(ErrorCode, message ?? "Resource is busy") - { - } - } - - /// The exception for a raw code and its serialized payload. - internal static new Exception FromError(int code, string message, byte[]? payload) - { - var given = message.Length == 0 ? null : message; - switch (code) - { - case Busy.ErrorCode: - return new Busy(given); - default: - return global::EdgeCases.NativeException.FromError(code, message, payload); - } - } -} - -/// Deprecated enum with non-contiguous values and keyword-like variants -[Obsolete("Use Mode instead")] -public enum Legacy -{ - None = 0, - Default = 5, - Class = 10, -} - -public enum Mode -{ - Fast = 1, - Safe = 2, -} - -public sealed class Leaf -{ - public float Weight { get; } - - /// Creates a from every field. - public Leaf(float weight) - { - Weight = weight; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteF32(Weight); - } - - internal static Leaf ReadFrom(FfiBufferReader reader) - { - return new Leaf( - reader.ReadF32()); - } -} - -/// Middle layer of a three-deep struct nesting -public sealed class Inner -{ - public Leaf Leaf { get; } - - public Leaf[] Leaves { get; } - - /// Creates a from every field. - public Inner(Leaf leaf, Leaf[] leaves) - { - Leaf = leaf; - Leaves = leaves; - } - - internal void WriteTo(FfiBufferWriter writer) - { - Leaf.WriteTo(writer); - FfiCodecs.WriteListOfLeaf(writer, Leaves); - } - - internal static Inner ReadFrom(FfiBufferReader reader) - { - return new Inner( - global::EdgeCases.Leaf.ReadFrom(reader), - FfiCodecs.ReadListOfLeaf(reader)); - } -} - -public sealed class Outer -{ - public Inner Inner { get; } - - public Inner? MaybeInner { get; } - - public Dictionary ByName { get; } - - public Mode[] Modes { get; } - - public int[][] Matrix { get; } - - public long?[] Sparse { get; } - - public byte[]? Blob { get; } - - public string? Note { get; } - - public Dictionary ById { get; } - - public bool Hidden { get; } - - public byte V2 { get; } - - /// Creates a from every field. - public Outer(Inner inner, Inner? maybeInner, Dictionary byName, Mode[] modes, int[][] matrix, long?[] sparse, byte[]? blob, string? note, Dictionary byId, bool hidden, byte v2) - { - Inner = inner; - MaybeInner = maybeInner; - ByName = byName; - Modes = modes; - Matrix = matrix; - Sparse = sparse; - Blob = blob; - Note = note; - ById = byId; - Hidden = hidden; - V2 = v2; - } - - internal void WriteTo(FfiBufferWriter writer) - { - Inner.WriteTo(writer); - FfiCodecs.WriteOptionalOfInner(writer, MaybeInner); - FfiCodecs.WriteMapOfStringToInner(writer, ByName); - FfiCodecs.WriteListOfMode(writer, Modes); - FfiCodecs.WriteListOfListOfI32(writer, Matrix); - FfiCodecs.WriteListOfOptionalOfI64(writer, Sparse); - FfiCodecs.WriteOptionalOfBytes(writer, Blob); - FfiCodecs.WriteOptionalOfString(writer, Note); - FfiCodecs.WriteMapOfI64ToString(writer, ById); - writer.WriteBool(Hidden); - writer.WriteU8(V2); - } - - internal static Outer ReadFrom(FfiBufferReader reader) - { - return new Outer( - global::EdgeCases.Inner.ReadFrom(reader), - FfiCodecs.ReadOptionalOfInner(reader), - FfiCodecs.ReadMapOfStringToInner(reader), - FfiCodecs.ReadListOfMode(reader), - FfiCodecs.ReadListOfListOfI32(reader), - FfiCodecs.ReadListOfOptionalOfI64(reader), - FfiCodecs.ReadOptionalOfBytes(reader), - FfiCodecs.ReadOptionalOfString(reader), - FfiCodecs.ReadMapOfI64ToString(reader), - reader.ReadBool(), - reader.ReadU8()); - } -} - -/// Every field is a reserved word somewhere -public sealed class Keywords -{ - public int Class { get; } - - public int Def { get; } - - public int Import { get; } - - public int From { get; } - - public int Func { get; } - - public int Range { get; } - - public int Register { get; } - - public int Volatile { get; } - - public int New { get; } - - public int Default { get; } - - public int End { get; } - - public int In { get; } - - public int Is { get; } - - public int Int { get; } - - public int Var { get; } - - public int Val { get; } - - public int Self { get; } - - public int This { get; } - - public int Object { get; } - - public int String { get; } - - public int Case { get; } - - public int Select { get; } - - public int Switch { get; } - - public int Void { get; } - - public int Union { get; } - - public int Namespace { get; } - - public int Operator { get; } - - public int Template { get; } - - public int Package { get; } - - public int Protocol { get; } - - public int Guard { get; } - - public int Defer { get; } - - public int Inout { get; } - - public int Internal { get; } - - public int When { get; } - - public int Fun { get; } - - public int Data { get; } - - public int Late { get; } - - public int Part { get; } - - public int Out { get; } - - public int Ref { get; } - - public int Event { get; } - - public int Base { get; } - - public int Lambda { get; } - - public int Yield { get; } - - public int Nil { get; } - - public int Null { get; } - - public int Begin { get; } - - public int Rescue { get; } - - public int Module { get; } - - public int Chan { get; } - - public int Go { get; } - - public int Let { get; } - - public int Export { get; } - - public int Delete { get; } - - public int Typedef { get; } - - public int Sizeof { get; } - - public int Static { get; } - - public int Super { get; } - - /// Creates a from every field. - public Keywords(int @class, int def, int import, int from, int func, int range, int register, int @volatile, int @new, int @default, int end, int @in, int @is, int @int, int var, int val, int self, int @this, int @object, int @string, int @case, int select, int @switch, int @void, int union, int @namespace, int @operator, int template, int package, int protocol, int guard, int defer, int inout, int @internal, int when, int fun, int data, int late, int part, int @out, int @ref, int @event, int @base, int lambda, int yield, int nil, int @null, int begin, int rescue, int module, int chan, int go, int let, int export, int delete, int typedef, int @sizeof, int @static, int super) - { - Class = @class; - Def = def; - Import = import; - From = from; - Func = func; - Range = range; - Register = register; - Volatile = @volatile; - New = @new; - Default = @default; - End = end; - In = @in; - Is = @is; - Int = @int; - Var = var; - Val = val; - Self = self; - This = @this; - Object = @object; - String = @string; - Case = @case; - Select = select; - Switch = @switch; - Void = @void; - Union = union; - Namespace = @namespace; - Operator = @operator; - Template = template; - Package = package; - Protocol = protocol; - Guard = guard; - Defer = defer; - Inout = inout; - Internal = @internal; - When = when; - Fun = fun; - Data = data; - Late = late; - Part = part; - Out = @out; - Ref = @ref; - Event = @event; - Base = @base; - Lambda = lambda; - Yield = yield; - Nil = nil; - Null = @null; - Begin = begin; - Rescue = rescue; - Module = module; - Chan = chan; - Go = go; - Let = let; - Export = export; - Delete = delete; - Typedef = typedef; - Sizeof = @sizeof; - Static = @static; - Super = super; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteI32(Class); - writer.WriteI32(Def); - writer.WriteI32(Import); - writer.WriteI32(From); - writer.WriteI32(Func); - writer.WriteI32(Range); - writer.WriteI32(Register); - writer.WriteI32(Volatile); - writer.WriteI32(New); - writer.WriteI32(Default); - writer.WriteI32(End); - writer.WriteI32(In); - writer.WriteI32(Is); - writer.WriteI32(Int); - writer.WriteI32(Var); - writer.WriteI32(Val); - writer.WriteI32(Self); - writer.WriteI32(This); - writer.WriteI32(Object); - writer.WriteI32(String); - writer.WriteI32(Case); - writer.WriteI32(Select); - writer.WriteI32(Switch); - writer.WriteI32(Void); - writer.WriteI32(Union); - writer.WriteI32(Namespace); - writer.WriteI32(Operator); - writer.WriteI32(Template); - writer.WriteI32(Package); - writer.WriteI32(Protocol); - writer.WriteI32(Guard); - writer.WriteI32(Defer); - writer.WriteI32(Inout); - writer.WriteI32(Internal); - writer.WriteI32(When); - writer.WriteI32(Fun); - writer.WriteI32(Data); - writer.WriteI32(Late); - writer.WriteI32(Part); - writer.WriteI32(Out); - writer.WriteI32(Ref); - writer.WriteI32(Event); - writer.WriteI32(Base); - writer.WriteI32(Lambda); - writer.WriteI32(Yield); - writer.WriteI32(Nil); - writer.WriteI32(Null); - writer.WriteI32(Begin); - writer.WriteI32(Rescue); - writer.WriteI32(Module); - writer.WriteI32(Chan); - writer.WriteI32(Go); - writer.WriteI32(Let); - writer.WriteI32(Export); - writer.WriteI32(Delete); - writer.WriteI32(Typedef); - writer.WriteI32(Sizeof); - writer.WriteI32(Static); - writer.WriteI32(Super); - } - - internal static Keywords ReadFrom(FfiBufferReader reader) - { - return new Keywords( - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32(), - reader.ReadI32()); - } -} - -/// Deprecated struct -[Obsolete("Use Outer instead")] -public sealed class Retired -{ - public long Id { get; } - - /// Creates a from every field. - public Retired(long id) - { - Id = id; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteI64(Id); - } - - internal static Retired ReadFrom(FfiBufferReader reader) - { - return new Retired( - reader.ReadI64()); - } -} - -/// Record carrying objects in every buffered position -public sealed class WidgetHolder -{ - public Widget Primary { get; } - - public Widget? Fallback { get; } - - public Widget[] Extras { get; } - - public Dictionary ByName { get; } - - /// Creates a from every field. - public WidgetHolder(Widget primary, Widget? fallback, Widget[] extras, Dictionary byName) - { - Primary = primary; - Fallback = fallback; - Extras = extras; - ByName = byName; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteObject(Primary.CloneHandle()); - FfiCodecs.WriteOptionalOfWidget(writer, Fallback); - FfiCodecs.WriteListOfWidget(writer, Extras); - FfiCodecs.WriteMapOfStringToWidget(writer, ByName); - } - - internal static WidgetHolder ReadFrom(FfiBufferReader reader) - { - return new WidgetHolder( - global::EdgeCases.Widget.Adopt(reader.ReadObject()), - FfiCodecs.ReadOptionalOfWidget(reader), - FfiCodecs.ReadListOfWidget(reader), - FfiCodecs.ReadMapOfStringToWidget(reader)); - } -} - -/// Callback interface carrying composite payloads and objects -/// The native library may call any method from any thread until -/// it releases its last reference to the implementation. Object arguments -/// belong to the implementation. An exception thrown by a method fails the -/// native library's call in progress: a method documented to throw a domain -/// exception reports it with its code and fields, and any other exception -/// reaches the library as . -public interface IBatchListener -{ - /// Composite payloads cross as value buffers - void OnBatch(Outer[] items, byte[] raw, Outer? first, Mode mode); - - /// Objects cross as strong references the consumer must release - void OnWidget(Widget widget, Widget? fallback, Widget[] children); - - void OnTick(); - - /// Scalar return travels back through the vtable - bool ShouldContinue(int @class); - - /// Enum return travels back as its integer discriminant - Mode PickMode(); -} - -/// The process-wide edge_cases_edge_cases_BatchListener_vtable and the trampolines behind it, -/// adapting an to the C ABI. -internal static unsafe class FfiVtable_edge_cases_BatchListener -{ - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public uint Size; - public uint Flags; - public delegate* unmanaged[Cdecl] Free; - public delegate* unmanaged[Cdecl] on_batch; - public delegate* unmanaged[Cdecl] on_widget; - public delegate* unmanaged[Cdecl] on_tick; - public delegate* unmanaged[Cdecl] should_continue; - public delegate* unmanaged[Cdecl] pick_mode; - } - - /// The vtable's address, valid for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var vtable = (Layout*)NativeMemory.AllocZeroed((nuint)sizeof(Layout)); - vtable->Size = (uint)sizeof(Layout); - vtable->Flags = 0; - vtable->Free = &FreeTrampoline; - vtable->on_batch = &OnBatchTrampoline; - vtable->on_widget = &OnWidgetTrampoline; - vtable->on_tick = &OnTickTrampoline; - vtable->should_continue = &ShouldContinueTrampoline; - vtable->pick_mode = &PickModeTrampoline; - return (IntPtr)vtable; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - Ffi.Unregister(ctx); - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnBatchTrampoline(IntPtr ctx, byte* items_ptr, nuint items_len, byte* raw_ptr, nuint raw_len, byte* first_ptr, nuint first_len, int mode, FfiError* out_err) - { - try - { - Ffi.Target(ctx).OnBatch(FfiCodecs.Decode(Ffi.ReadBuffer(items_ptr, items_len), static r => FfiCodecs.ReadListOfOuter(r)), Ffi.ReadBytes(raw_ptr, raw_len), FfiCodecs.Decode(Ffi.ReadBuffer(first_ptr, first_len), static r => FfiCodecs.ReadOptionalOfOuter(r)), (global::EdgeCases.Mode)mode); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnWidgetTrampoline(IntPtr ctx, IntPtr widget, IntPtr fallback, byte* children_ptr, nuint children_len, FfiError* out_err) - { - try - { - Ffi.Target(ctx).OnWidget(global::EdgeCases.Widget.Adopt(widget), fallback == IntPtr.Zero ? null : global::EdgeCases.Widget.Adopt(fallback), FfiCodecs.Decode(Ffi.ReadBuffer(children_ptr, children_len), static r => FfiCodecs.ReadListOfWidget(r))); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnTickTrampoline(IntPtr ctx, FfiError* out_err) - { - try - { - Ffi.Target(ctx).OnTick(); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static byte ShouldContinueTrampoline(IntPtr ctx, int @class, FfiError* out_err) - { - try - { - return (byte)(Ffi.Target(ctx).ShouldContinue(@class) ? 1 : 0); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - return default; - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static int PickModeTrampoline(IntPtr ctx, FfiError* out_err) - { - try - { - return (int)Ffi.Target(ctx).PickMode(); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - return default; - } - } -} - -/// Callback interface whose name and methods are keywords -/// The native library may call any method from any thread until -/// it releases its last reference to the implementation. Object arguments -/// belong to the implementation. An exception thrown by a method fails the -/// native library's call in progress: a method documented to throw a domain -/// exception reports it with its code and fields, and any other exception -/// reaches the library as . -public interface IKeyword -{ - void Delete(int self); - - int Import(); -} - -/// The process-wide edge_cases_edge_cases_Keyword_vtable and the trampolines behind it, -/// adapting an to the C ABI. -internal static unsafe class FfiVtable_edge_cases_Keyword -{ - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public uint Size; - public uint Flags; - public delegate* unmanaged[Cdecl] Free; - public delegate* unmanaged[Cdecl] delete; - public delegate* unmanaged[Cdecl] import; - } - - /// The vtable's address, valid for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var vtable = (Layout*)NativeMemory.AllocZeroed((nuint)sizeof(Layout)); - vtable->Size = (uint)sizeof(Layout); - vtable->Flags = 0; - vtable->Free = &FreeTrampoline; - vtable->delete = &DeleteTrampoline; - vtable->import = &ImportTrampoline; - return (IntPtr)vtable; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - Ffi.Unregister(ctx); - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void DeleteTrampoline(IntPtr ctx, int self, FfiError* out_err) - { - try - { - Ffi.Target(ctx).Delete(self); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static int ImportTrampoline(IntPtr ctx, FfiError* out_err) - { - try - { - return Ffi.Target(ctx).Import(); - } - catch (Exception e) - { - Ffi.SetForeignError(out_err, e); - return default; - } - } -} - -/// Interface with no constructor; instances come from make_widget -public sealed unsafe class Widget : IDisposable, IEquatable -{ - /// The native object; borrowed by every call. - internal NativeHandle Handle { get; } - - private Widget(NativeHandle handle) - { - Handle = handle; - } - - /// Wraps one strong reference the caller transfers. - internal static Widget Adopt(IntPtr ptr) - { - return new Widget(new NativeHandle(ptr)); - } - - /// Mints a second strong reference the caller owns, as an - /// object token in a value buffer requires. - internal IntPtr CloneHandle() - { - return NativeMethods.edge_cases_edge_cases_Widget_clone(Handle); - } - - /// Void method that can fail - /// The call reported a EdgeErrors code. - public void Refresh() - { - var ffiErr = default(FfiError); - NativeMethods.edge_cases_edge_cases_Widget_refresh(Handle, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); - } - - public string Rename(string @class, string? import) - { - var classBytes = Ffi.Utf8(@class); - var importWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfString(importWriter, import); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* classPtr = classBytes, importPtr = importWriter.Written) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_Widget_rename(Handle, classPtr, (nuint)classBytes.Length, importPtr, (nuint)importWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return Ffi.TakeString(ffiResult, ffiOutLen); - } - } - - /// Async method with a struct result - /// The call reported a EdgeErrors code. - public Task Fetch(long id) - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_Widget_fetch(Handle, id, &CompleteFetch, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteFetch(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); - return; - } - ffiCall.SetResult(FfiCodecs.Decode(Ffi.TakeBuffer(result_ptr, result_len), static r => global::EdgeCases.Outer.ReadFrom(r))); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - [Obsolete("Use rename instead")] - public string OldName() - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.edge_cases_edge_cases_Widget_old_name(Handle, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return Ffi.TakeString(ffiResult, ffiOutLen); - } - - /// Spelled like the release method of several wrappers - public bool Close() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_Widget_close(Handle, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return ffiResult != 0; - } - - /// Spelled like the release method of the Dart and .NET wrappers - public void Dispose_(bool force) - { - var ffiErr = default(FfiError); - NativeMethods.edge_cases_edge_cases_Widget_dispose(Handle, (byte)(force ? 1 : 0), &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - } - - /// Releases this wrapper's reference. The native object is - /// dropped once its last reference is released, and an in-flight - /// call finishes first. - public void Dispose() - { - Handle.Dispose(); - } - - /// True when both wrappers reference the same native object. - public bool Equals(Widget? other) - { - return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); - } - - /// - public override bool Equals(object? obj) - { - return obj is Widget other && Equals(other); - } - - /// - public override int GetHashCode() - { - return Handle.DangerousGetHandle().GetHashCode(); - } - - /// One strong reference, released through - /// edge_cases_edge_cases_Widget_destroy. - internal sealed class NativeHandle : SafeHandle - { - /// A null handle, passed for an absent optional object. - internal static readonly NativeHandle Null = new NativeHandle(); - - public NativeHandle() : base(IntPtr.Zero, true) - { - } - - internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) - { - SetHandle(ptr); - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - NativeMethods.edge_cases_edge_cases_Widget_destroy(handle); - return true; - } - } -} - -/// Deprecated interface with a throwing constructor -[Obsolete("Use Widget instead")] -public sealed unsafe class Vault : IDisposable, IEquatable -{ - /// The native object; borrowed by every call. - internal NativeHandle Handle { get; } - - private Vault(NativeHandle handle) - { - Handle = handle; - } - - /// Wraps one strong reference the caller transfers. - internal static Vault Adopt(IntPtr ptr) - { - return new Vault(new NativeHandle(ptr)); - } - - /// Mints a second strong reference the caller owns, as an - /// object token in a value buffer requires. - internal IntPtr CloneHandle() - { - return NativeMethods.edge_cases_edge_cases_Vault_clone(Handle); - } - - /// Fails when the vault is busy - /// The call reported a EdgeErrors code. - public static Vault Open(string path) - { - var pathBytes = Ffi.Utf8(path); - var ffiErr = default(FfiError); - fixed (byte* pathPtr = pathBytes) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_open(pathPtr, (nuint)pathBytes.Length, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); - return global::EdgeCases.Vault.Adopt(ffiResult); - } - } - - public Widget? Peek() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_peek(Handle, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return ffiResult == IntPtr.Zero ? null : global::EdgeCases.Widget.Adopt(ffiResult); - } - - /// Spelled like the handle accessor of several wrappers - public long Handle_() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_handle(Handle, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return ffiResult; - } - - /// Throwing static - /// The call reported a EdgeErrors code. - public static bool Probe() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_probe(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); - return ffiResult != 0; - } - - /// Static spelled like an inherited object member - public static string ToString_() - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_to_string(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return Ffi.TakeString(ffiResult, ffiOutLen); - } - - /// Releases this wrapper's reference. The native object is - /// dropped once its last reference is released, and an in-flight - /// call finishes first. - public void Dispose() - { - Handle.Dispose(); - } - - /// True when both wrappers reference the same native object. - public bool Equals(Vault? other) - { - return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); - } - - /// - public override bool Equals(object? obj) - { - return obj is Vault other && Equals(other); - } - - /// - public override int GetHashCode() - { - return Handle.DangerousGetHandle().GetHashCode(); - } - - /// One strong reference, released through - /// edge_cases_edge_cases_Vault_destroy. - internal sealed class NativeHandle : SafeHandle - { - /// A null handle, passed for an absent optional object. - internal static readonly NativeHandle Null = new NativeHandle(); - - public NativeHandle() : base(IntPtr.Zero, true) - { - } - - internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) - { - SetHandle(ptr); - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - NativeMethods.edge_cases_edge_cases_Vault_destroy(handle); - return true; - } - } -} - -public static unsafe class EdgeCases -{ - /// No parameters and no result - public static void Noop() - { - var ffiErr = default(FfiError); - NativeMethods.edge_cases_edge_cases_noop(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - } - - /// Function named after a keyword whose parameters are all keywords - public static int Select(int @class, string from, string[] func, int? range, bool type) - { - var fromBytes = Ffi.Utf8(from); - var funcWriter = new FfiBufferWriter(); - FfiCodecs.WriteListOfString(funcWriter, func); - var rangeWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfI32(rangeWriter, range); - var ffiErr = default(FfiError); - fixed (byte* fromPtr = fromBytes, funcPtr = funcWriter.Written, rangePtr = rangeWriter.Written) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_select(@class, fromPtr, (nuint)fromBytes.Length, funcPtr, (nuint)funcWriter.Length, rangePtr, (nuint)rangeWriter.Length, (byte)(type ? 1 : 0), &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return ffiResult; - } - } - - /// Optional list of optional structs, the deepest legal nesting - public static Outer?[]? NestedOptional(Outer?[]? items) - { - var itemsWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfListOfOptionalOfOuter(itemsWriter, items); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* itemsPtr = itemsWriter.Written) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_nested_optional(itemsPtr, (nuint)itemsWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadOptionalOfListOfOptionalOfOuter(r)); - } - } - - public static Mode? OptionalParams(string? text, int? count, Outer? outer, string[]? tags, byte[]? blob, Mode? mode) - { - var textWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfString(textWriter, text); - var countWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfI32(countWriter, count); - var outerWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfOuter(outerWriter, outer); - var tagsWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfListOfString(tagsWriter, tags); - var blobWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfBytes(blobWriter, blob); - var modeWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfMode(modeWriter, mode); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* textPtr = textWriter.Written, countPtr = countWriter.Written, outerPtr = outerWriter.Written, tagsPtr = tagsWriter.Written, blobPtr = blobWriter.Written, modePtr = modeWriter.Written) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_optional_params(textPtr, (nuint)textWriter.Length, countPtr, (nuint)countWriter.Length, outerPtr, (nuint)outerWriter.Length, tagsPtr, (nuint)tagsWriter.Length, blobPtr, (nuint)blobWriter.Length, modePtr, (nuint)modeWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadOptionalOfMode(r)); - } - } - - /// Two callback interface parameters on one function - public static void RegisterBatch(IBatchListener listener, IKeyword keyword) - { - var listenerCtx = Ffi.Register(listener); - var keywordCtx = Ffi.Register(keyword); - var ffiErr = default(FfiError); - try - { - NativeMethods.edge_cases_edge_cases_register_batch(listenerCtx, FfiVtable_edge_cases_BatchListener.Pointer, keywordCtx, FfiVtable_edge_cases_Keyword.Pointer, &ffiErr); - } - catch - { - Ffi.Unregister(listenerCtx); - Ffi.Unregister(keywordCtx); - throw; - } - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - } - - /// Objects in every buffered position - public static WidgetHolder WidgetsEverywhere(Widget[] list, Dictionary byName, Widget?[] sparse, WidgetHolder holder) - { - var listWriter = new FfiBufferWriter(); - FfiCodecs.WriteListOfWidget(listWriter, list); - var byNameWriter = new FfiBufferWriter(); - FfiCodecs.WriteMapOfStringToWidget(byNameWriter, byName); - var sparseWriter = new FfiBufferWriter(); - FfiCodecs.WriteListOfOptionalOfWidget(sparseWriter, sparse); - var holderWriter = new FfiBufferWriter(); - holder.WriteTo(holderWriter); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* listPtr = listWriter.Written, byNamePtr = byNameWriter.Written, sparsePtr = sparseWriter.Written, holderPtr = holderWriter.Written) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_widgets_everywhere(listPtr, (nuint)listWriter.Length, byNamePtr, (nuint)byNameWriter.Length, sparsePtr, (nuint)sparseWriter.Length, holderPtr, (nuint)holderWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::EdgeCases.WidgetHolder.ReadFrom(r)); - } - } - - /// Iterator of objects - /// Streams lazily, one native call per item. The sequence can be - /// enumerated once; disposing its enumerator (as foreach does) - /// releases the native iterator. - public static IEnumerable WidgetStream() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_widget_stream(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_WidgetStreamIterator_destroy), &NextWidgetStream); - } - - private static bool NextWidgetStream(FfiIteratorHandle iter, out Widget item) - { - IntPtr ffiItem = default; - var ffiErr = default(FfiError); - var ffiMore = NativeMethods.edge_cases_edge_cases_WidgetStreamIterator_next(iter, &ffiItem, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - if (ffiMore == 0) - { - item = default!; - return false; - } - item = global::EdgeCases.Widget.Adopt(ffiItem); - return true; - } - - /// Async returning an object - /// The call reported a EdgeErrors code. - public static Task AsyncWidget() - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_async_widget(&CompleteAsyncWidget, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncWidget(IntPtr context, FfiError* err, IntPtr result) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); - return; - } - ffiCall.SetResult(global::EdgeCases.Widget.Adopt(result)); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - /// Factory for an interface with no constructor - public static Widget MakeWidget(string label) - { - var labelBytes = Ffi.Utf8(label); - var ffiErr = default(FfiError); - fixed (byte* labelPtr = labelBytes) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_make_widget(labelPtr, (nuint)labelBytes.Length, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return global::EdgeCases.Widget.Adopt(ffiResult); - } - } - - /// Optional interface return - /// The call reported a EdgeErrors code. - public static Widget? FindWidget(long id) - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_find_widget(id, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); - return ffiResult == IntPtr.Zero ? null : global::EdgeCases.Widget.Adopt(ffiResult); - } - - /// Interface parameter, including an optional one - public static string InspectWidget(Widget widget, Widget? fallback) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.edge_cases_edge_cases_inspect_widget(widget.Handle, fallback?.Handle ?? global::EdgeCases.Widget.NativeHandle.Null, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return Ffi.TakeString(ffiResult, ffiOutLen); - } - - /// Iterator of strings - /// Streams lazily, one native call per item. The sequence can be - /// enumerated once; disposing its enumerator (as foreach does) - /// releases the native iterator. - public static IEnumerable StringStream() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_string_stream(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_StringStreamIterator_destroy), &NextStringStream); - } - - private static bool NextStringStream(FfiIteratorHandle iter, out string item) - { - byte* ffiItem = default; - nuint ffiLen = default; - var ffiErr = default(FfiError); - var ffiMore = NativeMethods.edge_cases_edge_cases_StringStreamIterator_next(iter, &ffiItem, &ffiLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - if (ffiMore == 0) - { - item = default!; - return false; - } - item = Ffi.TakeString(ffiItem, ffiLen); - return true; - } - - /// Iterator of scalars - /// Streams lazily, one native call per item. The sequence can be - /// enumerated once; disposing its enumerator (as foreach does) - /// releases the native iterator. - public static IEnumerable ScalarStream() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_scalar_stream(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_ScalarStreamIterator_destroy), &NextScalarStream); - } - - private static bool NextScalarStream(FfiIteratorHandle iter, out int item) - { - int ffiItem = default; - var ffiErr = default(FfiError); - var ffiMore = NativeMethods.edge_cases_edge_cases_ScalarStreamIterator_next(iter, &ffiItem, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - if (ffiMore == 0) - { - item = default!; - return false; - } - item = ffiItem; - return true; - } - - /// Iterator of enums - /// Streams lazily, one native call per item. The sequence can be - /// enumerated once; disposing its enumerator (as foreach does) - /// releases the native iterator. - public static IEnumerable ModeStream() - { - var ffiErr = default(FfiError); - var ffiResult = NativeMethods.edge_cases_edge_cases_mode_stream(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_ModeStreamIterator_destroy), &NextModeStream); - } - - private static bool NextModeStream(FfiIteratorHandle iter, out Mode item) - { - int ffiItem = default; - var ffiErr = default(FfiError); - var ffiMore = NativeMethods.edge_cases_edge_cases_ModeStreamIterator_next(iter, &ffiItem, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - if (ffiMore == 0) - { - item = default!; - return false; - } - item = (global::EdgeCases.Mode)ffiItem; - return true; - } - - /// Async with no result - public static Task AsyncVoid() - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_async_void(&CompleteAsyncVoid, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncVoid(IntPtr context, FfiError* err) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.NativeBugException.FromError); - return; - } - ffiCall.SetResult(true); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - public static Task AsyncBytes(uint size) - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_async_bytes(size, &CompleteAsyncBytes, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncBytes(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.NativeBugException.FromError); - return; - } - ffiCall.SetResult(Ffi.TakeBytes(result_ptr, result_len)); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - /// The call reported a EdgeErrors code. - public static Task AsyncStruct() - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_async_struct(&CompleteAsyncStruct, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncStruct(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); - return; - } - ffiCall.SetResult(FfiCodecs.Decode(Ffi.TakeBuffer(result_ptr, result_len), static r => global::EdgeCases.Outer.ReadFrom(r))); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - /// The call reported a EdgeErrors code. - /// Cancels the native call; the task - /// then completes as canceled. - public static Task AsyncList(CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - var ffiCall = new FfiCall(cancellationToken); - try - { - NativeMethods.edge_cases_edge_cases_async_list(ffiCall.CancelToken(), &CompleteAsyncList, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncList(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); - return; - } - ffiCall.SetResult(FfiCodecs.Decode(Ffi.TakeBuffer(result_ptr, result_len), static r => FfiCodecs.ReadListOfOuter(r))); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - public static Task AsyncOptional() - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_async_optional(&CompleteAsyncOptional, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncOptional(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.NativeBugException.FromError); - return; - } - ffiCall.SetResult(FfiCodecs.Decode(Ffi.TakeBuffer(result_ptr, result_len), static r => FfiCodecs.ReadOptionalOfI64(r))); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - public static Task AsyncEnum() - { - var ffiCall = new FfiCall(); - try - { - NativeMethods.edge_cases_edge_cases_async_enum(&CompleteAsyncEnum, ffiCall.Context); - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncEnum(IntPtr context, FfiError* err, int result) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.NativeBugException.FromError); - return; - } - ffiCall.SetResult((global::EdgeCases.Mode)result); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - public static Task AsyncKeywords(string @class, int import) - { - var classBytes = Ffi.Utf8(@class); - var ffiCall = new FfiCall(); - try - { - fixed (byte* classPtr = classBytes) - { - NativeMethods.edge_cases_edge_cases_async_keywords(classPtr, (nuint)classBytes.Length, import, &CompleteAsyncKeywords, ffiCall.Context); - } - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void CompleteAsyncKeywords(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) - { - var ffiCall = FfiCall.Complete(context); - try - { - if (err != null) - { - ffiCall.SetError(err, global::EdgeCases.NativeBugException.FromError); - return; - } - ffiCall.SetResult(Ffi.TakeString(result_ptr, result_len)); - } - catch (Exception e) - { - ffiCall.SetException(e); - } - } - - public static Keywords KeywordsRoundtrip(Keywords value) - { - var valueWriter = new FfiBufferWriter(); - value.WriteTo(valueWriter); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* valuePtr = valueWriter.Written) - { - var ffiResult = NativeMethods.edge_cases_edge_cases_keywords_roundtrip(valuePtr, (nuint)valueWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::EdgeCases.Keywords.ReadFrom(r)); - } - } - - [Obsolete("Use Mode-based APIs")] - public static Retired LegacyValue(Legacy value) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.edge_cases_edge_cases_legacy_value((int)value, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::EdgeCases.Retired.ReadFrom(r)); - } -} - -/// The value-buffer codec of every composite type the API -/// carries. -internal static class FfiCodecs -{ - /// Reads one value that must fill the whole buffer. - internal static T Decode(FfiBufferReader reader, Func read) - { - var value = read(reader); - reader.ExpectEnd(); - return value; - } - - internal static void WriteListOfI32(FfiBufferWriter writer, int[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - writer.WriteI32(item); - } - } - - internal static int[] ReadListOfI32(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(reader.ReadI32()); - } - return list.ToArray(); - } - - internal static void WriteListOfLeaf(FfiBufferWriter writer, Leaf[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - item.WriteTo(writer); - } - } - - internal static Leaf[] ReadListOfLeaf(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(global::EdgeCases.Leaf.ReadFrom(reader)); - } - return list.ToArray(); - } - - internal static void WriteListOfListOfI32(FfiBufferWriter writer, int[][] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - FfiCodecs.WriteListOfI32(writer, item); - } - } - - internal static int[][] ReadListOfListOfI32(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(FfiCodecs.ReadListOfI32(reader)); - } - return list.ToArray(); - } - - internal static void WriteListOfMode(FfiBufferWriter writer, Mode[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - writer.WriteI32((int)item); - } - } - - internal static Mode[] ReadListOfMode(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add((global::EdgeCases.Mode)reader.ReadI32()); - } - return list.ToArray(); - } - - internal static void WriteListOfOptionalOfI64(FfiBufferWriter writer, long?[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - FfiCodecs.WriteOptionalOfI64(writer, item); - } - } - - internal static long?[] ReadListOfOptionalOfI64(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(FfiCodecs.ReadOptionalOfI64(reader)); - } - return list.ToArray(); - } - - internal static void WriteListOfOptionalOfOuter(FfiBufferWriter writer, Outer?[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - FfiCodecs.WriteOptionalOfOuter(writer, item); - } - } - - internal static Outer?[] ReadListOfOptionalOfOuter(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(FfiCodecs.ReadOptionalOfOuter(reader)); - } - return list.ToArray(); - } - - internal static void WriteListOfOptionalOfWidget(FfiBufferWriter writer, Widget?[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - FfiCodecs.WriteOptionalOfWidget(writer, item); - } - } - - internal static Widget?[] ReadListOfOptionalOfWidget(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(FfiCodecs.ReadOptionalOfWidget(reader)); - } - return list.ToArray(); - } - - internal static void WriteListOfOuter(FfiBufferWriter writer, Outer[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - item.WriteTo(writer); - } - } - - internal static Outer[] ReadListOfOuter(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(global::EdgeCases.Outer.ReadFrom(reader)); - } - return list.ToArray(); - } - - internal static void WriteListOfString(FfiBufferWriter writer, string[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - writer.WriteString(item); - } - } - - internal static string[] ReadListOfString(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(reader.ReadString()); - } - return list.ToArray(); - } - - internal static void WriteListOfWidget(FfiBufferWriter writer, Widget[] value) - { - writer.WriteLen(value.Length); - foreach (var item in value) - { - writer.WriteObject(item.CloneHandle()); - } - } - - internal static Widget[] ReadListOfWidget(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - list.Add(global::EdgeCases.Widget.Adopt(reader.ReadObject())); - } - return list.ToArray(); - } - - internal static void WriteMapOfI64ToString(FfiBufferWriter writer, Dictionary value) - { - writer.WriteLen(value.Count); - foreach (var entry in value) - { - writer.WriteI64(entry.Key); - writer.WriteString(entry.Value); - } - } - - internal static Dictionary ReadMapOfI64ToString(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var map = new Dictionary(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - var key = reader.ReadI64(); - if (!map.TryAdd(key, reader.ReadString())) - { - throw FfiBufferReader.Malformed("repeated map key"); - } - } - return map; - } - - internal static void WriteMapOfStringToInner(FfiBufferWriter writer, Dictionary value) - { - writer.WriteLen(value.Count); - foreach (var entry in value) - { - writer.WriteString(entry.Key); - entry.Value.WriteTo(writer); - } - } - - internal static Dictionary ReadMapOfStringToInner(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var map = new Dictionary(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - var key = reader.ReadString(); - if (!map.TryAdd(key, global::EdgeCases.Inner.ReadFrom(reader))) - { - throw FfiBufferReader.Malformed("repeated map key"); - } - } - return map; - } - - internal static void WriteMapOfStringToWidget(FfiBufferWriter writer, Dictionary value) - { - writer.WriteLen(value.Count); - foreach (var entry in value) - { - writer.WriteString(entry.Key); - writer.WriteObject(entry.Value.CloneHandle()); - } - } - - internal static Dictionary ReadMapOfStringToWidget(FfiBufferReader reader) - { - var count = reader.ReadLen(); - var map = new Dictionary(reader.Capacity(count)); - for (var i = 0; i < count; i++) - { - var key = reader.ReadString(); - if (!map.TryAdd(key, global::EdgeCases.Widget.Adopt(reader.ReadObject()))) - { - throw FfiBufferReader.Malformed("repeated map key"); - } - } - return map; - } - - internal static void WriteOptionalOfBytes(FfiBufferWriter writer, byte[]? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - writer.WriteBytes(present); - } - } - - internal static byte[]? ReadOptionalOfBytes(FfiBufferReader reader) - { - return reader.ReadBool() ? reader.ReadBytes() : null; - } - - internal static void WriteOptionalOfI32(FfiBufferWriter writer, int? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - writer.WriteI32(present); - } - } - - internal static int? ReadOptionalOfI32(FfiBufferReader reader) - { - return reader.ReadBool() ? reader.ReadI32() : null; - } - - internal static void WriteOptionalOfI64(FfiBufferWriter writer, long? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - writer.WriteI64(present); - } - } - - internal static long? ReadOptionalOfI64(FfiBufferReader reader) - { - return reader.ReadBool() ? reader.ReadI64() : null; - } - - internal static void WriteOptionalOfInner(FfiBufferWriter writer, Inner? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - present.WriteTo(writer); - } - } - - internal static Inner? ReadOptionalOfInner(FfiBufferReader reader) - { - return reader.ReadBool() ? global::EdgeCases.Inner.ReadFrom(reader) : null; - } - - internal static void WriteOptionalOfListOfOptionalOfOuter(FfiBufferWriter writer, Outer?[]? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - FfiCodecs.WriteListOfOptionalOfOuter(writer, present); - } - } - - internal static Outer?[]? ReadOptionalOfListOfOptionalOfOuter(FfiBufferReader reader) - { - return reader.ReadBool() ? FfiCodecs.ReadListOfOptionalOfOuter(reader) : null; - } - - internal static void WriteOptionalOfListOfString(FfiBufferWriter writer, string[]? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - FfiCodecs.WriteListOfString(writer, present); - } - } - - internal static string[]? ReadOptionalOfListOfString(FfiBufferReader reader) - { - return reader.ReadBool() ? FfiCodecs.ReadListOfString(reader) : null; - } - - internal static void WriteOptionalOfMode(FfiBufferWriter writer, Mode? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - writer.WriteI32((int)present); - } - } - - internal static Mode? ReadOptionalOfMode(FfiBufferReader reader) - { - return reader.ReadBool() ? (global::EdgeCases.Mode)reader.ReadI32() : null; - } - - internal static void WriteOptionalOfOuter(FfiBufferWriter writer, Outer? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - present.WriteTo(writer); - } - } - - internal static Outer? ReadOptionalOfOuter(FfiBufferReader reader) - { - return reader.ReadBool() ? global::EdgeCases.Outer.ReadFrom(reader) : null; - } - - internal static void WriteOptionalOfString(FfiBufferWriter writer, string? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - writer.WriteString(present); - } - } - - internal static string? ReadOptionalOfString(FfiBufferReader reader) - { - return reader.ReadBool() ? reader.ReadString() : null; - } - - internal static void WriteOptionalOfWidget(FfiBufferWriter writer, Widget? value) - { - writer.WriteBool(value != null); - if (value is { } present) - { - writer.WriteObject(present.CloneHandle()); - } - } - - internal static Widget? ReadOptionalOfWidget(FfiBufferReader reader) - { - return reader.ReadBool() ? global::EdgeCases.Widget.Adopt(reader.ReadObject()) : null; - } -} - -internal static unsafe partial class NativeMethods -{ - static partial void VerifyContracts() - { - VerifyContract("edge_cases_edge_cases_contract", &edge_cases_edge_cases_contract, new (ulong, ulong, string)[] - { - (0x056a86511785ba2bUL, 0x5cba3f16d57c6cdbUL, "edge_cases.make_widget"), - (0x07bfa9ecb2b720d2UL, 0x3813b3f57c64bf35UL, "edge_cases.EdgeErrors"), - (0x0aaa11867e37f991UL, 0xf090dd52146ebdf0UL, "edge_cases.Widget.refresh"), - (0x0f9701493898ae4cUL, 0x6944f47a275c96e8UL, "edge_cases.Inner"), - (0x1226b1adac179f7dUL, 0x0e95a00ab0bef637UL, "edge_cases.async_keywords"), - (0x16d36bef1541136cUL, 0x5ea7021ec324298fUL, "edge_cases.find_widget"), - (0x1b7324c55e980febUL, 0xabf87fe372358f42UL, "edge_cases.Widget.old_name"), - (0x1cbd731467c2bab8UL, 0x4915b9989c2c9074UL, "edge_cases.Vault"), - (0x2c2a6de67d894b0aUL, 0x50a4496b1e7191caUL, "edge_cases.Vault.open"), - (0x322b4cfcd2d6861cUL, 0xca7527a379ae8465UL, "edge_cases.Keywords"), - (0x3f8e23a5f32906f4UL, 0x8d4c908d796f130dUL, "edge_cases.Leaf"), - (0x42ab8a685457b97cUL, 0xe5ce30500cadb32dUL, "edge_cases.Vault.handle"), - (0x4a68540e2836d3d0UL, 0xa20e44c71e420ed2UL, "edge_cases.mode_stream"), - (0x4e030580ac0b111dUL, 0xb68b7a18d910e175UL, "edge_cases.legacy_value"), - (0x4f5e4e5291864ef3UL, 0x55a1a88b21a08f8bUL, "edge_cases.Legacy"), - (0x4f84b06171b4213dUL, 0xbaedcfdebb6a2820UL, "edge_cases.scalar_stream"), - (0x5067a5ff2d9ef910UL, 0xbfc79b18fc4714fcUL, "edge_cases.Widget"), - (0x59aff159112fbf5bUL, 0x18e4def57a8cfa49UL, "edge_cases.optional_params"), - (0x5daf65b98fa77c48UL, 0xc817a8cb202e59c2UL, "edge_cases.keywords_roundtrip"), - (0x5de8a5c9726c2c5aUL, 0x7c50b5b65f7a896dUL, "edge_cases.register_batch"), - (0x618a620309994c3cUL, 0x826ad0b1d208d723UL, "edge_cases.async_bytes"), - (0x648285cdba0a7b8eUL, 0xcf6e80e783a4d8beUL, "edge_cases.WidgetHolder"), - (0x649c78079824b10dUL, 0x5b12dd26e0634b69UL, "edge_cases.async_widget"), - (0x7da7a7357d122a37UL, 0xf544e025ffaa3d20UL, "edge_cases.async_void"), - (0x7e4015bc3a472e2dUL, 0xfe22291d90b82e4aUL, "edge_cases.widget_stream"), - (0x805276e5d81fdba7UL, 0xd9b64e360bd03b2bUL, "edge_cases.Outer"), - (0x80e1183326542df4UL, 0xe8a57e12cc6ed276UL, "edge_cases.select"), - (0x923ce5e9bcd769a1UL, 0x0b973f5dc07dda8bUL, "edge_cases.Vault.to_string"), - (0x98ca8c01f5239404UL, 0xfe11c93eb6b303efUL, "edge_cases.Widget.rename"), - (0xa13e22e98ea278d4UL, 0xf1fae215d7387800UL, "edge_cases.widgets_everywhere"), - (0xa84a8c9bf2a881e1UL, 0xbe1dc840bf221996UL, "edge_cases.async_optional"), - (0xad958c478fbf205dUL, 0xe386b0716d24f19bUL, "edge_cases.Retired"), - (0xb2fc3964ef56ab39UL, 0x73c325c8bc351236UL, "edge_cases.async_list"), - (0xb3403af521cf0006UL, 0x6abc30d48206a3baUL, "edge_cases.Widget.close"), - (0xb794304a22a406b8UL, 0xf6fb98711c516105UL, "edge_cases.async_struct"), - (0xba3936106a49c47dUL, 0xb7025f2fadd6288eUL, "edge_cases.Vault.peek"), - (0xc155b0c3cfb164f4UL, 0xc777ae57d92ebc47UL, "edge_cases.noop"), - (0xcdc18607777019ddUL, 0xb81a31e5c20aa7a8UL, "edge_cases.inspect_widget"), - (0xd164a82aeb64b8ccUL, 0xded02c68005c4737UL, "edge_cases.nested_optional"), - (0xd978ccad2468038fUL, 0x30c39c65d6235153UL, "edge_cases.Mode"), - (0xdbe9d947431c9124UL, 0x39537fb848fdf980UL, "edge_cases.BatchListener"), - (0xeae8b7862c08bb07UL, 0xfe12eeaa830452eaUL, "edge_cases.Keyword"), - (0xf1e155a05ae262b8UL, 0xb575eb818a423d42UL, "edge_cases.async_enum"), - (0xf22e210bdbce7343UL, 0x2a5e0f801bc8c768UL, "edge_cases.Widget.dispose"), - (0xf3c7cf675563ee6cUL, 0x0ecffeef399facc8UL, "edge_cases.Vault.probe"), - (0xfd34bc67acac379eUL, 0x576caa14f3c0901aUL, "edge_cases.Widget.fetch"), - (0xff2e9d2bff941786UL, 0x7e96764ef1f5fbaaUL, "edge_cases.string_stream"), - }); - } - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_contract")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial FfiContractEntry* edge_cases_edge_cases_contract(nuint* out_len); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_clone")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_Widget_clone(Widget.NativeHandle self); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_Widget_destroy(IntPtr self); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_refresh")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_Widget_refresh(Widget.NativeHandle self, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_rename")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_Widget_rename(Widget.NativeHandle self, byte* class_ptr, nuint class_len, byte* import_ptr, nuint import_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_fetch")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_Widget_fetch(Widget.NativeHandle self, long id, delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_old_name")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_Widget_old_name(Widget.NativeHandle self, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_close")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte edge_cases_edge_cases_Widget_close(Widget.NativeHandle self, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_dispose")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_Widget_dispose(Widget.NativeHandle self, byte force, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_clone")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_Vault_clone(Vault.NativeHandle self); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_Vault_destroy(IntPtr self); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_open")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_Vault_open(byte* path_ptr, nuint path_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_peek")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_Vault_peek(Vault.NativeHandle self, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_handle")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial long edge_cases_edge_cases_Vault_handle(Vault.NativeHandle self, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_probe")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte edge_cases_edge_cases_Vault_probe(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_to_string")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_Vault_to_string(nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_noop")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_noop(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_select")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int edge_cases_edge_cases_select(int @class, byte* from_ptr, nuint from_len, byte* func_ptr, nuint func_len, byte* range_ptr, nuint range_len, byte type_, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_nested_optional")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_nested_optional(byte* items_ptr, nuint items_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_optional_params")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_optional_params(byte* text_ptr, nuint text_len, byte* count_ptr, nuint count_len, byte* outer_ptr, nuint outer_len, byte* tags_ptr, nuint tags_len, byte* blob_ptr, nuint blob_len, byte* mode_ptr, nuint mode_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_register_batch")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_register_batch(IntPtr listener_ctx, IntPtr listener_vtable, IntPtr keyword_ctx, IntPtr keyword_vtable, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_widgets_everywhere")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_widgets_everywhere(byte* list_ptr, nuint list_len, byte* by_name_ptr, nuint by_name_len, byte* sparse_ptr, nuint sparse_len, byte* holder_ptr, nuint holder_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_widget_stream")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_widget_stream(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_WidgetStreamIterator_next")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int edge_cases_edge_cases_WidgetStreamIterator_next(FfiIteratorHandle iter, IntPtr* out_item, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_WidgetStreamIterator_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_WidgetStreamIterator_destroy(IntPtr iter); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_widget")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_widget(delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_make_widget")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_make_widget(byte* label_ptr, nuint label_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_find_widget")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_find_widget(long id, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_inspect_widget")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_inspect_widget(Widget.NativeHandle widget, Widget.NativeHandle fallback, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_string_stream")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_string_stream(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_StringStreamIterator_next")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int edge_cases_edge_cases_StringStreamIterator_next(FfiIteratorHandle iter, byte** out_item, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_StringStreamIterator_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_StringStreamIterator_destroy(IntPtr iter); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_scalar_stream")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_scalar_stream(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ScalarStreamIterator_next")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int edge_cases_edge_cases_ScalarStreamIterator_next(FfiIteratorHandle iter, int* out_item, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ScalarStreamIterator_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_ScalarStreamIterator_destroy(IntPtr iter); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_mode_stream")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr edge_cases_edge_cases_mode_stream(FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ModeStreamIterator_next")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial int edge_cases_edge_cases_ModeStreamIterator_next(FfiIteratorHandle iter, int* out_item, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ModeStreamIterator_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_ModeStreamIterator_destroy(IntPtr iter); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_void")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_void(delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_bytes")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_bytes(uint size, delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_struct")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_struct(delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_list")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_list(IntPtr cancel_token, delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_optional")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_optional(delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_enum")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_enum(delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_keywords")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void edge_cases_edge_cases_async_keywords(byte* class_ptr, nuint class_len, int import, delegate* unmanaged[Cdecl] callback, IntPtr context); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_keywords_roundtrip")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_keywords_roundtrip(byte* value_ptr, nuint value_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_legacy_value")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* edge_cases_edge_cases_legacy_value(int value, nuint* out_len, FfiError* out_err); -} - -// END EdgeCases.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_cs.snap index 6b7ff4ed..6c990528 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_cs.snap @@ -17,75 +17,148 @@ using System.Threading.Tasks; namespace KitchenSink; /// The errors of the KitchenErrors domain, declared by module kitchen. -/// Catch a nested class for one code, or this class for any of them. -public abstract class KitchenErrorsException : NativeException +/// Catch a nested class for one code, or this class for any of them. A code +/// these bindings don't know (one the library added later) surfaces as this +/// class itself, with its and message. +public class KitchenException : NativeException { - private protected KitchenErrorsException(int code, string message) : base(code, message) - { - } - - /// Writes this code's fields, the error payload a callback - /// reports with it. - internal virtual void WritePayload(FfiBufferWriter writer) + internal KitchenException(int code, string message) : base(code, message) { } /// Item not found - public sealed class NotFound : KitchenErrorsException + public sealed class NotFound : KitchenException { /// The domain code, 1. public const int ErrorCode = 1; - /// Creates the error with , or the - /// default message "Item not found" when it's null. + /// Creates the error, as a callback-interface implementation + /// throws it to report this code. + /// The message, or null for the default "Item not found". public NotFound(string? message = null) : base(ErrorCode, message ?? "Item not found") { } } /// Invalid input - public sealed class InvalidInput : KitchenErrorsException + public sealed class InvalidInput : KitchenException { /// The domain code, 2. public const int ErrorCode = 2; - /// Creates the error with , or the - /// default message "Invalid input" when it's null. + /// Creates the error, as a callback-interface implementation + /// throws it to report this code. + /// The message, or null for the default "Invalid input". public InvalidInput(string? message = null) : base(ErrorCode, message ?? "Invalid input") { } } - /// The exception for a raw code and its serialized payload. - internal static new Exception FromError(int code, string message, byte[]? payload) + /// The exception for a raw code and its payload: a typed code + /// class, this class for a code it doesn't know (or a payload that doesn't + /// decode), or the root mapping for a runtime code. + internal static new Exception FromError(int code, string message, FfiBufferReader payload) { + if (code <= 0) + { + return global::KitchenSink.NativeException.FromError(code, message, payload); + } var given = message.Length == 0 ? null : message; - switch (code) + try + { + switch (code) + { + case NotFound.ErrorCode: + return new NotFound(given); + case InvalidInput.ErrorCode: + return new InvalidInput(given); + } + } + catch (NativeBugException e) when (e.Code == NativeException.MarshalErrorCode) { - case NotFound.ErrorCode: - return new NotFound(given); - case InvalidInput.ErrorCode: - return new InvalidInput(given); - default: - return global::KitchenSink.NativeException.FromError(code, message, payload); + // A payload that doesn't decode keeps the code and message. } + return new KitchenException(code, message); } } -/// A reusable token referenced across modules -public sealed class Token +/// The errors of the PantryError domain, declared by module kitchen. +/// Catch a nested class for one code, or this class for any of them. A code +/// these bindings don't know (one the library added later) surfaces as this +/// class itself, with its and message. +public class PantryException : NativeException { - public long Id { get; } + internal PantryException(int code, string message) : base(code, message) + { + } - public string Label { get; } + /// Out of stock + public sealed class OutOfStock : PantryException + { + /// The domain code, 1. + public const int ErrorCode = 1; + + public string Item { get; } - /// Creates a from every field. - public Token(long id, string label) + /// Creates the error, as a callback-interface implementation + /// throws it to report this code. + /// The message, or null for the default "Out of stock". + public OutOfStock(string item, string? message = null) : base(ErrorCode, message ?? "Out of stock") + { + Item = item; + } + + internal override void WritePayload(FfiBufferWriter writer) + { + writer.WriteString(Item); + } + } + + /// Spoiled + public sealed class Spoiled : PantryException + { + /// The domain code, 2. + public const int ErrorCode = 2; + + /// Creates the error, as a callback-interface implementation + /// throws it to report this code. + /// The message, or null for the default "Spoiled". + public Spoiled(string? message = null) : base(ErrorCode, message ?? "Spoiled") + { + } + } + + /// The exception for a raw code and its payload: a typed code + /// class, this class for a code it doesn't know (or a payload that doesn't + /// decode), or the root mapping for a runtime code. + internal static new Exception FromError(int code, string message, FfiBufferReader payload) { - Id = id; - Label = label; + if (code <= 0) + { + return global::KitchenSink.NativeException.FromError(code, message, payload); + } + var given = message.Length == 0 ? null : message; + try + { + switch (code) + { + case OutOfStock.ErrorCode: + return payload.End(new OutOfStock(payload.ReadString(), given)); + case Spoiled.ErrorCode: + return new Spoiled(given); + } + } + catch (NativeBugException e) when (e.Code == NativeException.MarshalErrorCode) + { + // A payload that doesn't decode keeps the code and message. + } + return new PantryException(code, message); } +} +/// A reusable token referenced across modules +public sealed record Token(long Id, string Label) +{ internal void WriteTo(FfiBufferWriter writer) { writer.WriteI64(Id); @@ -112,57 +185,53 @@ public enum Priority } /// Kitchen sink struct exercising every field feature -public sealed class Item +/// Stable identifier +/// Display name +/// Initial count +/// Whether the item is enabled +/// Scaling ratio +/// Cross-module struct reference +/// An optional object inside a record +public sealed record Item(long Id, string Name, int Count, bool Enabled, double Ratio, uint Nick, byte[] Payload, IReadOnlyList Tags, IReadOnlyDictionary Attrs, long? Parent, Token Token, Priority Priority, Gadget? Gadget) { - /// Stable identifier - public long Id { get; } - - /// Display name - public string Name { get; } - - /// Initial count - public int Count { get; } - - /// Whether the item is enabled - public bool Enabled { get; } - - /// Scaling ratio - public double Ratio { get; } - - public uint Nick { get; } - - public byte[] Payload { get; } - - public string[] Tags { get; } - - public Dictionary Attrs { get; } - - public long? Parent { get; } - - /// Cross-module struct reference - public Token Token { get; } - - public Priority Priority { get; } - - /// An optional object inside a record - public Gadget? Gadget { get; } + /// True when every field of is equal to this + /// one's, comparing byte arrays, lists, and maps by their contents. + public bool Equals(Item? other) + { + return ReferenceEquals(this, other) || other is not null + && FfiEquality.Equal(Id, other.Id) + && FfiEquality.Equal(Name, other.Name) + && FfiEquality.Equal(Count, other.Count) + && FfiEquality.Equal(Enabled, other.Enabled) + && FfiEquality.Equal(Ratio, other.Ratio) + && FfiEquality.Equal(Nick, other.Nick) + && FfiEquality.Bytes(Payload, other.Payload) + && FfiEquality.Lists(Tags, other.Tags) + && FfiEquality.Maps(Attrs, other.Attrs) + && FfiEquality.Equal(Parent, other.Parent) + && FfiEquality.Equal(Token, other.Token) + && FfiEquality.Equal(Priority, other.Priority) + && FfiEquality.Equal(Gadget, other.Gadget); + } - /// Creates a from every field. - public Item(long id, string name, int count, bool enabled, double ratio, uint nick, byte[] payload, string[] tags, Dictionary attrs, long? parent, Token token, Priority priority, Gadget? gadget) + /// + public override int GetHashCode() { - Id = id; - Name = name; - Count = count; - Enabled = enabled; - Ratio = ratio; - Nick = nick; - Payload = payload; - Tags = tags; - Attrs = attrs; - Parent = parent; - Token = token; - Priority = priority; - Gadget = gadget; + var hash = new global::System.HashCode(); + hash.Add(Id); + hash.Add(Name); + hash.Add(Count); + hash.Add(Enabled); + hash.Add(Ratio); + hash.Add(Nick); + hash.Add(FfiEquality.BytesHash(Payload)); + hash.Add(FfiEquality.ListHash(Tags)); + hash.Add(FfiEquality.MapHash(Attrs)); + hash.Add(Parent); + hash.Add(Token); + hash.Add(Priority); + hash.Add(Gadget); + return hash.ToHashCode(); } internal void WriteTo(FfiBufferWriter writer) @@ -174,12 +243,12 @@ public sealed class Item writer.WriteF64(Ratio); writer.WriteU32(Nick); writer.WriteBytes(Payload); - FfiCodecs.WriteListOfString(writer, Tags); - FfiCodecs.WriteMapOfStringToString(writer, Attrs); - FfiCodecs.WriteOptionalOfI64(writer, Parent); + writer.WriteList(Tags, static (w0, v0) => w0.WriteString(v0)); + writer.WriteMap(Attrs, static (w0, v0) => w0.WriteString(v0), static (w0, v0) => w0.WriteString(v0)); + writer.WriteOptionalValue(Parent, static (w0, v0) => w0.WriteI64(v0)); Token.WriteTo(writer); writer.WriteI32((int)Priority); - FfiCodecs.WriteOptionalOfGadget(writer, Gadget); + writer.WriteOptional(Gadget, static (w0, v0) => w0.WriteObject(v0.CloneHandle())); } internal static Item ReadFrom(FfiBufferReader reader) @@ -192,22 +261,22 @@ public sealed class Item reader.ReadF64(), reader.ReadU32(), reader.ReadBytes(), - FfiCodecs.ReadListOfString(reader), - FfiCodecs.ReadMapOfStringToString(reader), - FfiCodecs.ReadOptionalOfI64(reader), + reader.ReadList(static r0 => r0.ReadString()), + reader.ReadMap(static r0 => r0.ReadString(), static r0 => r0.ReadString()), + reader.ReadOptionalValue(static r0 => r0.ReadI64()), global::KitchenSink.Token.ReadFrom(reader), (global::KitchenSink.Priority)reader.ReadI32(), - FfiCodecs.ReadOptionalOfGadget(reader)); + reader.ReadOptional(static r0 => global::KitchenSink.Gadget.Adopt(r0.ReadObject()))); } } /// Consumer-implemented observer of ready items /// The native library may call any method from any thread until -/// it releases its last reference to the implementation. Object arguments -/// belong to the implementation. An exception thrown by a method fails the -/// native library's call in progress: a method documented to throw a domain -/// exception reports it with its code and fields, and any other exception -/// reaches the library as . +/// it releases its last reference to the implementation. Span arguments are +/// valid only during the call; object arguments belong to the implementation. +/// An exception thrown by a method fails the native call in progress: an +/// exception of the domain a method documents is reported with its code and +/// fields, and any other reaches the library as a failure carrying its message. public interface IReadyListener { /// Fires when an item is ready @@ -217,7 +286,7 @@ public interface IReadyListener bool OnItem(Item item, Gadget gadget); /// A display label, failing with a kitchen error - /// Reported to the library with its code and fields. + /// Reported to the library with its code and fields. string Label(); /// The most recent item the listener kept, if any @@ -229,7 +298,7 @@ public interface IReadyListener /// The process-wide kitchen_sink_kitchen_ReadyListener_vtable and the trampolines behind it, /// adapting an to the C ABI. -internal static unsafe class FfiVtable_kitchen_ReadyListener +internal static unsafe class FfiReadyListenerVtable { [StructLayout(LayoutKind.Sequential)] private struct Layout @@ -238,7 +307,7 @@ internal static unsafe class FfiVtable_kitchen_ReadyListener public uint Flags; public delegate* unmanaged[Cdecl] Free; public delegate* unmanaged[Cdecl] on_ready; - public delegate* unmanaged[Cdecl] on_item; + public delegate* unmanaged[Cdecl] on_item; public delegate* unmanaged[Cdecl] label; public delegate* unmanaged[Cdecl] latest; public delegate* unmanaged[Cdecl] favorite; @@ -251,6 +320,7 @@ internal static unsafe class FfiVtable_kitchen_ReadyListener { var vtable = (Layout*)NativeMemory.AllocZeroed((nuint)sizeof(Layout)); vtable->Size = (uint)sizeof(Layout); + // Not thread-affine: every method may be called from any thread. vtable->Flags = 0; vtable->Free = &FreeTrampoline; vtable->on_ready = &OnReadyTrampoline; @@ -276,20 +346,20 @@ internal static unsafe class FfiVtable_kitchen_ReadyListener } catch (Exception e) { - Ffi.SetForeignError(out_err, e); + Ffi.Report(out_err, e, NativeException.ForeignErrorCode); } } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static byte OnItemTrampoline(IntPtr ctx, byte* item_ptr, nuint item_len, IntPtr gadget, FfiError* out_err) + private static bool OnItemTrampoline(IntPtr ctx, byte* item_ptr, nuint item_len, IntPtr gadget, FfiError* out_err) { try { - return (byte)(Ffi.Target(ctx).OnItem(FfiCodecs.Decode(Ffi.ReadBuffer(item_ptr, item_len), static r => global::KitchenSink.Item.ReadFrom(r)), global::KitchenSink.Gadget.Adopt(gadget)) ? 1 : 0); + return Ffi.Target(ctx).OnItem(Ffi.ReadBuffer(item_ptr, item_len, static r0 => global::KitchenSink.Item.ReadFrom(r0)), global::KitchenSink.Gadget.Adopt(gadget)); } catch (Exception e) { - Ffi.SetForeignError(out_err, e); + Ffi.Report(out_err, e, NativeException.ForeignErrorCode); return default; } } @@ -301,13 +371,13 @@ internal static unsafe class FfiVtable_kitchen_ReadyListener { Ffi.ReturnString(Ffi.Target(ctx).Label(), out_ptr, out_len); } - catch (global::KitchenSink.KitchenErrorsException e) + catch (global::KitchenSink.KitchenException e) { - Ffi.SetDomainError(out_err, e.Code, e, e.WritePayload); + Ffi.ReportDomain(out_err, e); } catch (Exception e) { - Ffi.SetForeignError(out_err, e); + Ffi.Report(out_err, e, NativeException.GenericErrorCode); } } @@ -316,14 +386,11 @@ internal static unsafe class FfiVtable_kitchen_ReadyListener { try { - var ffiResult = Ffi.Target(ctx).Latest(); - var ffiWriter = new FfiBufferWriter(); - FfiCodecs.WriteOptionalOfItem(ffiWriter, ffiResult); - Ffi.ReturnBuffer(ffiWriter, out_ptr, out_len); + Ffi.ReturnBuffer(Ffi.Target(ctx).Latest(), static (w0, v0) => w0.WriteOptional(v0, static (w1, v1) => v1.WriteTo(w1)), out_ptr, out_len); } catch (Exception e) { - Ffi.SetForeignError(out_err, e); + Ffi.Report(out_err, e, NativeException.ForeignErrorCode); } } @@ -336,12 +403,114 @@ internal static unsafe class FfiVtable_kitchen_ReadyListener } catch (Exception e) { - Ffi.SetForeignError(out_err, e); + Ffi.Report(out_err, e, NativeException.ForeignErrorCode); return default; } } } +/// Consumer-implemented source of scalar samples +/// The native library may call any method from any thread until +/// it releases its last reference to the implementation. Span arguments are +/// valid only during the call; object arguments belong to the implementation. +/// An exception thrown by a method fails the native call in progress: an +/// exception of the domain a method documents is reported with its code and +/// fields, and any other reaches the library as a failure carrying its message. +public interface ISampler +{ + /// The cutoff for a level, if the sampler has one + double? Threshold(uint? level); + + /// Up to limit samples, failing when the pantry is empty + /// Reported to the library with its code and fields. + short[] Samples(uint? limit, ReadOnlySpan weights); + + /// A free-form description, failing with an untyped error + string Describe(); +} + +/// The process-wide kitchen_sink_kitchen_Sampler_vtable and the trampolines behind it, +/// adapting an to the C ABI. +internal static unsafe class FfiSamplerVtable +{ + [StructLayout(LayoutKind.Sequential)] + private struct Layout + { + public uint Size; + public uint Flags; + public delegate* unmanaged[Cdecl] Free; + public delegate* unmanaged[Cdecl] threshold; + public delegate* unmanaged[Cdecl] samples; + public delegate* unmanaged[Cdecl] describe; + } + + /// The vtable's address, valid for the process lifetime. + internal static readonly IntPtr Pointer = Allocate(); + + private static IntPtr Allocate() + { + var vtable = (Layout*)NativeMemory.AllocZeroed((nuint)sizeof(Layout)); + vtable->Size = (uint)sizeof(Layout); + // Not thread-affine: every method may be called from any thread. + vtable->Flags = 0; + vtable->Free = &FreeTrampoline; + vtable->threshold = &ThresholdTrampoline; + vtable->samples = &SamplesTrampoline; + vtable->describe = &DescribeTrampoline; + return (IntPtr)vtable; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void FreeTrampoline(IntPtr ctx) + { + Ffi.Unregister(ctx); + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static bool ThresholdTrampoline(IntPtr ctx, bool has_level, uint level, double* out_value, FfiError* out_err) + { + try + { + return Ffi.ReturnOptional(Ffi.Target(ctx).Threshold(has_level ? level : null), out_value); + } + catch (Exception e) + { + Ffi.Report(out_err, e, NativeException.ForeignErrorCode); + return default; + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void SamplesTrampoline(IntPtr ctx, bool has_limit, uint limit, double* weights_ptr, nuint weights_len, short** out_ptr, nuint* out_len, FfiError* out_err) + { + try + { + Ffi.ReturnArray(Ffi.Target(ctx).Samples(has_limit ? limit : null, Ffi.Borrow(weights_ptr, weights_len)), out_ptr, out_len); + } + catch (global::KitchenSink.PantryException e) + { + Ffi.ReportDomain(out_err, e); + } + catch (Exception e) + { + Ffi.Report(out_err, e, NativeException.GenericErrorCode); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void DescribeTrampoline(IntPtr ctx, byte** out_ptr, nuint* out_len, FfiError* out_err) + { + try + { + Ffi.ReturnString(Ffi.Target(ctx).Describe(), out_ptr, out_len); + } + catch (Exception e) + { + Ffi.Report(out_err, e, NativeException.GenericErrorCode); + } + } +} + /// A pocket-sized interface exercising the object surface public sealed unsafe class Gadget : IDisposable, IEquatable { @@ -371,7 +540,7 @@ public sealed unsafe class Gadget : IDisposable, IEquatable { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_new(id, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); Handle = new NativeHandle(ffiResult); } @@ -381,27 +550,38 @@ public sealed unsafe class Gadget : IDisposable, IEquatable var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_describe(Handle, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return Ffi.TakeString(ffiResult, ffiOutLen); } /// Poke the gadget a number of times, failing on a negative count - /// The call reported a KitchenErrors code. + /// The call failed with a KitchenErrors code. public int Poke(int times) { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_poke(Handle, times, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.KitchenErrorsException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.KitchenException.FromError); return ffiResult; } + /// The gadget's sensor readings, optionally limited + /// The call failed. + public double[] Readings(uint? limit) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_readings(Handle, limit.HasValue, limit.GetValueOrDefault(), &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeException.FromError); + return Ffi.TakeArray(ffiResult, ffiOutLen); + } + /// The gadget subsystem version string public static string Version() { var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_version(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return Ffi.TakeString(ffiResult, ffiOutLen); } @@ -465,7 +645,7 @@ public static unsafe class Shared var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_shared_ping(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return Ffi.TakeString(ffiResult, ffiOutLen); } } @@ -476,16 +656,16 @@ public static unsafe class Kitchen public static bool BoolId(bool x) { var ffiErr = default(FfiError); - var ffiResult = NativeMethods.kitchen_sink_kitchen_bool_id((byte)(x ? 1 : 0), &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return ffiResult != 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_bool_id(x, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult; } public static int I32Id(int x) { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_i32_id(x, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } @@ -493,7 +673,7 @@ public static unsafe class Kitchen { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_u32_id(x, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } @@ -501,7 +681,7 @@ public static unsafe class Kitchen { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_i64_id(x, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } @@ -509,19 +689,19 @@ public static unsafe class Kitchen { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_f64_id(x, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } public static string EchoString(string s) { - var sBytes = Ffi.Utf8(s); + using var sUtf8 = new FfiUtf8(s); var ffiErr = default(FfiError); nuint ffiOutLen = 0; - fixed (byte* sPtr = sBytes) + fixed (byte* sPtr = sUtf8) { - var ffiResult = NativeMethods.kitchen_sink_kitchen_echo_string(sPtr, (nuint)sBytes.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_echo_string(sPtr, sUtf8.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return Ffi.TakeString(ffiResult, ffiOutLen); } } @@ -533,13 +713,11 @@ public static unsafe class Kitchen fixed (byte* bPtr = b) { var ffiResult = NativeMethods.kitchen_sink_kitchen_echo_bytes(bPtr, (nuint)b.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return Ffi.TakeBytes(ffiResult, ffiOutLen); } } - public static byte[] EchoBytes(byte[] b) => EchoBytes((ReadOnlySpan)b); - /// Register a listener, returning how many are attached public static int Subscribe(IReadyListener listener) { @@ -548,14 +726,14 @@ public static unsafe class Kitchen int ffiResult; try { - ffiResult = NativeMethods.kitchen_sink_kitchen_subscribe(listenerCtx, FfiVtable_kitchen_ReadyListener.Pointer, &ffiErr); + ffiResult = NativeMethods.kitchen_sink_kitchen_subscribe(listenerCtx, FfiReadyListenerVtable.Pointer, &ffiErr); } catch { Ffi.Unregister(listenerCtx); throw; } - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } @@ -564,18 +742,18 @@ public static unsafe class Kitchen { var listenerCtx = Ffi.Register(listener); var ffiErr = default(FfiError); - byte ffiResult; + bool ffiResult; try { - ffiResult = NativeMethods.kitchen_sink_kitchen_maybe_subscribe(listenerCtx, listener == null ? IntPtr.Zero : FfiVtable_kitchen_ReadyListener.Pointer, &ffiErr); + ffiResult = NativeMethods.kitchen_sink_kitchen_maybe_subscribe(listenerCtx, listener == null ? IntPtr.Zero : FfiReadyListenerVtable.Pointer, &ffiErr); } catch { Ffi.Unregister(listenerCtx); throw; } - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return ffiResult != 0; + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult; } /// Optional object in both directions @@ -583,30 +761,36 @@ public static unsafe class Kitchen { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_maybe_gadget(fallback?.Handle ?? global::KitchenSink.Gadget.NativeHandle.Null, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult == IntPtr.Zero ? null : global::KitchenSink.Gadget.Adopt(ffiResult); } /// A list of objects - public static Gadget[] Gadgets() + public static IReadOnlyList Gadgets() { var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_gadgets(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadListOfGadget(r)); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeBuffer(ffiResult, ffiOutLen, static r0 => r0.ReadList(static r1 => global::KitchenSink.Gadget.Adopt(r1.ReadObject()))); } /// An iterator over objects - /// Streams lazily, one native call per item. The sequence can be - /// enumerated once; disposing its enumerator (as foreach does) - /// releases the native iterator. + /// Streams lazily, one native call per item. Each enumeration launches a + /// new native iterator, so a failure to start throws from + /// GetEnumerator (the start of a foreach); disposing the enumerator + /// (as foreach does) releases it. public static IEnumerable StreamGadgets() + { + return new FfiSequence(() => LaunchStreamGadgets(), &NextStreamGadgets); + } + + private static FfiIteratorHandle LaunchStreamGadgets() { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_stream_gadgets(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamGadgetsIterator_destroy), &NextStreamGadgets); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamGadgetsIterator_destroy); } private static bool NextStreamGadgets(FfiIteratorHandle iter, out Gadget item) @@ -614,7 +798,7 @@ public static unsafe class Kitchen IntPtr ffiItem = default; var ffiErr = default(FfiError); var ffiMore = NativeMethods.kitchen_sink_kitchen_StreamGadgetsIterator_next(iter, &ffiItem, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); if (ffiMore == 0) { item = default!; @@ -625,46 +809,52 @@ public static unsafe class Kitchen } /// Optional struct return - /// The call reported a KitchenErrors code. + /// The call failed with a KitchenErrors code. public static Item? MaybeItem(long id) { var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_maybe_item(id, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.KitchenErrorsException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadOptionalOfItem(r)); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.KitchenException.FromError); + return Ffi.TakeBuffer(ffiResult, ffiOutLen, static r0 => r0.ReadOptional(static r1 => global::KitchenSink.Item.ReadFrom(r1))); } /// List of structs - public static Item[] ListItems() + public static IReadOnlyList ListItems() { var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_list_items(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadListOfItem(r)); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeBuffer(ffiResult, ffiOutLen, static r0 => r0.ReadList(static r1 => global::KitchenSink.Item.ReadFrom(r1))); } /// Map return type - public static Dictionary GetAttrs() + public static IReadOnlyDictionary GetAttrs() { var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_get_attrs(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => FfiCodecs.ReadMapOfStringToI32(r)); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeBuffer(ffiResult, ffiOutLen, static r0 => r0.ReadMap(static r1 => r1.ReadString(), static r1 => r1.ReadI32())); } /// Iterator return type - /// Streams lazily, one native call per item. The sequence can be - /// enumerated once; disposing its enumerator (as foreach does) - /// releases the native iterator. + /// Streams lazily, one native call per item. Each enumeration launches a + /// new native iterator, so a failure to start throws from + /// GetEnumerator (the start of a foreach); disposing the enumerator + /// (as foreach does) releases it. public static IEnumerable StreamItems() + { + return new FfiSequence(() => LaunchStreamItems(), &NextStreamItems); + } + + private static FfiIteratorHandle LaunchStreamItems() { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_stream_items(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamItemsIterator_destroy), &NextStreamItems); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamItemsIterator_destroy); } private static bool NextStreamItems(FfiIteratorHandle iter, out Item item) @@ -673,13 +863,13 @@ public static unsafe class Kitchen nuint ffiLen = default; var ffiErr = default(FfiError); var ffiMore = NativeMethods.kitchen_sink_kitchen_StreamItemsIterator_next(iter, &ffiItem, &ffiLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); if (ffiMore == 0) { item = default!; return false; } - item = FfiCodecs.Decode(Ffi.TakeBuffer(ffiItem, ffiLen), static r => global::KitchenSink.Item.ReadFrom(r)); + item = Ffi.TakeBuffer(ffiItem, ffiLen, static r0 => global::KitchenSink.Item.ReadFrom(r0)); return true; } @@ -689,20 +879,27 @@ public static unsafe class Kitchen var ffiErr = default(FfiError); nuint ffiOutLen = 0; var ffiResult = NativeMethods.kitchen_sink_kitchen_cross_module_token(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::KitchenSink.Token.ReadFrom(r)); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeBuffer(ffiResult, ffiOutLen, static r0 => global::KitchenSink.Token.ReadFrom(r0)); } /// Async operation - public static Task DoAsync(string input) + /// Stops waiting: the task completes as + /// canceled at once, and the native call, which can't be cancelled, finishes + /// in the background and releases its result. + public static Task DoAsync(string input, CancellationToken cancellationToken = default) { - var inputBytes = Ffi.Utf8(input); - var ffiCall = new FfiCall(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + using var inputUtf8 = new FfiUtf8(input); + var ffiCall = new FfiCall(cancellationToken); try { - fixed (byte* inputPtr = inputBytes) + fixed (byte* inputPtr = inputUtf8) { - NativeMethods.kitchen_sink_kitchen_do_async(inputPtr, (nuint)inputBytes.Length, &CompleteDoAsync, ffiCall.Context); + NativeMethods.kitchen_sink_kitchen_do_async(inputPtr, inputUtf8.Length, &CompleteDoAsync, ffiCall.Context); } } catch @@ -710,7 +907,7 @@ public static unsafe class Kitchen ffiCall.Abandon(); throw; } - return ffiCall.Task; + return ffiCall.Launched(); } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] @@ -721,7 +918,7 @@ public static unsafe class Kitchen { if (err != null) { - ffiCall.SetError(err, global::KitchenSink.NativeBugException.FromError); + ffiCall.SetError(err, &global::KitchenSink.NativeBugException.FromError); return; } ffiCall.SetResult(Ffi.TakeString(result_ptr, result_len)); @@ -733,21 +930,21 @@ public static unsafe class Kitchen } /// Cancellable async operation - /// Cancels the native call; the task - /// then completes as canceled. + /// Cancels the native call; the task then + /// completes as canceled. public static Task DoCancellable(string input, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - var inputBytes = Ffi.Utf8(input); + using var inputUtf8 = new FfiUtf8(input); var ffiCall = new FfiCall(cancellationToken); try { - fixed (byte* inputPtr = inputBytes) + fixed (byte* inputPtr = inputUtf8) { - NativeMethods.kitchen_sink_kitchen_do_cancellable(inputPtr, (nuint)inputBytes.Length, ffiCall.CancelToken(), &CompleteDoCancellable, ffiCall.Context); + NativeMethods.kitchen_sink_kitchen_do_cancellable(inputPtr, inputUtf8.Length, ffiCall.CancelToken(), &CompleteDoCancellable, ffiCall.Context); } } catch @@ -755,7 +952,7 @@ public static unsafe class Kitchen ffiCall.Abandon(); throw; } - return ffiCall.Task; + return ffiCall.Launched(); } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] @@ -766,7 +963,7 @@ public static unsafe class Kitchen { if (err != null) { - ffiCall.SetError(err, global::KitchenSink.NativeBugException.FromError); + ffiCall.SetError(err, &global::KitchenSink.NativeBugException.FromError); return; } ffiCall.SetResult(Ffi.TakeString(result_ptr, result_len)); @@ -778,256 +975,412 @@ public static unsafe class Kitchen } /// Legacy operation kept for compatibility - [Obsolete("Use new_op instead")] + [Obsolete("Use `NewOp` instead")] public static int LegacyOp() { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_legacy_op(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } - /// Replacement for legacy_op + /// Replacement for LegacyOp public static int NewOp() { var ffiErr = default(FfiError); var ffiResult = NativeMethods.kitchen_sink_kitchen_new_op(&ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); return ffiResult; } -} -public static unsafe class KitchenNested -{ - /// Trivial nested-module function - public static string Hello() + /// Optional scalar in both directions + public static int? OptI32Id(int? x) { var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.kitchen_sink_kitchen_nested_hello(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeBugException.FromError); - return Ffi.TakeString(ffiResult, ffiOutLen); + int ffiValue = default; + var ffiResult = NativeMethods.kitchen_sink_kitchen_opt_i32_id(x.HasValue, x.GetValueOrDefault(), &ffiValue, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult ? ffiValue : null; } -} -/// The value-buffer codec of every composite type the API -/// carries. -internal static class FfiCodecs -{ - /// Reads one value that must fill the whole buffer. - internal static T Decode(FfiBufferReader reader, Func read) + public static bool? OptBoolId(bool? x) { - var value = read(reader); - reader.ExpectEnd(); - return value; + var ffiErr = default(FfiError); + bool ffiValue = default; + var ffiResult = NativeMethods.kitchen_sink_kitchen_opt_bool_id(x.HasValue, x.GetValueOrDefault(), &ffiValue, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult ? ffiValue : null; } - internal static void WriteListOfGadget(FfiBufferWriter writer, Gadget[] value) + /// Optional C-style enum in both directions + public static Priority? OptPriority(Priority? p) { - writer.WriteLen(value.Length); - foreach (var item in value) - { - writer.WriteObject(item.CloneHandle()); - } + var ffiErr = default(FfiError); + global::KitchenSink.Priority ffiValue = default; + var ffiResult = NativeMethods.kitchen_sink_kitchen_opt_priority(p.HasValue, p.GetValueOrDefault(), &ffiValue, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult ? ffiValue : null; } - internal static Gadget[] ReadListOfGadget(FfiBufferReader reader) + /// Optional scalars next to ordinary parameters + public static ushort? OptMixed(double @base, float? scale, long? offset, string? label) { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) + using var labelWriter = new FfiBufferWriter(); + labelWriter.WriteOptional(label, static (w0, v0) => w0.WriteString(v0)); + var ffiErr = default(FfiError); + ushort ffiValue = default; + fixed (byte* labelPtr = labelWriter.Written) { - list.Add(global::KitchenSink.Gadget.Adopt(reader.ReadObject())); + var ffiResult = NativeMethods.kitchen_sink_kitchen_opt_mixed(@base, scale.HasValue, scale.GetValueOrDefault(), offset.HasValue, offset.GetValueOrDefault(), labelPtr, (nuint)labelWriter.Length, &ffiValue, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult ? ffiValue : null; } - return list.ToArray(); } - internal static void WriteListOfItem(FfiBufferWriter writer, Item[] value) + /// A typed-array parameter + public static double SumF64(ReadOnlySpan xs) { - writer.WriteLen(value.Length); - foreach (var item in value) + var ffiErr = default(FfiError); + fixed (double* xsPtr = xs) { - item.WriteTo(writer); + var ffiResult = NativeMethods.kitchen_sink_kitchen_sum_f64(xsPtr, (nuint)xs.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return ffiResult; } } - internal static Item[] ReadListOfItem(FfiBufferReader reader) + /// Typed arrays in both directions + public static int[] ScaleI32(ReadOnlySpan xs, int factor) { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (int* xsPtr = xs) { - list.Add(global::KitchenSink.Item.ReadFrom(reader)); + var ffiResult = NativeMethods.kitchen_sink_kitchen_scale_i32(xsPtr, (nuint)xs.Length, factor, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeArray(ffiResult, ffiOutLen); } - return list.ToArray(); } - internal static void WriteListOfString(FfiBufferWriter writer, string[] value) + public static ulong[] IdsU64(ReadOnlySpan ids) { - writer.WriteLen(value.Length); - foreach (var item in value) + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (ulong* idsPtr = ids) { - writer.WriteString(item); + var ffiResult = NativeMethods.kitchen_sink_kitchen_ids_u64(idsPtr, (nuint)ids.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeArray(ffiResult, ffiOutLen); } } - internal static string[] ReadListOfString(FfiBufferReader reader) + /// Every slice element type + public static uint[] MixedSlices(ReadOnlySpan a, ReadOnlySpan b, ReadOnlySpan c, ReadOnlySpan d, ReadOnlySpan e, ReadOnlySpan f) { - var count = reader.ReadLen(); - var list = new List(reader.Capacity(count)); - for (var i = 0; i < count; i++) + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (sbyte* aPtr = a) + fixed (short* bPtr = b) + fixed (ushort* cPtr = c) + fixed (uint* dPtr = d) + fixed (long* ePtr = e) + fixed (float* fPtr = f) { - list.Add(reader.ReadString()); + var ffiResult = NativeMethods.kitchen_sink_kitchen_mixed_slices(aPtr, (nuint)a.Length, bPtr, (nuint)b.Length, cPtr, (nuint)c.Length, dPtr, (nuint)d.Length, ePtr, (nuint)e.Length, fPtr, (nuint)f.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeArray(ffiResult, ffiOutLen); } - return list.ToArray(); } - internal static void WriteMapOfStringToI32(FfiBufferWriter writer, Dictionary value) + /// Async returning an optional scalar + /// Stops waiting: the task completes as + /// canceled at once, and the native call, which can't be cancelled, finishes + /// in the background and releases its result. + public static Task AsyncMaybeCount(CancellationToken cancellationToken = default) { - writer.WriteLen(value.Count); - foreach (var entry in value) + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + var ffiCall = new FfiCall(cancellationToken); + try + { + NativeMethods.kitchen_sink_kitchen_async_maybe_count(&CompleteAsyncMaybeCount, ffiCall.Context); + } + catch { - writer.WriteString(entry.Key); - writer.WriteI32(entry.Value); + ffiCall.Abandon(); + throw; } + return ffiCall.Launched(); } - internal static Dictionary ReadMapOfStringToI32(FfiBufferReader reader) + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncMaybeCount(IntPtr context, FfiError* err, bool has_result, long result) { - var count = reader.ReadLen(); - var map = new Dictionary(reader.Capacity(count)); - for (var i = 0; i < count; i++) + var ffiCall = FfiCall.Complete(context); + try { - var key = reader.ReadString(); - if (!map.TryAdd(key, reader.ReadI32())) + if (err != null) { - throw FfiBufferReader.Malformed("repeated map key"); + ffiCall.SetError(err, &global::KitchenSink.NativeBugException.FromError); + return; } + ffiCall.SetResult(has_result ? result : null); + } + catch (Exception e) + { + ffiCall.SetException(e); } - return map; } - internal static void WriteMapOfStringToString(FfiBufferWriter writer, Dictionary value) + /// Async returning a typed array + /// Cancels the native call; the task then + /// completes as canceled. + public static Task AsyncSamples(uint n, CancellationToken cancellationToken = default) { - writer.WriteLen(value.Count); - foreach (var entry in value) + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + var ffiCall = new FfiCall(cancellationToken); + try { - writer.WriteString(entry.Key); - writer.WriteString(entry.Value); + NativeMethods.kitchen_sink_kitchen_async_samples(n, ffiCall.CancelToken(), &CompleteAsyncSamples, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; } + return ffiCall.Launched(); } - internal static Dictionary ReadMapOfStringToString(FfiBufferReader reader) + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncSamples(IntPtr context, FfiError* err, float* result_ptr, nuint result_len) { - var count = reader.ReadLen(); - var map = new Dictionary(reader.Capacity(count)); - for (var i = 0; i < count; i++) + var ffiCall = FfiCall.Complete(context); + try { - var key = reader.ReadString(); - if (!map.TryAdd(key, reader.ReadString())) + if (err != null) { - throw FfiBufferReader.Malformed("repeated map key"); + ffiCall.SetError(err, &global::KitchenSink.NativeBugException.FromError); + return; } + ffiCall.SetResult(Ffi.TakeArray(result_ptr, result_len)); + } + catch (Exception e) + { + ffiCall.SetException(e); } - return map; } - internal static void WriteOptionalOfGadget(FfiBufferWriter writer, Gadget? value) + /// Iterator of optional scalars + /// Streams lazily, one native call per item. Each enumeration launches a + /// new native iterator, so a failure to start throws from + /// GetEnumerator (the start of a foreach); disposing the enumerator + /// (as foreach does) releases it. + public static IEnumerable StreamMaybe() { - writer.WriteBool(value != null); - if (value is { } present) + return new FfiSequence(() => LaunchStreamMaybe(), &NextStreamMaybe); + } + + private static FfiIteratorHandle LaunchStreamMaybe() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_stream_maybe(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamMaybeIterator_destroy); + } + + private static bool NextStreamMaybe(FfiIteratorHandle iter, out int? item) + { + bool ffiHas = default; + int ffiItem = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.kitchen_sink_kitchen_StreamMaybeIterator_next(iter, &ffiHas, &ffiItem, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + if (ffiMore == 0) { - writer.WriteObject(present.CloneHandle()); + item = default!; + return false; } + item = ffiHas ? ffiItem : null; + return true; } - internal static Gadget? ReadOptionalOfGadget(FfiBufferReader reader) + /// Iterator of typed arrays + /// Streams lazily, one native call per item. Each enumeration launches a + /// new native iterator, so a failure to start throws from + /// GetEnumerator (the start of a foreach); disposing the enumerator + /// (as foreach does) releases it. + public static IEnumerable StreamChunks() { - return reader.ReadBool() ? global::KitchenSink.Gadget.Adopt(reader.ReadObject()) : null; + return new FfiSequence(() => LaunchStreamChunks(), &NextStreamChunks); } - internal static void WriteOptionalOfI64(FfiBufferWriter writer, long? value) + private static FfiIteratorHandle LaunchStreamChunks() { - writer.WriteBool(value != null); - if (value is { } present) + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_stream_chunks(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamChunksIterator_destroy); + } + + private static bool NextStreamChunks(FfiIteratorHandle iter, out int[] item) + { + int* ffiItem = default; + nuint ffiLen = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.kitchen_sink_kitchen_StreamChunksIterator_next(iter, &ffiItem, &ffiLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + if (ffiMore == 0) { - writer.WriteI64(present); + item = default!; + return false; } + item = Ffi.TakeArray(ffiItem, ffiLen); + return true; } - internal static long? ReadOptionalOfI64(FfiBufferReader reader) + /// Drive a consumer-implemented sampler + /// The call failed with a PantryError code. + public static short[] SampleWith(ISampler sampler, uint? level) { - return reader.ReadBool() ? reader.ReadI64() : null; + var samplerCtx = Ffi.Register(sampler); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + short* ffiResult; + try + { + ffiResult = NativeMethods.kitchen_sink_kitchen_sample_with(samplerCtx, FfiSamplerVtable.Pointer, level.HasValue, level.GetValueOrDefault(), &ffiOutLen, &ffiErr); + } + catch + { + Ffi.Unregister(samplerCtx); + throw; + } + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.PantryException.FromError); + return Ffi.TakeArray(ffiResult, ffiOutLen); } - internal static void WriteOptionalOfItem(FfiBufferWriter writer, Item? value) + /// Fails with the second domain + /// The call failed with a PantryError code. + public static int TakeFromPantry(string item) { - writer.WriteBool(value != null); - if (value is { } present) + using var itemUtf8 = new FfiUtf8(item); + var ffiErr = default(FfiError); + fixed (byte* itemPtr = itemUtf8) { - present.WriteTo(writer); + var ffiResult = NativeMethods.kitchen_sink_kitchen_take_from_pantry(itemPtr, itemUtf8.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.PantryException.FromError); + return ffiResult; } } - internal static Item? ReadOptionalOfItem(FfiBufferReader reader) + /// Fails with an untyped error + /// The call failed. + public static long ParseAny(string text) { - return reader.ReadBool() ? global::KitchenSink.Item.ReadFrom(reader) : null; + using var textUtf8 = new FfiUtf8(text); + var ffiErr = default(FfiError); + fixed (byte* textPtr = textUtf8) + { + var ffiResult = NativeMethods.kitchen_sink_kitchen_parse_any(textPtr, textUtf8.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeException.FromError); + return ffiResult; + } + } +} + +public static unsafe class KitchenNested +{ + /// Trivial nested-module function + public static string Hello() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_nested_hello(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, &global::KitchenSink.NativeBugException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); } } internal static unsafe partial class NativeMethods { - static partial void VerifyContracts() + static partial void VerifyContracts(IntPtr library) { - VerifyContract("kitchen_sink_shared_contract", &kitchen_sink_shared_contract, new (ulong, ulong, string)[] + VerifyContract(library, "kitchen_sink_shared_contract", new (ulong, ulong, string)[] { - (0x709e234caa962446UL, 0x3894f2ca452e9f98UL, "shared.ping"), - (0xcfb306fa5801ee5bUL, 0x1cbd9e10983cac2cUL, "shared.Token"), + (0x709e234caa962446UL, 0x3894f2ca452e9f98UL, "shared.ping"), // function ping() -> string + (0xcfb306fa5801ee5bUL, 0x18e9fc204a13df6fUL, "shared.Token"), // record Token {i64, string} }); - VerifyContract("kitchen_sink_kitchen_contract", &kitchen_sink_kitchen_contract, new (ulong, ulong, string)[] - { - (0x01d4f09bbddc986fUL, 0x29eb88fecc66544dUL, "kitchen.Gadget.new"), - (0x10472d8fe748c2eeUL, 0xb265832b6a759fc4UL, "kitchen.maybe_gadget"), - (0x11368eec07a74078UL, 0xfa049f78b77e146aUL, "kitchen.gadgets"), - (0x137f08a29cde6021UL, 0xda8de7aeb2dbb8a3UL, "kitchen.stream_gadgets"), - (0x2111b54980826dd7UL, 0x452783f3e6ca6e1dUL, "kitchen.do_cancellable"), - (0x29dddbc60bf8d7dbUL, 0x452a3e573d57f6dbUL, "kitchen.Priority"), - (0x2b227003b74a0d16UL, 0x12332957470e9782UL, "kitchen.maybe_subscribe"), - (0x2dd66cb0064f0494UL, 0x60197363fefd92ddUL, "kitchen.Item"), - (0x33c265bbf079b0e9UL, 0xc3dcd0927e6c1debUL, "kitchen.u32_id"), - (0x33e6381d01660dfeUL, 0x077c63c31fd7f90aUL, "kitchen.KitchenErrors"), - (0x3f5fc99d73504b76UL, 0x2df88d710f1ea10eUL, "kitchen.Gadget.poke"), - (0x4c5764cf109528d1UL, 0x2a65529af4338a58UL, "kitchen.maybe_item"), - (0x4de17dfb93fd494dUL, 0xba0ad0dd52ed74afUL, "kitchen.i32_id"), - (0x504e95b7f7e65570UL, 0x01b3e8e0c9a10e23UL, "kitchen.stream_items"), - (0x68cdb9a4bd3e3dd5UL, 0xc678b9164348d497UL, "kitchen.bool_id"), - (0x69e7cd703d16eb85UL, 0xf3b28dd22ae9e689UL, "kitchen.do_async"), - (0x70865079ce41be82UL, 0xbe3b69464672af46UL, "kitchen.Gadget.describe"), - (0x71c5a1116cb7c49aUL, 0xcba1837108a89b84UL, "kitchen.echo_bytes"), - (0x735ee1e0049ee91aUL, 0xca74320bc53eddabUL, "kitchen.echo_string"), - (0x76d7fc58f4e41d84UL, 0x7373a0e47d30e690UL, "kitchen.i64_id"), - (0x79abfe1aa960fd6aUL, 0xbca7a9e5eb26f3a6UL, "kitchen.nested.hello"), - (0x7d3dd7d5cf6749e3UL, 0xa192f5a7298adb39UL, "kitchen.new_op"), - (0x89a5e664bc25c67bUL, 0x5764a95e81ecfd31UL, "kitchen.f64_id"), - (0x8cfdbdcecac5f7f1UL, 0x07e272f09c41fffcUL, "kitchen.Gadget.version"), - (0x974b9f3e823c1738UL, 0xb81b91e975695f27UL, "kitchen.cross_module_token"), - (0xaca0655fac15d230UL, 0x11c4d2b28abe07bbUL, "kitchen.list_items"), - (0xc883b4afae20543eUL, 0xc40560e5411ff392UL, "kitchen.legacy_op"), - (0xf2f7753e6e1dffc2UL, 0xc9047a95bdf49e93UL, "kitchen.get_attrs"), - (0xfae25d3391922eadUL, 0x1c7f95739f3210b8UL, "kitchen.subscribe"), - (0xfd0059b05cb2fcfbUL, 0x248bf8404a5823b4UL, "kitchen.Gadget"), - (0xfed9c551fb9fd2d6UL, 0x59aec980ef3f3291UL, "kitchen.ReadyListener"), + VerifyContract(library, "kitchen_sink_kitchen_contract", new (ulong, ulong, string)[] + { + (0x01d4f09bbddc986fUL, 0xffcf2bd3b04aa002UL, "kitchen.Gadget.new"), // constructor new(i64) -> Gadget + (0x02bbd5f31c953704UL, 0x2cfa679ea1373e09UL, "kitchen.Sampler.samples"), // callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + (0x10472d8fe748c2eeUL, 0xcc570a2649bbf7dcUL, "kitchen.maybe_gadget"), // function maybe_gadget(Gadget?) -> Gadget? + (0x11368eec07a74078UL, 0xfa049f78b77e146aUL, "kitchen.gadgets"), // function gadgets() -> [Gadget] + (0x137f08a29cde6021UL, 0xda8de7aeb2dbb8a3UL, "kitchen.stream_gadgets"), // function stream_gadgets() -> iter + (0x184bfc4ab0e3fe45UL, 0xb64587477022696bUL, "kitchen.ids_u64"), // function ids_u64([u64]) -> [u64] + (0x2111b54980826dd7UL, 0x627bf01ab365b209UL, "kitchen.do_cancellable"), // function do_cancellable(string) -> string async cancellable + (0x21c70f9371374d74UL, 0x25d5d88a30fe0a79UL, "kitchen.ReadyListener.favorite"), // callback_method #4 favorite() -> Gadget + (0x23de5bd597372f46UL, 0xcf1bc6bb37872798UL, "kitchen.stream_chunks"), // function stream_chunks() -> iter<[i32]> + (0x249f1ddaec6a6cbeUL, 0x0710d6c9a4de93daUL, "kitchen.take_from_pantry"), // function take_from_pantry(string) -> i32 throws PantryError + (0x29dddbc60bf8d7dbUL, 0x452a3e573d57f6dbUL, "kitchen.Priority"), // enum Priority {Low = 0, Normal = 1, High = 2} + (0x2b227003b74a0d16UL, 0x5dae036b8a6aed30UL, "kitchen.maybe_subscribe"), // function maybe_subscribe(ReadyListener?) -> bool + (0x2dd66cb0064f0494UL, 0x74b131724dd0d867UL, "kitchen.Item"), // record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + (0x33c265bbf079b0e9UL, 0x97c32b764693f2cbUL, "kitchen.u32_id"), // function u32_id(u32) -> u32 + (0x33e6381d01660dfeUL, 0x05e35a563089bf7dUL, "kitchen.KitchenErrors"), // errors KitchenErrors + (0x3d969ae76db6cb23UL, 0x65a325cfc772296bUL, "kitchen.ReadyListener.latest"), // callback_method #3 latest() -> Item? + (0x3f5fc99d73504b76UL, 0x417e4a2acd3210b3UL, "kitchen.Gadget.poke"), // method poke(i32) -> i32 throws KitchenErrors + (0x4082f0085db28781UL, 0x18848e701962912eUL, "kitchen.Sampler"), // callback Sampler + (0x4340a6b1e7ec8c50UL, 0xa59d0f32a66dc7a2UL, "kitchen.Sampler.describe"), // callback_method #2 describe() -> string throws any + (0x4c5764cf109528d1UL, 0xb374accf14676d90UL, "kitchen.maybe_item"), // function maybe_item(i64) -> Item? throws KitchenErrors + (0x4de17dfb93fd494dUL, 0x78be3203bcd10727UL, "kitchen.i32_id"), // function i32_id(i32) -> i32 + (0x504e95b7f7e65570UL, 0x01b3e8e0c9a10e23UL, "kitchen.stream_items"), // function stream_items() -> iter + (0x5dafabb7775fcb2dUL, 0x6c4b78c7d3f858c1UL, "kitchen.PantryError.Spoiled"), // code Spoiled = 2 + (0x5e0a8f6be4b00600UL, 0x18ea1a42036c96a0UL, "kitchen.Sampler.threshold"), // callback_method #0 threshold(u32?) -> f64? + (0x5ec0af1d4ecc6fedUL, 0x6540bdb74246420cUL, "kitchen.async_samples"), // function async_samples(u32) -> [f32] async cancellable + (0x61a358b0571e0a4eUL, 0x11a24c4b50d11996UL, "kitchen.async_maybe_count"), // function async_maybe_count() -> i64? async + (0x68cdb9a4bd3e3dd5UL, 0xcde022fe6bc3b1bfUL, "kitchen.bool_id"), // function bool_id(bool) -> bool + (0x69e7cd703d16eb85UL, 0xb13cb8d39e702c79UL, "kitchen.do_async"), // function do_async(string) -> string async + (0x70865079ce41be82UL, 0xbe3b69464672af46UL, "kitchen.Gadget.describe"), // method describe() -> string + (0x71c5a1116cb7c49aUL, 0x1506cfe6f548fac4UL, "kitchen.echo_bytes"), // function echo_bytes(bytes) -> bytes + (0x735ee1e0049ee91aUL, 0xa713dbd1be707752UL, "kitchen.echo_string"), // function echo_string(string) -> string + (0x76d7fc58f4e41d84UL, 0x6bdb4ccea2841d5eUL, "kitchen.i64_id"), // function i64_id(i64) -> i64 + (0x78d91fd617e162b8UL, 0xd88e2753503b36dfUL, "kitchen.stream_maybe"), // function stream_maybe() -> iter + (0x79abfe1aa960fd6aUL, 0xbca7a9e5eb26f3a6UL, "kitchen.nested.hello"), // function hello() -> string + (0x7b3865b536da998cUL, 0x1c4e79d60a20e74fUL, "kitchen.mixed_slices"), // function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + (0x7ba266dc26a58989UL, 0x0f86001cac0c21e8UL, "kitchen.KitchenErrors.InvalidInput"), // code InvalidInput = 2 + (0x7d3dd7d5cf6749e3UL, 0xa192f5a7298adb39UL, "kitchen.new_op"), // function new_op() -> i32 + (0x81ded69adee0abf2UL, 0x8c71141a931906f6UL, "kitchen.scale_i32"), // function scale_i32([i32], i32) -> [i32] + (0x81fa72769330fed6UL, 0x89681e305b55a4f4UL, "kitchen.ReadyListener.label"), // callback_method #2 label() -> string throws KitchenErrors + (0x89a5e664bc25c67bUL, 0x357fd9b65ad879a9UL, "kitchen.f64_id"), // function f64_id(f64) -> f64 + (0x8cfdbdcecac5f7f1UL, 0x07e272f09c41fffcUL, "kitchen.Gadget.version"), // static version() -> string + (0x8dc42397e47614b9UL, 0xcddf6fede09b2e7fUL, "kitchen.opt_priority"), // function opt_priority(Priority?) -> Priority? + (0x92f060ef68961fc1UL, 0x4af6ce5b4f9fa508UL, "kitchen.ReadyListener.on_item"), // callback_method #1 on_item(Item, Gadget) -> bool + (0x974b9f3e823c1738UL, 0xb81b91e975695f27UL, "kitchen.cross_module_token"), // function cross_module_token() -> Token + (0x9f0f001aa85f97d5UL, 0x55815247f895bd45UL, "kitchen.sum_f64"), // function sum_f64([f64]) -> f64 + (0xab2a29fd1ef5458bUL, 0x471a34aa4c09ae2bUL, "kitchen.KitchenErrors.NotFound"), // code NotFound = 1 + (0xaca0655fac15d230UL, 0x11c4d2b28abe07bbUL, "kitchen.list_items"), // function list_items() -> [Item] + (0xb57c8e6ae3fb4383UL, 0x3f857851ab75366cUL, "kitchen.PantryError"), // errors PantryError + (0xbdc795b195edd428UL, 0x36127462f8b0969fUL, "kitchen.opt_mixed"), // function opt_mixed(f64, f32?, i64?, string?) -> u16? + (0xbe4821ab3c340e71UL, 0xe1d32e38b766e623UL, "kitchen.ReadyListener.on_ready"), // callback_method #0 on_ready(i32, string) -> void + (0xc2ad53f2a6a36323UL, 0x071e29abee823f17UL, "kitchen.opt_i32_id"), // function opt_i32_id(i32?) -> i32? + (0xc7ab409554fd3d7bUL, 0xfea12b6bc219fc46UL, "kitchen.parse_any"), // function parse_any(string) -> i64 throws any + (0xc82a7f2d85f99f4eUL, 0xa9bcb8bacedb036cUL, "kitchen.sample_with"), // function sample_with(Sampler, u32?) -> [i16] throws PantryError + (0xc883b4afae20543eUL, 0xc40560e5411ff392UL, "kitchen.legacy_op"), // function legacy_op() -> i32 + (0xd180bf646bff59e2UL, 0xa4515a6d00d7c600UL, "kitchen.PantryError.OutOfStock"), // code OutOfStock = 1 {string} + (0xe6778df0f5c1b82bUL, 0x38eaafdb350559e1UL, "kitchen.opt_bool_id"), // function opt_bool_id(bool?) -> bool? + (0xf2f7753e6e1dffc2UL, 0xc9047a95bdf49e93UL, "kitchen.get_attrs"), // function get_attrs() -> {string:i32} + (0xfae25d3391922eadUL, 0xcc034cf506cae182UL, "kitchen.subscribe"), // function subscribe(ReadyListener) -> i32 + (0xfd0059b05cb2fcfbUL, 0x248bf8404a5823b4UL, "kitchen.Gadget"), // interface Gadget + (0xfed9c551fb9fd2d6UL, 0xe1020573f084dc1dUL, "kitchen.ReadyListener"), // callback ReadyListener + (0xff3a99b52597dee2UL, 0x14c4ecac97bdfc9bUL, "kitchen.Gadget.readings"), // method readings(u32?) -> [f64] throws any }); } - [LibraryImport(LibName, EntryPoint = "kitchen_sink_shared_contract")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial FfiContractEntry* kitchen_sink_shared_contract(nuint* out_len); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_contract")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial FfiContractEntry* kitchen_sink_kitchen_contract(nuint* out_len); - [LibraryImport(LibName, EntryPoint = "kitchen_sink_shared_ping")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] internal static partial byte* kitchen_sink_shared_ping(nuint* out_len, FfiError* out_err); @@ -1052,13 +1405,18 @@ internal static unsafe partial class NativeMethods [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] internal static partial int kitchen_sink_kitchen_Gadget_poke(Gadget.NativeHandle self, int times, FfiError* out_err); + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_readings")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial double* kitchen_sink_kitchen_Gadget_readings(Gadget.NativeHandle self, [MarshalAs(UnmanagedType.U1)] bool has_limit, uint limit, nuint* out_len, FfiError* out_err); + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_version")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] internal static partial byte* kitchen_sink_kitchen_Gadget_version(nuint* out_len, FfiError* out_err); [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_bool_id")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte kitchen_sink_kitchen_bool_id(byte x, FfiError* out_err); + [return: MarshalAs(UnmanagedType.U1)] + internal static partial bool kitchen_sink_kitchen_bool_id([MarshalAs(UnmanagedType.U1)] bool x, FfiError* out_err); [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_i32_id")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] @@ -1090,7 +1448,8 @@ internal static unsafe partial class NativeMethods [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_maybe_subscribe")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte kitchen_sink_kitchen_maybe_subscribe(IntPtr listener_ctx, IntPtr listener_vtable, FfiError* out_err); + [return: MarshalAs(UnmanagedType.U1)] + internal static partial bool kitchen_sink_kitchen_maybe_subscribe(IntPtr listener_ctx, IntPtr listener_vtable, FfiError* out_err); [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_maybe_gadget")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] @@ -1156,6 +1515,86 @@ internal static unsafe partial class NativeMethods [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] internal static partial int kitchen_sink_kitchen_new_op(FfiError* out_err); + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_opt_i32_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.U1)] + internal static partial bool kitchen_sink_kitchen_opt_i32_id([MarshalAs(UnmanagedType.U1)] bool has_x, int x, int* out_value, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_opt_bool_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.U1)] + internal static partial bool kitchen_sink_kitchen_opt_bool_id([MarshalAs(UnmanagedType.U1)] bool has_x, [MarshalAs(UnmanagedType.U1)] bool x, bool* out_value, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_opt_priority")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.U1)] + internal static partial bool kitchen_sink_kitchen_opt_priority([MarshalAs(UnmanagedType.U1)] bool has_p, global::KitchenSink.Priority p, global::KitchenSink.Priority* out_value, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_opt_mixed")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + [return: MarshalAs(UnmanagedType.U1)] + internal static partial bool kitchen_sink_kitchen_opt_mixed(double @base, [MarshalAs(UnmanagedType.U1)] bool has_scale, float scale, [MarshalAs(UnmanagedType.U1)] bool has_offset, long offset, byte* label_ptr, nuint label_len, ushort* out_value, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_sum_f64")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial double kitchen_sink_kitchen_sum_f64(double* xs_ptr, nuint xs_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_scale_i32")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int* kitchen_sink_kitchen_scale_i32(int* xs_ptr, nuint xs_len, int factor, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_ids_u64")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong* kitchen_sink_kitchen_ids_u64(ulong* ids_ptr, nuint ids_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_mixed_slices")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint* kitchen_sink_kitchen_mixed_slices(sbyte* a_ptr, nuint a_len, short* b_ptr, nuint b_len, ushort* c_ptr, nuint c_len, uint* d_ptr, nuint d_len, long* e_ptr, nuint e_len, float* f_ptr, nuint f_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_async_maybe_count")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_async_maybe_count(delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_async_samples")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_async_samples(uint n, IntPtr cancel_token, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_stream_maybe")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_stream_maybe(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamMaybeIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_StreamMaybeIterator_next(FfiIteratorHandle iter, bool* out_has_item, int* out_item, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamMaybeIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_StreamMaybeIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_stream_chunks")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_stream_chunks(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamChunksIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_StreamChunksIterator_next(FfiIteratorHandle iter, int** out_item, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamChunksIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_StreamChunksIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_sample_with")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial short* kitchen_sink_kitchen_sample_with(IntPtr sampler_ctx, IntPtr sampler_vtable, [MarshalAs(UnmanagedType.U1)] bool has_level, uint level, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_take_from_pantry")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_take_from_pantry(byte* item_ptr, nuint item_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_parse_any")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial long kitchen_sink_kitchen_parse_any(byte* text_ptr, nuint text_len, FfiError* out_err); + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_nested_hello")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] internal static partial byte* kitchen_sink_kitchen_nested_hello(nuint* out_len, FfiError* out_err); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_csproj.snap deleted file mode 100644 index 3b5dda59..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_csproj.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - net8.0 - KitchenSink - KitchenSink - KitchenSink - 0.1.0 - Native bindings for kitchen_sink - enable - true - true - - $(NoWarn);CS0618 - - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap deleted file mode 100644 index 555101d6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# KitchenSink (.NET) - -Native bindings for kitchen_sink - -The `KitchenSink` namespace binds the `kitchen_sink` native library through -source-generated P/Invoke (`[LibraryImport]`), so it's trim- and -AOT-friendly. - -## Build - -```bash -dotnet build -``` - -Reference this project from yours (`dotnet add reference KitchenSink.csproj`) -or pack it (`dotnet pack -c Release`). - -## Loading the native library - -The bindings load `kitchen_sink` with the platform's normal rules -(`libkitchen_sink.dylib`, `libkitchen_sink.so`, or `kitchen_sink.dll` next to the app or on the -library search path). Set `KITCHEN_SINK_LIBRARY` to a full path to load a -specific file instead. - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__Runtime_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__Runtime_cs.snap deleted file mode 100644 index 0d4f3fa1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__Runtime_cs.snap +++ /dev/null @@ -1,849 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#nullable enable - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace KitchenSink; - -/// The root of every error a throwing call reports. Each typed domain -/// exception derives from it, and a runtime failure of a throwing call (an -/// untyped producer error, a panic, a marshalling failure, or a failed -/// callback-interface implementation) surfaces as this type itself, with its -/// negative . -public class NativeException : Exception -{ - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation failed; the message carries - /// the implementation's message. - public const int ForeignErrorCode = -4; - /// The call was cancelled. Surfaces as - /// , never as this type. - public const int CancelledErrorCode = -5; - - /// The error code the native library reported: positive for a - /// domain error, negative for a runtime failure. - public int Code { get; } - - /// Creates an exception carrying a native error code. - public NativeException(int code, string message) : base(message) - { - Code = code; - } - - /// Maps a code outside every declared domain to its exception: - /// cancellation to , anything else - /// to this type. - internal static Exception FromError(int code, string message, byte[]? payload) - { - if (code == CancelledErrorCode) - { - return new OperationCanceledException(message); - } - return new NativeException(code, message); - } -} - -/// A call that can't fail failed anyway: a bug in the native library, -/// or a callback failure the call couldn't report, rather than an error to -/// handle. is the runtime code (-2 a producer panic, -3 a -/// marshalling failure, -4 a failed callback, and so on), and the message -/// names it along with the producer's message. -public sealed class NativeBugException : InvalidOperationException -{ - /// The error code the native library reported. - public int Code { get; } - - /// Creates the exception for a failed call that can't - /// fail. - public NativeBugException(int code, string message) - : base($"native call failed with code {code}: {message}") - { - Code = code; - } - - /// Maps a failure of a call that isn't declared to throw: - /// cancellation to , anything else - /// to this type. - internal static Exception FromError(int code, string message, byte[]? payload) - { - if (code == NativeException.CancelledErrorCode) - { - return new OperationCanceledException(message); - } - return new NativeBugException(code, message); - } -} - -/// Maps a native error code, message, and serialized payload to the -/// exception a call throws. -internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); - -/// The native kitchen_sink_error slot. -[StructLayout(LayoutKind.Sequential)] -internal unsafe struct FfiError -{ - public int Code; - public byte* Message; - public byte* PayloadPtr; - public nuint PayloadLen; -} - -/// The native kitchen_sink_contract_entry: one declaration's -/// fingerprint in a module's contract table. -[StructLayout(LayoutKind.Sequential)] -internal struct FfiContractEntry -{ - public ulong Id; - public ulong Hash; -} - -/// Marshalling helpers shared by every generated call. -internal static unsafe class Ffi -{ - /// The UTF-8 bytes of a string argument. The native side borrows - /// them as a (pointer, length) pair, so interior NULs survive. - internal static byte[] Utf8(string value) - { - return Encoding.UTF8.GetBytes(value); - } - - /// Decodes a borrowed UTF-8 run; the native side keeps - /// ownership. - internal static string ReadString(byte* ptr, nuint len) - { - return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); - } - - /// Copies a borrowed byte run; the native side keeps - /// ownership. - internal static byte[] ReadBytes(byte* ptr, nuint len) - { - return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); - } - - /// A reader over a copy of a borrowed value buffer; the native - /// side keeps ownership. - internal static FfiBufferReader ReadBuffer(byte* ptr, nuint len) - { - return new FfiBufferReader(ReadBytes(ptr, len)); - } - - /// Decodes a returned UTF-8 string and releases it. - internal static string TakeString(byte* ptr, nuint len) - { - try - { - return ReadString(ptr, len); - } - finally - { - NativeMethods.FreeBytes(ptr, len); - } - } - - /// Copies returned bytes and releases them. - internal static byte[] TakeBytes(byte* ptr, nuint len) - { - try - { - return ReadBytes(ptr, len); - } - finally - { - NativeMethods.FreeBytes(ptr, len); - } - } - - /// Copies a returned value buffer, releases it, and returns a - /// reader over the copy. - internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) - { - return new FfiBufferReader(TakeBytes(ptr, len)); - } - - /// Releases a caller-owned error slot and returns the exception - /// it described. - internal static Exception TakeError(FfiError* err, FfiErrorMap map) - { - var code = err->Code; - var message = ErrorMessage(err); - var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); - NativeMethods.ErrorClear(err); - return map(code, message, payload.Length == 0 ? null : payload); - } - - /// Releases a heap-boxed async error and returns the exception it - /// described. - internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) - { - code = err->Code; - var message = ErrorMessage(err); - var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); - NativeMethods.ErrorFree(err); - return map(code, message, payload.Length == 0 ? null : payload); - } - - private static string ErrorMessage(FfiError* err) - { - return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; - } - - /// Reports a failed callback-interface method to the producer - /// through out_err: with the exception's - /// message and, for a domain error with fields, the encoded fields as - /// its payload. The producer copies both. - internal static void SetError(FfiError* err, int code, Exception e, FfiBufferWriter? payload = null) - { - var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); - fixed (byte* message = bytes) - { - NativeMethods.ErrorSet(err, code, message); - } - if (payload != null && payload.Length > 0) - { - fixed (byte* ptr = payload.Written) - { - NativeMethods.ErrorSetPayload(err, ptr, (nuint)payload.Length); - } - } - } - - /// Reports a domain exception a throwing callback-interface - /// method raised: its positive , its message, and - /// the fields encodes. A failure to - /// encode them is reported as a foreign error instead. - internal static void SetDomainError(FfiError* err, int code, Exception e, Action writePayload) - { - var payload = new FfiBufferWriter(); - try - { - writePayload(payload); - } - catch (Exception inner) - { - SetForeignError(err, inner); - return; - } - SetError(err, code, e, payload); - } - - /// Reports a failed callback-interface method that isn't a - /// declared domain error: the producer receives the foreign error code - /// with the exception's message. - internal static void SetForeignError(FfiError* err, Exception e) - { - SetError(err, NativeException.ForeignErrorCode, e); - } - - /// Hands to the producer through a - /// callback method's out slots, as a run allocated with - /// kitchen_sink_alloc that the producer adopts. - internal static void Return(ReadOnlySpan bytes, byte** outPtr, nuint* outLen) - { - var run = NativeMethods.Alloc((nuint)bytes.Length); - if (run == null && bytes.Length > 0) - { - throw new OutOfMemoryException("the native allocator returned null"); - } - bytes.CopyTo(new Span(run, bytes.Length)); - *outPtr = run; - *outLen = (nuint)bytes.Length; - } - - /// Returns a string from a callback method. - internal static void ReturnString(string value, byte** outPtr, nuint* outLen) - { - Return(Encoding.UTF8.GetBytes(value), outPtr, outLen); - } - - /// Returns bytes from a callback method. - internal static void ReturnBytes(byte[] value, byte** outPtr, nuint* outLen) - { - Return(value, outPtr, outLen); - } - - /// Returns an encoded value buffer from a callback method. - internal static void ReturnBuffer(FfiBufferWriter writer, byte** outPtr, nuint* outLen) - { - Return(writer.Written, outPtr, outLen); - } - - /// Pins a callback-interface implementation for the producer and - /// returns the ctx it hands back to every trampoline. The - /// producer releases it through the vtable's free. An absent - /// optional implementation is a null ctx. - internal static IntPtr Register(object? implementation) - { - return implementation == null ? IntPtr.Zero : GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); - } - - /// Releases a ctx the producer never received (the call - /// failed before reaching it) or no longer holds. - internal static void Unregister(IntPtr ctx) - { - if (ctx != IntPtr.Zero) - { - GCHandle.FromIntPtr(ctx).Free(); - } - } - - /// The implementation behind a trampoline's ctx. - internal static T Target(IntPtr ctx) where T : class - { - return (T)GCHandle.FromIntPtr(ctx).Target!; - } -} - -/// Imports of the native runtime surface plus the load-time checks. -/// The generated API declares its own imports, and the contract it was -/// generated with, in the other half of this partial class. -internal static unsafe partial class NativeMethods -{ - /// The native library's base name. - internal const string LibName = "kitchen_sink"; - - /// The environment variable naming an explicit library - /// path. - internal const string LibraryEnvVar = "KITCHEN_SINK_LIBRARY"; - - /// The C ABI revision these bindings were generated - /// against. - internal const uint AbiVersion = 4; - - // Runs before the first native call through this class, so a library - // built for another ABI revision or missing a declaration these bindings - // use fails here instead of misreading memory later. - static NativeMethods() - { - try - { - NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); - } - catch (InvalidOperationException) - { - // The assembly already has a resolver; default probing still applies. - } - uint found; - try - { - found = AbiVersionOf(); - } - catch (EntryPointNotFoundException e) - { - throw new InvalidOperationException( - $"the loaded {LibName} library doesn't export kitchen_sink_abi_version; these bindings expect ABI revision {AbiVersion}", e); - } - if (found != AbiVersion) - { - throw new InvalidOperationException( - $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); - } - VerifyContracts(); - } - - static partial void VerifyContracts(); - - private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) - { - if (name != LibName) - { - return IntPtr.Zero; - } - var path = Environment.GetEnvironmentVariable(LibraryEnvVar); - return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); - } - - /// Fails the load unless the loaded library's contract table, - /// read through (the import of - /// ), carries every - /// declaration with an unchanged signature hash. Declarations the - /// library has and these bindings don't are fine. - private static void VerifyContract( - string symbol, - delegate* table, - (ulong Id, ulong Hash, string Path)[] expected) - { - nuint len = 0; - FfiContractEntry* entries; - try - { - entries = table(&len); - } - catch (EntryPointNotFoundException e) - { - throw new InvalidOperationException( - $"the loaded {LibName} library doesn't export {symbol}; regenerate the bindings or rebuild the library", e); - } - var hashes = new Dictionary(checked((int)len)); - for (nuint i = 0; i < len; i++) - { - hashes[entries[i].Id] = entries[i].Hash; - } - foreach (var (id, hash, path) in expected) - { - if (!hashes.TryGetValue(id, out var actual)) - { - throw new InvalidOperationException( - $"{path} is missing from the library {LibName}; regenerate the bindings or rebuild the library"); - } - if (actual != hash) - { - throw new InvalidOperationException( - $"{path} changed since these bindings were generated; regenerate the bindings or rebuild the library {LibName}"); - } - } - } - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_abi_version")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial uint AbiVersionOf(); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_set")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void ErrorSet(FfiError* err, int code, byte* message); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_set_payload")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void ErrorSetPayload(FfiError* err, byte* ptr, nuint len); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_clear")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void ErrorClear(FfiError* err); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_free")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void ErrorFree(FfiError* err); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_alloc")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* Alloc(nuint len); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_free_bytes")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void FreeBytes(byte* ptr, nuint len); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_cancel_token_create")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial IntPtr CancelTokenCreate(); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_cancel_token_cancel")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void CancelTokenCancel(IntPtr token); - - [LibraryImport(LibName, EntryPoint = "kitchen_sink_cancel_token_destroy")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial void CancelTokenDestroy(IntPtr token); -} - -/// One pending async call: the task it completes, the native -/// context that finds it again, and, for a cancellable function, the -/// native cancel token linked to the caller's -/// . -internal sealed class FfiCall -{ - private readonly TaskCompletionSource _tcs = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly CancellationToken _cancellation; - private readonly object _gate = new object(); - private GCHandle _self; - private IntPtr _token; - private CancellationTokenRegistration _registration; - private bool _completed; - private bool _cancelling; - - internal FfiCall(CancellationToken cancellation = default) - { - _cancellation = cancellation; - _self = GCHandle.Alloc(this); - } - - /// The task the completion resolves. - internal Task Task => _tcs.Task; - - /// The context argument for the launcher. - internal IntPtr Context => GCHandle.ToIntPtr(_self); - - /// Creates the native cancel token for the launcher and cancels - /// it when the caller's fires. - internal IntPtr CancelToken() - { - _token = NativeMethods.CancelTokenCreate(); - if (_cancellation.CanBeCanceled) - { - _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); - } - return _token; - } - - /// Recovers the call behind a completion's context. Runs - /// exactly once per launch, on a producer thread. - internal static FfiCall Complete(IntPtr context) - { - var handle = GCHandle.FromIntPtr(context); - var call = (FfiCall)handle.Target!; - handle.Free(); - call.Release(); - return call; - } - - /// Releases everything when the launcher threw before the - /// producer took the call, so no completion will arrive. - internal void Abandon() - { - _self.Free(); - Release(); - } - - internal void SetResult(T value) - { - _tcs.TrySetResult(value); - } - - internal void SetException(Exception e) - { - _tcs.TrySetException(e); - } - - /// Faults (or cancels, for the cancelled code) the task from a - /// heap-boxed error, releasing the box. - internal unsafe void SetError(FfiError* err, FfiErrorMap map) - { - var e = Ffi.TakeBoxedError(err, map, out var code); - if (code == NativeException.CancelledErrorCode) - { - _tcs.TrySetCanceled(_cancellation); - } - else - { - _tcs.TrySetException(e); - } - } - - // The registration callback and the completion race: whichever runs - // last destroys the consumer's token reference, so a cancel never - // touches a destroyed token. - private void Cancel() - { - lock (_gate) - { - if (_completed) - { - return; - } - _cancelling = true; - } - NativeMethods.CancelTokenCancel(_token); - bool destroy; - lock (_gate) - { - _cancelling = false; - destroy = _completed; - } - if (destroy) - { - NativeMethods.CancelTokenDestroy(_token); - } - } - - private void Release() - { - if (_token == IntPtr.Zero) - { - return; - } - bool destroy; - lock (_gate) - { - _completed = true; - destroy = !_cancelling; - } - if (destroy) - { - _registration.Dispose(); - NativeMethods.CancelTokenDestroy(_token); - } - } -} - -/// Owns one native iterator and destroys it exactly once, when -/// enumeration ends, the enumerator is disposed, or the garbage collector -/// finalizes an abandoned one. -internal sealed unsafe class FfiIteratorHandle : SafeHandle -{ - private readonly delegate* _destroy; - - internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) - : base(IntPtr.Zero, true) - { - _destroy = destroy; - SetHandle(iterator); - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - _destroy(handle); - return true; - } -} - -/// A lazily streamed sequence backed by a native iterator. It can be -/// enumerated once; each MoveNext pulls one item. -internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator -{ - private readonly FfiIteratorHandle _iterator; - private readonly delegate* _next; - private int _started; - private T _current = default!; - - internal FfiSequence(FfiIteratorHandle iterator, delegate* next) - { - _iterator = iterator; - _next = next; - } - - public T Current => _current; - - object? IEnumerator.Current => _current; - - public IEnumerator GetEnumerator() - { - if (Interlocked.Exchange(ref _started, 1) != 0) - { - throw new InvalidOperationException("this sequence can be enumerated only once"); - } - return this; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public bool MoveNext() - { - if (_iterator.IsClosed) - { - return false; - } - if (_next(_iterator, out _current)) - { - return true; - } - _iterator.Dispose(); - return false; - } - - public void Reset() - { - throw new NotSupportedException("a native iterator can't be reset"); - } - - public void Dispose() - { - _iterator.Dispose(); - } -} - -/// Serializes values into the value-buffer wire format -/// (little-endian, packed). -internal sealed class FfiBufferWriter -{ - private byte[] _buf = new byte[64]; - private int _len; - - /// The bytes written so far. - internal ReadOnlySpan Written => _buf.AsSpan(0, _len); - - /// The number of bytes written so far. - internal int Length => _len; - - private Span Grow(int extra) - { - if (_len + extra > _buf.Length) - { - Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); - } - var span = _buf.AsSpan(_len, extra); - _len += extra; - return span; - } - - internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); - - internal void WriteI8(sbyte v) => WriteU8((byte)v); - - internal void WriteU8(byte v) => Grow(1)[0] = v; - - internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); - - internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); - - internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); - - internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); - - internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); - - internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); - - internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); - - internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); - - internal void WriteLen(int len) => WriteU32((uint)len); - - internal void WriteString(string v) - { - var count = Encoding.UTF8.GetByteCount(v); - WriteLen(count); - Encoding.UTF8.GetBytes(v, Grow(count)); - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - v.CopyTo(Grow(v.Length)); - } - - /// Writes an object token. The token must carry its own strong - /// reference (a fresh clone), which the reader adopts. - internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); -} - -/// Decodes values from the value-buffer wire format. A malformed -/// buffer is a bug on the side that encoded it and throws -/// with the marshalling code. -internal sealed class FfiBufferReader -{ - private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal FfiBufferReader(byte[] data) - { - _data = data; - } - - /// The exception for a buffer that breaks the wire - /// format. - internal static NativeBugException Malformed(string what) - { - return new NativeBugException(NativeException.MarshalErrorCode, "malformed value buffer: " + what); - } - - private ReadOnlySpan Take(int n) - { - if (_data.Length - _pos < n) - { - throw Malformed("buffer exhausted"); - } - var span = _data.AsSpan(_pos, n); - _pos += n; - return span; - } - - internal bool ReadBool() - { - var b = ReadU8(); - if (b > 1) - { - throw Malformed("invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() => (sbyte)ReadU8(); - - internal byte ReadU8() => Take(1)[0]; - - internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); - - internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); - - internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); - - internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); - - internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); - - internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); - - internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); - - internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); - - /// A collection count. Elements can be zero-sized, so the count - /// isn't checked against the remaining bytes; callers cap their - /// preallocation with . - internal int ReadLen() - { - var len = ReadU32(); - if (len > int.MaxValue) - { - throw Malformed("length prefix out of range"); - } - return (int)len; - } - - /// A preallocation size for elements - /// that a malformed count can't inflate. - internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); - - internal string ReadString() - { - var bytes = Take(ReadLen()); - try - { - return Utf8Strict.GetString(bytes); - } - catch (ArgumentException) - { - throw Malformed("string is not valid UTF-8"); - } - } - - internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); - - /// Reads an object token carrying one strong reference. - internal IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw Malformed("null object token"); - } - return (IntPtr)(nint)token; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw Malformed("trailing bytes"); - } - } -} - -// END Runtime.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_cs.snap deleted file mode 100644 index a0473c7c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_cs.snap +++ /dev/null @@ -1,261 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace NestedModules; - -/// The errors of the GraphicsError domain, declared by module graphics. -/// Catch a nested class for one code, or this class for any of them. -public abstract class GraphicsException : NativeException -{ - private protected GraphicsException(int code, string message) : base(code, message) - { - } - - /// Writes this code's fields, the error payload a callback - /// reports with it. - internal virtual void WritePayload(FfiBufferWriter writer) - { - } - - /// coordinate out of bounds - public sealed class OutOfBounds : GraphicsException - { - /// The domain code, 1. - public const int ErrorCode = 1; - - /// Creates the error with , or the - /// default message "coordinate out of bounds" when it's null. - public OutOfBounds(string? message = null) : base(ErrorCode, message ?? "coordinate out of bounds") - { - } - } - - /// The exception for a raw code and its serialized payload. - internal static new Exception FromError(int code, string message, byte[]? payload) - { - var given = message.Length == 0 ? null : message; - switch (code) - { - case OutOfBounds.ErrorCode: - return new OutOfBounds(given); - default: - return global::NestedModules.NativeException.FromError(code, message, payload); - } - } -} - -/// A measurement unit shared with nested submodules. -public enum Unit -{ - pixels = 0, - points = 1, -} - -/// Top-level struct that references a struct defined one level deeper. -public sealed class Canvas -{ - public int Width { get; } - - /// Reference down into a nested module - public Shape Root { get; } - - /// Creates a from every field. - public Canvas(int width, Shape root) - { - Width = width; - Root = root; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteI32(Width); - Root.WriteTo(writer); - } - - internal static Canvas ReadFrom(FfiBufferReader reader) - { - return new Canvas( - reader.ReadI32(), - global::NestedModules.Shape.ReadFrom(reader)); - } -} - -/// A struct one level deep that references both an ancestor enum and a deeper struct. -public sealed class Shape -{ - public int Sides { get; } - - /// Reference up to the parent module's enum - public Unit Unit { get; } - - /// Reference down into a deeper module - public Detail Detail { get; } - - /// Creates a from every field. - public Shape(int sides, Unit unit, Detail detail) - { - Sides = sides; - Unit = unit; - Detail = detail; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteI32(Sides); - writer.WriteI32((int)Unit); - Detail.WriteTo(writer); - } - - internal static Shape ReadFrom(FfiBufferReader reader) - { - return new Shape( - reader.ReadI32(), - (global::NestedModules.Unit)reader.ReadI32(), - global::NestedModules.Detail.ReadFrom(reader)); - } -} - -/// A struct two levels deep. -public sealed class Detail -{ - public double Area { get; } - - /// Creates a from every field. - public Detail(double area) - { - Area = area; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteF64(Area); - } - - internal static Detail ReadFrom(FfiBufferReader reader) - { - return new Detail( - reader.ReadF64()); - } -} - -public static unsafe class Graphics -{ - /// Construct a Canvas. - public static Canvas NewCanvas(int width) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.nested_modules_graphics_new_canvas(width, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::NestedModules.Canvas.ReadFrom(r)); - } -} - -public static unsafe class GraphicsShapes -{ - /// Construct a Shape. - public static Shape MakeShape(int sides) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.nested_modules_graphics_shapes_make_shape(sides, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::NestedModules.Shape.ReadFrom(r)); - } - - /// Nested function returning a struct owned by an ancestor module. - public static Canvas Describe() - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.nested_modules_graphics_shapes_describe(&ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::NestedModules.Canvas.ReadFrom(r)); - } -} - -public static unsafe class GraphicsShapesGeometry -{ - /// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - /// The call reported a GraphicsError code. - public static Detail Compute(Shape shape) - { - var shapeWriter = new FfiBufferWriter(); - shape.WriteTo(shapeWriter); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* shapePtr = shapeWriter.Written) - { - var ffiResult = NativeMethods.nested_modules_graphics_shapes_geometry_compute(shapePtr, (nuint)shapeWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.GraphicsException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::NestedModules.Detail.ReadFrom(r)); - } - } -} - -/// The value-buffer codec of every composite type the API -/// carries. -internal static class FfiCodecs -{ - /// Reads one value that must fill the whole buffer. - internal static T Decode(FfiBufferReader reader, Func read) - { - var value = read(reader); - reader.ExpectEnd(); - return value; - } -} - -internal static unsafe partial class NativeMethods -{ - static partial void VerifyContracts() - { - VerifyContract("nested_modules_graphics_contract", &nested_modules_graphics_contract, new (ulong, ulong, string)[] - { - (0x2c29eef9327a6bd4UL, 0x12983d5271651389UL, "graphics.Unit"), - (0x304088bbc28f0015UL, 0x97e6e809913be6b6UL, "graphics.new_canvas"), - (0x49f8837532df9ac6UL, 0x32e5b27e81ee0957UL, "graphics.Canvas"), - (0x55717fa027336b47UL, 0xb2bd3a6bdcce48c8UL, "graphics.shapes.describe"), - (0x7626b91b8f1f53cdUL, 0x867bbf7cf0f41c74UL, "graphics.shapes.geometry.compute"), - (0xb3efcb50d9a61ab7UL, 0x3cb0207dfad2a149UL, "graphics.GraphicsError"), - (0xc62b16a9591b744bUL, 0x7895610c8487eb1bUL, "graphics.shapes.Shape"), - (0xe13001f4983ccd35UL, 0x16e7de070b4a4cfcUL, "graphics.shapes.geometry.Detail"), - (0xf3f658bcfc5dba62UL, 0x2665ccb47356e746UL, "graphics.shapes.make_shape"), - }); - } - - [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_contract")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial FfiContractEntry* nested_modules_graphics_contract(nuint* out_len); - - [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_new_canvas")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* nested_modules_graphics_new_canvas(int width, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_shapes_make_shape")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* nested_modules_graphics_shapes_make_shape(int sides, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_shapes_describe")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* nested_modules_graphics_shapes_describe(nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_shapes_geometry_compute")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* nested_modules_graphics_shapes_geometry_compute(byte* shape_ptr, nuint shape_len, nuint* out_len, FfiError* out_err); -} - -// END NestedModules.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_cs.snap deleted file mode 100644 index 9dfb635f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_cs.snap +++ /dev/null @@ -1,348 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Shapes; - -/// An algebraic shape (sum type with associated data) -public abstract class Shape -{ - private Shape() - { - } - - /// The empty shape (a unit variant) - public sealed class Empty : Shape - { - } - - /// A circle with a radius - public sealed class Circle : Shape - { - /// Radius in points - public double Radius { get; } - - /// Creates a from every field. - public Circle(double radius) - { - Radius = radius; - } - } - - /// An axis-aligned rectangle - public sealed class Rectangle : Shape - { - public float Width { get; } - - public float Height { get; } - - /// Creates a from every field. - public Rectangle(float width, float height) - { - Width = width; - Height = height; - } - } - - /// An integer-bounded shape (exercises narrow + wide ints) - public sealed class Bounded : Shape - { - public short Min { get; } - - public short Max { get; } - - public ulong Weight { get; } - - /// Creates a from every field. - public Bounded(short min, short max, ulong weight) - { - Min = min; - Max = max; - Weight = weight; - } - } - - /// A labeled shape with a small count - public sealed class Labeled : Shape - { - public string Label { get; } - - public byte Count { get; } - - /// Creates a from every field. - public Labeled(string label, byte count) - { - Label = label; - Count = count; - } - } - - internal void WriteTo(FfiBufferWriter writer) - { - switch (this) - { - case Empty _: - writer.WriteI32(0); - break; - case Circle v: - writer.WriteI32(1); - writer.WriteF64(v.Radius); - break; - case Rectangle v: - writer.WriteI32(2); - writer.WriteF32(v.Width); - writer.WriteF32(v.Height); - break; - case Bounded v: - writer.WriteI32(3); - writer.WriteI16(v.Min); - writer.WriteI16(v.Max); - writer.WriteU64(v.Weight); - break; - case Labeled v: - writer.WriteI32(4); - writer.WriteString(v.Label); - writer.WriteU8(v.Count); - break; - default: - throw new InvalidOperationException("unknown Shape variant"); - } - } - - internal static Shape ReadFrom(FfiBufferReader reader) - { - var tag = reader.ReadI32(); - switch (tag) - { - case 0: - { - return new Empty(); - } - case 1: - { - return new Circle( - reader.ReadF64()); - } - case 2: - { - return new Rectangle( - reader.ReadF32(), - reader.ReadF32()); - } - case 3: - { - return new Bounded( - reader.ReadI16(), - reader.ReadI16(), - reader.ReadU64()); - } - case 4: - { - return new Labeled( - reader.ReadString(), - reader.ReadU8()); - } - default: - throw FfiBufferReader.Malformed("unknown Shape tag " + tag); - } - } -} - -/// A plain C-style enum (no payloads) -public enum Channel -{ - Red = 0, - Green = 1, - Blue = 2, -} - -/// One field per supported numeric primitive -public sealed class Numerics -{ - public sbyte A { get; } - - public short B { get; } - - public int C { get; } - - public long D { get; } - - public byte E { get; } - - public ushort F { get; } - - public uint G { get; } - - public ulong H { get; } - - public float I { get; } - - public double J { get; } - - /// Creates a from every field. - public Numerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j) - { - A = a; - B = b; - C = c; - D = d; - E = e; - F = f; - G = g; - H = h; - I = i; - J = j; - } - - internal void WriteTo(FfiBufferWriter writer) - { - writer.WriteI8(A); - writer.WriteI16(B); - writer.WriteI32(C); - writer.WriteI64(D); - writer.WriteU8(E); - writer.WriteU16(F); - writer.WriteU32(G); - writer.WriteU64(H); - writer.WriteF32(I); - writer.WriteF64(J); - } - - internal static Numerics ReadFrom(FfiBufferReader reader) - { - return new Numerics( - reader.ReadI8(), - reader.ReadI16(), - reader.ReadI32(), - reader.ReadI64(), - reader.ReadU8(), - reader.ReadU16(), - reader.ReadU32(), - reader.ReadU64(), - reader.ReadF32(), - reader.ReadF64()); - } -} - -/// Rich (algebraic) enums and the expanded numeric primitives. -public static unsafe class Shapes -{ - /// Render a shape to a string - public static string Describe(Shape shape) - { - var shapeWriter = new FfiBufferWriter(); - shape.WriteTo(shapeWriter); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* shapePtr = shapeWriter.Written) - { - var ffiResult = NativeMethods.shapes_shapes_describe(shapePtr, (nuint)shapeWriter.Length, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeBugException.FromError); - return Ffi.TakeString(ffiResult, ffiOutLen); - } - } - - /// Scale a shape by a factor, returning a new shape - public static Shape Scale(Shape shape, double factor) - { - var shapeWriter = new FfiBufferWriter(); - shape.WriteTo(shapeWriter); - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - fixed (byte* shapePtr = shapeWriter.Written) - { - var ffiResult = NativeMethods.shapes_shapes_scale(shapePtr, (nuint)shapeWriter.Length, factor, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::Shapes.Shape.ReadFrom(r)); - } - } - - /// Sum a list of bytes into a wide integer (numerics smoke) - public static ulong SumBytes(ReadOnlySpan values) - { - var ffiErr = default(FfiError); - fixed (byte* valuesPtr = values) - { - var ffiResult = NativeMethods.shapes_shapes_sum_bytes(valuesPtr, (nuint)values.Length, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeBugException.FromError); - return ffiResult; - } - } - - /// Sum a list of bytes into a wide integer (numerics smoke) - public static ulong SumBytes(byte[] values) => SumBytes((ReadOnlySpan)values); - - /// Build a Numerics struct from every numeric primitive - public static Numerics MakeNumerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j) - { - var ffiErr = default(FfiError); - nuint ffiOutLen = 0; - var ffiResult = NativeMethods.shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &ffiOutLen, &ffiErr); - if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeBugException.FromError); - return FfiCodecs.Decode(Ffi.TakeBuffer(ffiResult, ffiOutLen), static r => global::Shapes.Numerics.ReadFrom(r)); - } -} - -/// The value-buffer codec of every composite type the API -/// carries. -internal static class FfiCodecs -{ - /// Reads one value that must fill the whole buffer. - internal static T Decode(FfiBufferReader reader, Func read) - { - var value = read(reader); - reader.ExpectEnd(); - return value; - } -} - -internal static unsafe partial class NativeMethods -{ - static partial void VerifyContracts() - { - VerifyContract("shapes_shapes_contract", &shapes_shapes_contract, new (ulong, ulong, string)[] - { - (0x0d7b3eec49243e7aUL, 0x5f232311808e1b1fUL, "shapes.Channel"), - (0x183fa8198100860eUL, 0xe40c6993cb31d359UL, "shapes.describe"), - (0x2d1395eb837b848eUL, 0x42202a0c12f3a16eUL, "shapes.sum_bytes"), - (0x7f9a5b87ba09b86bUL, 0xc5b32fb0efbbaa17UL, "shapes.scale"), - (0xc8edd779240fda17UL, 0x68e1be78fddb20e4UL, "shapes.Numerics"), - (0xd3d3881b2fd18a62UL, 0xf24d0eb76a99d918UL, "shapes.make_numerics"), - (0xf010844afff5ac3cUL, 0x50de28ae9b41ff15UL, "shapes.Shape"), - }); - } - - [LibraryImport(LibName, EntryPoint = "shapes_shapes_contract")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial FfiContractEntry* shapes_shapes_contract(nuint* out_len); - - [LibraryImport(LibName, EntryPoint = "shapes_shapes_describe")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* shapes_shapes_describe(byte* shape_ptr, nuint shape_len, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "shapes_shapes_scale")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* shapes_shapes_scale(byte* shape_ptr, nuint shape_len, double factor, nuint* out_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "shapes_shapes_sum_bytes")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial ulong shapes_shapes_sum_bytes(byte* values_ptr, nuint values_len, FfiError* out_err); - - [LibraryImport(LibName, EntryPoint = "shapes_shapes_make_numerics")] - [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] - internal static partial byte* shapes_shapes_make_numerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j, nuint* out_len, FfiError* out_err); -} - -// END Shapes.cs diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__bindings_go.snap deleted file mode 100644 index fa245077..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__bindings_go.snap +++ /dev/null @@ -1,415 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// Package docs_everywhere binds the docs_everywhere native library. -package docs_everywhere - -/* -#cgo LDFLAGS: -ldocs_everywhere -#include "docs_everywhere.h" -extern void goWv_docs_everywhere_docs_SavedListener_on_saved(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); -extern void goWv_docs_everywhere_docs_SavedListener_free(void* ctx); -static const docs_everywhere_docs_SavedListener_vtable wvVtable_docs_everywhere_docs_SavedListener_vtable = { - .size = sizeof(docs_everywhere_docs_SavedListener_vtable), - .flags = 0, - .free = goWv_docs_everywhere_docs_SavedListener_free, - .on_saved = goWv_docs_everywhere_docs_SavedListener_on_saved, -}; -__attribute__((unused)) static const docs_everywhere_docs_SavedListener_vtable* wvVtablePtr_docs_everywhere_docs_SavedListener_vtable(void) { return &wvVtable_docs_everywhere_docs_SavedListener_vtable; } -extern void goWv_docs_everywhere_docs_publish_async_callback(void* context, docs_everywhere_error* err, bool result); -*/ -import "C" - -import ( - "context" - "runtime" - "unsafe" -) - -func init() { - wvCheckABI() - var n C.size_t - table := C.docs_everywhere_docs_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0x00b2187fd75634ec, 0x2263c6f787bf6081, "docs.publish_async"}, - {0x12fc242b9dc6444c, 0x96405f484a642b31, "docs.Notebook.insert"}, - {0x2213822ed05cd5bd, 0x26c5833392aa63ce, "docs.Notebook.new"}, - {0x2bd762bcdaa207ff, 0x1b0ebab2d04c49b0, "docs.SavedListener"}, - {0x2e086c45e9d617de, 0x4d43bf32358c01ff, "docs.Notebook.find"}, - {0x4817dc98197ad960, 0xf8017b5a7d58b2c7, "docs.Notebook.size"}, - {0x614de47452d5a55c, 0xd9aae1472494ffef, "docs.get_document"}, - {0x67c5cb9fc119bcd5, 0x08d5b25a1a22a7fa, "docs.Document"}, - {0x8d484bf2d78c9b70, 0xe9093cf96912194c, "docs.Notebook.max_documents"}, - {0xaea61d6451691c67, 0xe4704128b3524b3d, "docs.watch"}, - {0xd68d6ad1540cdec5, 0x01afeb40b0a8bfe1, "docs.Notebook"}, - {0xdb0601444a4760cc, 0xe56c69629b444797, "docs.create_document"}, - {0xe12d6f4838347b3e, 0xa9bab86646d84f51, "docs.Status"}, - {0xee2f8d25b3313b50, 0x9691232f35439a2b, "docs.DocsErrors"}, - }) -} - -// DocsErrorsError is an error the docs module reports: *NotFoundError, or -// *InvalidStatusError. -// -// Match one code with errors.As and its type, or any of them with a -// DocsErrorsError. -type DocsErrorsError interface { - error - // Code returns the error's numeric code. - Code() int32 - isDocsErrorsError() -} - -// NotFoundError: Returned when no document matches the requested id -type NotFoundError struct { - // Message describes the failure; when it's empty, Error returns the - // code's default message, "Document not found". - Message string -} - -// Error returns the error's message. -func (e *NotFoundError) Error() string { - if e.Message == "" { - return "Document not found" - } - return e.Message -} - -// Code returns 1. -func (*NotFoundError) Code() int32 { - return 1 -} - -func (*NotFoundError) isDocsErrorsError() {} - -func (*NotFoundError) wvPayload() []byte { - return nil -} - -// InvalidStatusError: Raised when attempting to move into a forbidden state -type InvalidStatusError struct { - // Message describes the failure; when it's empty, Error returns the - // code's default message, "Invalid status transition". - Message string -} - -// Error returns the error's message. -func (e *InvalidStatusError) Error() string { - if e.Message == "" { - return "Invalid status transition" - } - return e.Message -} - -// Code returns 2. -func (*InvalidStatusError) Code() int32 { - return 2 -} - -func (*InvalidStatusError) isDocsErrorsError() {} - -func (*InvalidStatusError) wvPayload() []byte { - return nil -} - -func wvDocsErrorsError(f wvFailure) error { - switch f.code { - case 1: - return &NotFoundError{Message: f.message} - case 2: - return &InvalidStatusError{Message: f.message} - } - return f.err() -} - -// Status: Lifecycle status of a document. -// -// Used to drive UI badges and filters. -type Status int32 - -const ( - // StatusDraft: Document is being edited - StatusDraft Status = 0 - // StatusPublished: Document is publicly visible - StatusPublished Status = 1 - // StatusArchived: Document is hidden from listings - StatusArchived Status = 2 -) - -func wvWriteStatus(w *wvWriter, v Status) { - w.writeI32(int32(v)) -} - -func wvReadStatus(r *wvReader) Status { - return Status(r.readI32()) -} - -// Document represents a single document tracked by the system. -// -// Documents are persisted to disk and exposed via the public API. -type Document struct { - // ID: Stable opaque identifier - ID int64 - // Title: Human-readable title shown in lists - Title string - // Status: Current lifecycle status - Status Status - // Tags: Free-form tags for grouping - Tags []string -} - -func wvWriteDocument(w *wvWriter, v Document) { - w.writeI64(v.ID) - w.writeString(v.Title) - wvWriteStatus(w, v.Status) - wvWriteListString(w, v.Tags) -} - -func wvReadDocument(r *wvReader) Document { - var v Document - v.ID = r.readI64() - v.Title = r.readString() - v.Status = wvReadStatus(r) - v.Tags = wvReadListString(r) - return v -} - -// SavedListener: Consumer-implemented observer of document-saved notifications. -// -// Implementations must be safe to call from any thread. -// -// Implement SavedListener in Go and pass the value to the functions that take -// it. The native library may call its methods from any thread until it -// releases the implementation. A method that panics, or a throwing method that -// returns an error outside its domain, fails the native call in progress as a -// callback failure (code -4). -type SavedListener interface { - // OnSaved fires after a document has been persisted successfully - // - // Parameters: - // - // - docID: Identifier of the saved document - // - revision: Monotonically increasing revision - OnSaved(docID int64, revision int32) -} - -//export goWv_docs_everywhere_docs_SavedListener_on_saved -func goWv_docs_everywhere_docs_SavedListener_on_saved(ctx unsafe.Pointer, doc_id C.int64_t, revision C.int32_t, out_err *C.docs_everywhere_error) { - defer wvRecoverCallback(out_err) - wvCallback[SavedListener](ctx).OnSaved(int64(doc_id), int32(revision)) -} - -//export goWv_docs_everywhere_docs_SavedListener_free -func goWv_docs_everywhere_docs_SavedListener_free(ctx unsafe.Pointer) { - wvFreeCallback(ctx) -} - -// Notebook is an ordered collection of documents. -// -// Notebooks own their documents and release them when destroyed. -// -// Each wrapper holds one strong reference; Close releases it, and a finalizer -// releases it if the wrapper is garbage collected first. -type Notebook struct { - ref wvRef -} - -func wvDestroyNotebook(ptr unsafe.Pointer) { - C.docs_everywhere_docs_Notebook_destroy((*C.docs_everywhere_docs_Notebook)(ptr)) -} - -// wvAdoptNotebook wraps one owned strong reference. A null pointer adopts to nil. -func wvAdoptNotebook(ptr *C.docs_everywhere_docs_Notebook) *Notebook { - if ptr == nil { - return nil - } - s := &Notebook{} - s.ref.init(unsafe.Pointer(ptr), wvDestroyNotebook) - runtime.SetFinalizer(s, (*Notebook).Close) - return s -} - -// native borrows the wrapper's pointer for one call; pair it with a -// deferred s.ref.release(). -func (s *Notebook) native() *C.docs_everywhere_docs_Notebook { - if s == nil { - panic("docs_everywhere: nil *Notebook") - } - return (*C.docs_everywhere_docs_Notebook)(s.ref.acquire("Notebook")) -} - -// share returns a new strong reference to the wrapper's object for the -// native library to adopt (the wrapper keeps its own), or null for nil. -func (s *Notebook) share() *C.docs_everywhere_docs_Notebook { - if s == nil { - return nil - } - ptr := s.native() - defer s.ref.release() - return C.docs_everywhere_docs_Notebook_clone(ptr) -} - -// Close releases the wrapper's strong reference and always returns nil. -// It's idempotent and safe to call from any goroutine, even while a call -// on the wrapper is in flight: the reference is then released when that -// call returns. The object itself is dropped once its last reference, from -// any wrapper, record, or the native library, is gone. -func (s *Notebook) Close() error { - runtime.SetFinalizer(s, nil) - s.ref.close() - return nil -} - -// NewNotebook: Create an empty notebook -func NewNotebook() *Notebook { - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_Notebook_new(&cErr) - wvTrap(&cErr) - return wvAdoptNotebook(cRet) -} - -// Insert: Add a document, returning its position in the notebook -// -// Parameters: -// -// - document: The document to add -func (s *Notebook) Insert(document Document) int32 { - cSelf := s.native() - defer s.ref.release() - cDocumentPtr, cDocumentLen := wvBytes(wvEncode(document, wvWriteDocument)) - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_Notebook_insert(cSelf, cDocumentPtr, cDocumentLen, &cErr) - wvTrap(&cErr) - return int32(cRet) -} - -// Find: Look up a document by identifier, failing when none matches -// -// Parameters: -// -// - id: Identifier of the document to find -func (s *Notebook) Find(id int64) (Document, error) { - cSelf := s.native() - defer s.ref.release() - var cRetLen C.size_t - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_Notebook_find(cSelf, C.int64_t(id), &cRetLen, &cErr) - if cErr.code != 0 { - return Document{}, wvDocsErrorsError(wvTakeError(&cErr)) - } - return wvDecode(cRet, cRetLen, wvReadDocument), nil -} - -// Size returns the number of documents currently held -func (s *Notebook) Size() int32 { - cSelf := s.native() - defer s.ref.release() - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_Notebook_size(cSelf, &cErr) - wvTrap(&cErr) - return int32(cRet) -} - -// NotebookMaxDocuments returns the largest number of documents one notebook will hold -func NotebookMaxDocuments() int32 { - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_Notebook_max_documents(&cErr) - wvTrap(&cErr) - return int32(cRet) -} - -// CreateDocument: Create a brand new document. -// -// The document starts in `Draft` status; callers must publish -// it explicitly to make it visible. -// -// Parameters: -// -// - title: Human-readable title for the new document -// - tags: Initial set of tags (may be empty) -func CreateDocument(title string, tags []string) Document { - cTitlePtr, cTitleLen := wvStr(title) - cTagsPtr, cTagsLen := wvBytes(wvEncode(tags, wvWriteListString)) - var cRetLen C.size_t - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_create_document(cTitlePtr, cTitleLen, cTagsPtr, cTagsLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadDocument) -} - -// GetDocument: Look up a document by identifier -// -// Parameters: -// -// - id: Identifier returned from create_document -func GetDocument(id int64) *Document { - var cRetLen C.size_t - var cErr C.docs_everywhere_error - cRet := C.docs_everywhere_docs_get_document(C.int64_t(id), &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadOptDocument) -} - -//export goWv_docs_everywhere_docs_publish_async_callback -func goWv_docs_everywhere_docs_publish_async_callback(context_ unsafe.Pointer, err_ *C.docs_everywhere_error, result C._Bool) { - wvComplete(context_, err_, func() bool { - return bool(result) - }) -} - -// PublishAsync: Publish a document in the background -// -// Parameters: -// -// - id: Identifier of the document to publish -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func PublishAsync(ctx context.Context, id int64) (bool, error) { - if err := ctx.Err(); err != nil { - return false, err - } - call := wvNewAsyncCall[bool]() - C.docs_everywhere_docs_publish_async(C.int64_t(id), C.docs_everywhere_docs_publish_async_callback(unsafe.Pointer(C.goWv_docs_everywhere_docs_publish_async_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return false, err - } - if fail != nil { - panic(fail.err()) - } - return res, nil -} - -// Watch: Register a listener for saved notifications -// -// Parameters: -// -// - listener: Receiver of saved notifications -func Watch(listener SavedListener) { - var cErr C.docs_everywhere_error - C.docs_everywhere_docs_watch(wvNewCallback(listener), C.wvVtablePtr_docs_everywhere_docs_SavedListener_vtable(), &cErr) - wvTrap(&cErr) -} - -func wvWriteListString(w *wvWriter, v []string) { - wvWriteList(w, v, (*wvWriter).writeString) -} - -func wvReadListString(r *wvReader) []string { - return wvReadList(r, (*wvReader).readString) -} - -func wvWriteOptDocument(w *wvWriter, v *Document) { - wvWriteOptional(w, v, wvWriteDocument) -} - -func wvReadOptDocument(r *wvReader) *Document { - return wvReadOptional(r, wvReadDocument) -} - -// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__bindings_go.snap deleted file mode 100644 index 9ec90eb1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__bindings_go.snap +++ /dev/null @@ -1,1458 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// Package edge_cases binds the edge_cases native library. -package edge_cases - -/* -#cgo LDFLAGS: -ledge_cases -#cgo CFLAGS: -Wno-deprecated-declarations -#include "edge_cases.h" -__attribute__((unused)) static void* wvHandlePtr(uintptr_t h) { return (void*)h; } -extern void goWv_edge_cases_edge_cases_BatchListener_on_batch(void* ctx, uint8_t* items_ptr, size_t items_len, uint8_t* raw_ptr, size_t raw_len, uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); -extern void goWv_edge_cases_edge_cases_BatchListener_on_widget(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); -extern void goWv_edge_cases_edge_cases_BatchListener_on_tick(void* ctx, edge_cases_error* out_err); -extern bool goWv_edge_cases_edge_cases_BatchListener_should_continue(void* ctx, int32_t class_, edge_cases_error* out_err); -extern edge_cases_edge_cases_Mode goWv_edge_cases_edge_cases_BatchListener_pick_mode(void* ctx, edge_cases_error* out_err); -extern void goWv_edge_cases_edge_cases_BatchListener_free(void* ctx); -static const edge_cases_edge_cases_BatchListener_vtable wvVtable_edge_cases_edge_cases_BatchListener_vtable = { - .size = sizeof(edge_cases_edge_cases_BatchListener_vtable), - .flags = 0, - .free = goWv_edge_cases_edge_cases_BatchListener_free, - .on_batch = (void (*)(void*, const uint8_t*, size_t, const uint8_t*, size_t, const uint8_t*, size_t, edge_cases_edge_cases_Mode, edge_cases_error*))goWv_edge_cases_edge_cases_BatchListener_on_batch, - .on_widget = (void (*)(void*, edge_cases_edge_cases_Widget*, edge_cases_edge_cases_Widget*, const uint8_t*, size_t, edge_cases_error*))goWv_edge_cases_edge_cases_BatchListener_on_widget, - .on_tick = goWv_edge_cases_edge_cases_BatchListener_on_tick, - .should_continue = goWv_edge_cases_edge_cases_BatchListener_should_continue, - .pick_mode = goWv_edge_cases_edge_cases_BatchListener_pick_mode, -}; -__attribute__((unused)) static const edge_cases_edge_cases_BatchListener_vtable* wvVtablePtr_edge_cases_edge_cases_BatchListener_vtable(void) { return &wvVtable_edge_cases_edge_cases_BatchListener_vtable; } -extern void goWv_edge_cases_edge_cases_Keyword_delete(void* ctx, int32_t self, edge_cases_error* out_err); -extern int32_t goWv_edge_cases_edge_cases_Keyword_import(void* ctx, edge_cases_error* out_err); -extern void goWv_edge_cases_edge_cases_Keyword_free(void* ctx); -static const edge_cases_edge_cases_Keyword_vtable wvVtable_edge_cases_edge_cases_Keyword_vtable = { - .size = sizeof(edge_cases_edge_cases_Keyword_vtable), - .flags = 0, - .free = goWv_edge_cases_edge_cases_Keyword_free, - .delete_ = goWv_edge_cases_edge_cases_Keyword_delete, - .import = goWv_edge_cases_edge_cases_Keyword_import, -}; -__attribute__((unused)) static const edge_cases_edge_cases_Keyword_vtable* wvVtablePtr_edge_cases_edge_cases_Keyword_vtable(void) { return &wvVtable_edge_cases_edge_cases_Keyword_vtable; } -extern void goWv_edge_cases_edge_cases_async_widget_callback(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); -extern void goWv_edge_cases_edge_cases_async_void_callback(void* context, edge_cases_error* err); -extern void goWv_edge_cases_edge_cases_async_bytes_callback(void* context, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_edge_cases_edge_cases_async_struct_callback(void* context, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_edge_cases_edge_cases_async_list_callback(void* context, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_edge_cases_edge_cases_async_optional_callback(void* context, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_edge_cases_edge_cases_async_enum_callback(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); -extern void goWv_edge_cases_edge_cases_async_keywords_callback(void* context, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_edge_cases_edge_cases_Widget_fetch_callback(void* context, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); -*/ -import "C" - -import ( - "context" - "iter" - "runtime" - "unsafe" -) - -func init() { - wvCheckABI() - var n C.size_t - table := C.edge_cases_edge_cases_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0x056a86511785ba2b, 0x5cba3f16d57c6cdb, "edge_cases.make_widget"}, - {0x07bfa9ecb2b720d2, 0x3813b3f57c64bf35, "edge_cases.EdgeErrors"}, - {0x0aaa11867e37f991, 0xf090dd52146ebdf0, "edge_cases.Widget.refresh"}, - {0x0f9701493898ae4c, 0x6944f47a275c96e8, "edge_cases.Inner"}, - {0x1226b1adac179f7d, 0x0e95a00ab0bef637, "edge_cases.async_keywords"}, - {0x16d36bef1541136c, 0x5ea7021ec324298f, "edge_cases.find_widget"}, - {0x1b7324c55e980feb, 0xabf87fe372358f42, "edge_cases.Widget.old_name"}, - {0x1cbd731467c2bab8, 0x4915b9989c2c9074, "edge_cases.Vault"}, - {0x2c2a6de67d894b0a, 0x50a4496b1e7191ca, "edge_cases.Vault.open"}, - {0x322b4cfcd2d6861c, 0xca7527a379ae8465, "edge_cases.Keywords"}, - {0x3f8e23a5f32906f4, 0x8d4c908d796f130d, "edge_cases.Leaf"}, - {0x42ab8a685457b97c, 0xe5ce30500cadb32d, "edge_cases.Vault.handle"}, - {0x4a68540e2836d3d0, 0xa20e44c71e420ed2, "edge_cases.mode_stream"}, - {0x4e030580ac0b111d, 0xb68b7a18d910e175, "edge_cases.legacy_value"}, - {0x4f5e4e5291864ef3, 0x55a1a88b21a08f8b, "edge_cases.Legacy"}, - {0x4f84b06171b4213d, 0xbaedcfdebb6a2820, "edge_cases.scalar_stream"}, - {0x5067a5ff2d9ef910, 0xbfc79b18fc4714fc, "edge_cases.Widget"}, - {0x59aff159112fbf5b, 0x18e4def57a8cfa49, "edge_cases.optional_params"}, - {0x5daf65b98fa77c48, 0xc817a8cb202e59c2, "edge_cases.keywords_roundtrip"}, - {0x5de8a5c9726c2c5a, 0x7c50b5b65f7a896d, "edge_cases.register_batch"}, - {0x618a620309994c3c, 0x826ad0b1d208d723, "edge_cases.async_bytes"}, - {0x648285cdba0a7b8e, 0xcf6e80e783a4d8be, "edge_cases.WidgetHolder"}, - {0x649c78079824b10d, 0x5b12dd26e0634b69, "edge_cases.async_widget"}, - {0x7da7a7357d122a37, 0xf544e025ffaa3d20, "edge_cases.async_void"}, - {0x7e4015bc3a472e2d, 0xfe22291d90b82e4a, "edge_cases.widget_stream"}, - {0x805276e5d81fdba7, 0xd9b64e360bd03b2b, "edge_cases.Outer"}, - {0x80e1183326542df4, 0xe8a57e12cc6ed276, "edge_cases.select"}, - {0x923ce5e9bcd769a1, 0x0b973f5dc07dda8b, "edge_cases.Vault.to_string"}, - {0x98ca8c01f5239404, 0xfe11c93eb6b303ef, "edge_cases.Widget.rename"}, - {0xa13e22e98ea278d4, 0xf1fae215d7387800, "edge_cases.widgets_everywhere"}, - {0xa84a8c9bf2a881e1, 0xbe1dc840bf221996, "edge_cases.async_optional"}, - {0xad958c478fbf205d, 0xe386b0716d24f19b, "edge_cases.Retired"}, - {0xb2fc3964ef56ab39, 0x73c325c8bc351236, "edge_cases.async_list"}, - {0xb3403af521cf0006, 0x6abc30d48206a3ba, "edge_cases.Widget.close"}, - {0xb794304a22a406b8, 0xf6fb98711c516105, "edge_cases.async_struct"}, - {0xba3936106a49c47d, 0xb7025f2fadd6288e, "edge_cases.Vault.peek"}, - {0xc155b0c3cfb164f4, 0xc777ae57d92ebc47, "edge_cases.noop"}, - {0xcdc18607777019dd, 0xb81a31e5c20aa7a8, "edge_cases.inspect_widget"}, - {0xd164a82aeb64b8cc, 0xded02c68005c4737, "edge_cases.nested_optional"}, - {0xd978ccad2468038f, 0x30c39c65d6235153, "edge_cases.Mode"}, - {0xdbe9d947431c9124, 0x39537fb848fdf980, "edge_cases.BatchListener"}, - {0xeae8b7862c08bb07, 0xfe12eeaa830452ea, "edge_cases.Keyword"}, - {0xf1e155a05ae262b8, 0xb575eb818a423d42, "edge_cases.async_enum"}, - {0xf22e210bdbce7343, 0x2a5e0f801bc8c768, "edge_cases.Widget.dispose"}, - {0xf3c7cf675563ee6c, 0x0ecffeef399facc8, "edge_cases.Vault.probe"}, - {0xfd34bc67acac379e, 0x576caa14f3c0901a, "edge_cases.Widget.fetch"}, - {0xff2e9d2bff941786, 0x7e96764ef1f5fbaa, "edge_cases.string_stream"}, - }) -} - -// EdgeErrorsError is an error the edge_cases module reports: *BusyError. -// -// Match one code with errors.As and its type, or any of them with a -// EdgeErrorsError. -type EdgeErrorsError interface { - error - // Code returns the error's numeric code. - Code() int32 - isEdgeErrorsError() -} - -// BusyError is the EdgeErrorsError code Busy (7): Resource is busy. -type BusyError struct { - // Message describes the failure; when it's empty, Error returns the - // code's default message, "Resource is busy". - Message string -} - -// Error returns the error's message. -func (e *BusyError) Error() string { - if e.Message == "" { - return "Resource is busy" - } - return e.Message -} - -// Code returns 7. -func (*BusyError) Code() int32 { - return 7 -} - -func (*BusyError) isEdgeErrorsError() {} - -func (*BusyError) wvPayload() []byte { - return nil -} - -func wvEdgeErrorsError(f wvFailure) error { - switch f.code { - case 7: - return &BusyError{Message: f.message} - } - return f.err() -} - -// Legacy: Deprecated enum with non-contiguous values and keyword-like variants -// -// Deprecated: Use Mode instead -type Legacy int32 - -const ( - LegacyNone Legacy = 0 - LegacyDefault Legacy = 5 - LegacyClass Legacy = 10 -) - -type Mode int32 - -const ( - ModeFast Mode = 1 - ModeSafe Mode = 2 -) - -func wvWriteMode(w *wvWriter, v Mode) { - w.writeI32(int32(v)) -} - -func wvReadMode(r *wvReader) Mode { - return Mode(r.readI32()) -} - -type Leaf struct { - Weight float32 -} - -func wvWriteLeaf(w *wvWriter, v Leaf) { - w.writeF32(v.Weight) -} - -func wvReadLeaf(r *wvReader) Leaf { - var v Leaf - v.Weight = r.readF32() - return v -} - -// Inner: Middle layer of a three-deep struct nesting -type Inner struct { - Leaf Leaf - Leaves []Leaf -} - -func wvWriteInner(w *wvWriter, v Inner) { - wvWriteLeaf(w, v.Leaf) - wvWriteListLeaf(w, v.Leaves) -} - -func wvReadInner(r *wvReader) Inner { - var v Inner - v.Leaf = wvReadLeaf(r) - v.Leaves = wvReadListLeaf(r) - return v -} - -type Outer struct { - Inner Inner - MaybeInner *Inner - ByName map[string]Inner - Modes []Mode - Matrix [][]int32 - Sparse []*int64 - Blob []byte - Note *string - ByID map[int64]string - Hidden bool - V2 uint8 -} - -func wvWriteOuter(w *wvWriter, v Outer) { - wvWriteInner(w, v.Inner) - wvWriteOptInner(w, v.MaybeInner) - wvWriteMapStringInner(w, v.ByName) - wvWriteListMode(w, v.Modes) - wvWriteListListI32(w, v.Matrix) - wvWriteListOptI64(w, v.Sparse) - wvWriteOptBytes(w, v.Blob) - wvWriteOptString(w, v.Note) - wvWriteMapI64String(w, v.ByID) - w.writeBool(v.Hidden) - w.writeU8(v.V2) -} - -func wvReadOuter(r *wvReader) Outer { - var v Outer - v.Inner = wvReadInner(r) - v.MaybeInner = wvReadOptInner(r) - v.ByName = wvReadMapStringInner(r) - v.Modes = wvReadListMode(r) - v.Matrix = wvReadListListI32(r) - v.Sparse = wvReadListOptI64(r) - v.Blob = wvReadOptBytes(r) - v.Note = wvReadOptString(r) - v.ByID = wvReadMapI64String(r) - v.Hidden = r.readBool() - v.V2 = r.readU8() - return v -} - -// Keywords: Every field is a reserved word somewhere -type Keywords struct { - Class int32 - Def int32 - Import int32 - From int32 - Func int32 - Range int32 - Register int32 - Volatile int32 - New int32 - Default int32 - End int32 - In int32 - Is int32 - Int int32 - Var int32 - Val int32 - Self int32 - This int32 - Object int32 - String int32 - Case int32 - Select int32 - Switch int32 - Void int32 - Union int32 - Namespace int32 - Operator int32 - Template int32 - Package int32 - Protocol int32 - Guard int32 - Defer int32 - Inout int32 - Internal int32 - When int32 - Fun int32 - Data int32 - Late int32 - Part int32 - Out int32 - Ref int32 - Event int32 - Base int32 - Lambda int32 - Yield int32 - Nil int32 - Null int32 - Begin int32 - Rescue int32 - Module int32 - Chan int32 - Go int32 - Let int32 - Export int32 - Delete int32 - Typedef int32 - Sizeof int32 - Static int32 - Super int32 -} - -func wvWriteKeywords(w *wvWriter, v Keywords) { - w.writeI32(v.Class) - w.writeI32(v.Def) - w.writeI32(v.Import) - w.writeI32(v.From) - w.writeI32(v.Func) - w.writeI32(v.Range) - w.writeI32(v.Register) - w.writeI32(v.Volatile) - w.writeI32(v.New) - w.writeI32(v.Default) - w.writeI32(v.End) - w.writeI32(v.In) - w.writeI32(v.Is) - w.writeI32(v.Int) - w.writeI32(v.Var) - w.writeI32(v.Val) - w.writeI32(v.Self) - w.writeI32(v.This) - w.writeI32(v.Object) - w.writeI32(v.String) - w.writeI32(v.Case) - w.writeI32(v.Select) - w.writeI32(v.Switch) - w.writeI32(v.Void) - w.writeI32(v.Union) - w.writeI32(v.Namespace) - w.writeI32(v.Operator) - w.writeI32(v.Template) - w.writeI32(v.Package) - w.writeI32(v.Protocol) - w.writeI32(v.Guard) - w.writeI32(v.Defer) - w.writeI32(v.Inout) - w.writeI32(v.Internal) - w.writeI32(v.When) - w.writeI32(v.Fun) - w.writeI32(v.Data) - w.writeI32(v.Late) - w.writeI32(v.Part) - w.writeI32(v.Out) - w.writeI32(v.Ref) - w.writeI32(v.Event) - w.writeI32(v.Base) - w.writeI32(v.Lambda) - w.writeI32(v.Yield) - w.writeI32(v.Nil) - w.writeI32(v.Null) - w.writeI32(v.Begin) - w.writeI32(v.Rescue) - w.writeI32(v.Module) - w.writeI32(v.Chan) - w.writeI32(v.Go) - w.writeI32(v.Let) - w.writeI32(v.Export) - w.writeI32(v.Delete) - w.writeI32(v.Typedef) - w.writeI32(v.Sizeof) - w.writeI32(v.Static) - w.writeI32(v.Super) -} - -func wvReadKeywords(r *wvReader) Keywords { - var v Keywords - v.Class = r.readI32() - v.Def = r.readI32() - v.Import = r.readI32() - v.From = r.readI32() - v.Func = r.readI32() - v.Range = r.readI32() - v.Register = r.readI32() - v.Volatile = r.readI32() - v.New = r.readI32() - v.Default = r.readI32() - v.End = r.readI32() - v.In = r.readI32() - v.Is = r.readI32() - v.Int = r.readI32() - v.Var = r.readI32() - v.Val = r.readI32() - v.Self = r.readI32() - v.This = r.readI32() - v.Object = r.readI32() - v.String = r.readI32() - v.Case = r.readI32() - v.Select = r.readI32() - v.Switch = r.readI32() - v.Void = r.readI32() - v.Union = r.readI32() - v.Namespace = r.readI32() - v.Operator = r.readI32() - v.Template = r.readI32() - v.Package = r.readI32() - v.Protocol = r.readI32() - v.Guard = r.readI32() - v.Defer = r.readI32() - v.Inout = r.readI32() - v.Internal = r.readI32() - v.When = r.readI32() - v.Fun = r.readI32() - v.Data = r.readI32() - v.Late = r.readI32() - v.Part = r.readI32() - v.Out = r.readI32() - v.Ref = r.readI32() - v.Event = r.readI32() - v.Base = r.readI32() - v.Lambda = r.readI32() - v.Yield = r.readI32() - v.Nil = r.readI32() - v.Null = r.readI32() - v.Begin = r.readI32() - v.Rescue = r.readI32() - v.Module = r.readI32() - v.Chan = r.readI32() - v.Go = r.readI32() - v.Let = r.readI32() - v.Export = r.readI32() - v.Delete = r.readI32() - v.Typedef = r.readI32() - v.Sizeof = r.readI32() - v.Static = r.readI32() - v.Super = r.readI32() - return v -} - -// Retired: Deprecated struct -// -// Deprecated: Use Outer instead -type Retired struct { - ID int64 -} - -func wvWriteRetired(w *wvWriter, v Retired) { - w.writeI64(v.ID) -} - -func wvReadRetired(r *wvReader) Retired { - var v Retired - v.ID = r.readI64() - return v -} - -// WidgetHolder: Record carrying objects in every buffered position -type WidgetHolder struct { - Primary *Widget - Fallback *Widget - Extras []*Widget - ByName map[string]*Widget -} - -func wvWriteWidgetHolder(w *wvWriter, v WidgetHolder) { - wvWriteWidget(w, v.Primary) - wvWriteOptWidget(w, v.Fallback) - wvWriteListWidget(w, v.Extras) - wvWriteMapStringWidget(w, v.ByName) -} - -func wvReadWidgetHolder(r *wvReader) WidgetHolder { - var v WidgetHolder - v.Primary = wvReadWidget(r) - v.Fallback = wvReadOptWidget(r) - v.Extras = wvReadListWidget(r) - v.ByName = wvReadMapStringWidget(r) - return v -} - -// BatchListener: Callback interface carrying composite payloads and objects -// -// Implement BatchListener in Go and pass the value to the functions that take -// it. The native library may call its methods from any thread until it -// releases the implementation. A method that panics, or a throwing method that -// returns an error outside its domain, fails the native call in progress as a -// callback failure (code -4). -type BatchListener interface { - // OnBatch: Composite payloads cross as value buffers - OnBatch(items []Outer, raw []byte, first *Outer, mode Mode) - // OnWidget: Objects cross as strong references the consumer must release - OnWidget(widget *Widget, fallback *Widget, children []*Widget) - OnTick() - // ShouldContinue: Scalar return travels back through the vtable - ShouldContinue(class int32) bool - // PickMode: Enum return travels back as its integer discriminant - PickMode() Mode -} - -//export goWv_edge_cases_edge_cases_BatchListener_on_batch -func goWv_edge_cases_edge_cases_BatchListener_on_batch(ctx unsafe.Pointer, items_ptr *C.uint8_t, items_len C.size_t, raw_ptr *C.uint8_t, raw_len C.size_t, first_ptr *C.uint8_t, first_len C.size_t, mode C.edge_cases_edge_cases_Mode, out_err *C.edge_cases_error) { - defer wvRecoverCallback(out_err) - wvCallback[BatchListener](ctx).OnBatch(wvDecodeBorrowed(items_ptr, items_len, wvReadListOuter), wvBorrowBytes(raw_ptr, raw_len), wvDecodeBorrowed(first_ptr, first_len, wvReadOptOuter), Mode(mode)) -} - -//export goWv_edge_cases_edge_cases_BatchListener_on_widget -func goWv_edge_cases_edge_cases_BatchListener_on_widget(ctx unsafe.Pointer, widget *C.edge_cases_edge_cases_Widget, fallback *C.edge_cases_edge_cases_Widget, children_ptr *C.uint8_t, children_len C.size_t, out_err *C.edge_cases_error) { - defer wvRecoverCallback(out_err) - wvCallback[BatchListener](ctx).OnWidget(wvAdoptWidget(widget), wvAdoptWidget(fallback), wvDecodeBorrowed(children_ptr, children_len, wvReadListWidget)) -} - -//export goWv_edge_cases_edge_cases_BatchListener_on_tick -func goWv_edge_cases_edge_cases_BatchListener_on_tick(ctx unsafe.Pointer, out_err *C.edge_cases_error) { - defer wvRecoverCallback(out_err) - wvCallback[BatchListener](ctx).OnTick() -} - -//export goWv_edge_cases_edge_cases_BatchListener_should_continue -func goWv_edge_cases_edge_cases_BatchListener_should_continue(ctx unsafe.Pointer, class C.int32_t, out_err *C.edge_cases_error) (cRet C._Bool) { - defer wvRecoverCallback(out_err) - ret := wvCallback[BatchListener](ctx).ShouldContinue(int32(class)) - return C._Bool(ret) -} - -//export goWv_edge_cases_edge_cases_BatchListener_pick_mode -func goWv_edge_cases_edge_cases_BatchListener_pick_mode(ctx unsafe.Pointer, out_err *C.edge_cases_error) (cRet C.edge_cases_edge_cases_Mode) { - defer wvRecoverCallback(out_err) - ret := wvCallback[BatchListener](ctx).PickMode() - return C.edge_cases_edge_cases_Mode(ret) -} - -//export goWv_edge_cases_edge_cases_BatchListener_free -func goWv_edge_cases_edge_cases_BatchListener_free(ctx unsafe.Pointer) { - wvFreeCallback(ctx) -} - -// Keyword: Callback interface whose name and methods are keywords -// -// Implement Keyword in Go and pass the value to the functions that take it. -// The native library may call its methods from any thread until it releases -// the implementation. A method that panics, or a throwing method that returns -// an error outside its domain, fails the native call in progress as a callback -// failure (code -4). -type Keyword interface { - Delete(self int32) - Import() int32 -} - -//export goWv_edge_cases_edge_cases_Keyword_delete -func goWv_edge_cases_edge_cases_Keyword_delete(ctx unsafe.Pointer, self C.int32_t, out_err *C.edge_cases_error) { - defer wvRecoverCallback(out_err) - wvCallback[Keyword](ctx).Delete(int32(self)) -} - -//export goWv_edge_cases_edge_cases_Keyword_import -func goWv_edge_cases_edge_cases_Keyword_import(ctx unsafe.Pointer, out_err *C.edge_cases_error) (cRet C.int32_t) { - defer wvRecoverCallback(out_err) - ret := wvCallback[Keyword](ctx).Import() - return C.int32_t(ret) -} - -//export goWv_edge_cases_edge_cases_Keyword_free -func goWv_edge_cases_edge_cases_Keyword_free(ctx unsafe.Pointer) { - wvFreeCallback(ctx) -} - -// Widget: Interface with no constructor; instances come from make_widget -// -// Each wrapper holds one strong reference; Close releases it, and a finalizer -// releases it if the wrapper is garbage collected first. -type Widget struct { - ref wvRef -} - -func wvDestroyWidget(ptr unsafe.Pointer) { - C.edge_cases_edge_cases_Widget_destroy((*C.edge_cases_edge_cases_Widget)(ptr)) -} - -// wvAdoptWidget wraps one owned strong reference. A null pointer adopts to nil. -func wvAdoptWidget(ptr *C.edge_cases_edge_cases_Widget) *Widget { - if ptr == nil { - return nil - } - s := &Widget{} - s.ref.init(unsafe.Pointer(ptr), wvDestroyWidget) - runtime.SetFinalizer(s, (*Widget).Close) - return s -} - -// native borrows the wrapper's pointer for one call; pair it with a -// deferred s.ref.release(). -func (s *Widget) native() *C.edge_cases_edge_cases_Widget { - if s == nil { - panic("edge_cases: nil *Widget") - } - return (*C.edge_cases_edge_cases_Widget)(s.ref.acquire("Widget")) -} - -// share returns a new strong reference to the wrapper's object for the -// native library to adopt (the wrapper keeps its own), or null for nil. -func (s *Widget) share() *C.edge_cases_edge_cases_Widget { - if s == nil { - return nil - } - ptr := s.native() - defer s.ref.release() - return C.edge_cases_edge_cases_Widget_clone(ptr) -} - -func wvWriteWidget(w *wvWriter, v *Widget) { - w.writeU64(uint64(uintptr(unsafe.Pointer(v.share())))) -} - -func wvReadWidget(r *wvReader) *Widget { - token := r.readU64() - if token == 0 { - wvMalformed("null object token") - } - return wvAdoptWidget((*C.edge_cases_edge_cases_Widget)(C.wvHandlePtr(C.uintptr_t(token)))) -} - -// Close releases the wrapper's strong reference and always returns nil. -// It's idempotent and safe to call from any goroutine, even while a call -// on the wrapper is in flight: the reference is then released when that -// call returns. The object itself is dropped once its last reference, from -// any wrapper, record, or the native library, is gone. -func (s *Widget) Close() error { - runtime.SetFinalizer(s, nil) - s.ref.close() - return nil -} - -// Refresh: Void method that can fail -func (s *Widget) Refresh() error { - cSelf := s.native() - defer s.ref.release() - var cErr C.edge_cases_error - C.edge_cases_edge_cases_Widget_refresh(cSelf, &cErr) - if cErr.code != 0 { - return wvEdgeErrorsError(wvTakeError(&cErr)) - } - return nil -} - -func (s *Widget) Rename(class string, import_ *string) string { - cSelf := s.native() - defer s.ref.release() - cClassPtr, cClassLen := wvStr(class) - cImportPtr, cImportLen := wvBytes(wvEncode(import_, wvWriteOptString)) - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Widget_rename(cSelf, cClassPtr, cClassLen, cImportPtr, cImportLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvTakeString(cRet, cRetLen) -} - -//export goWv_edge_cases_edge_cases_Widget_fetch_callback -func goWv_edge_cases_edge_cases_Widget_fetch_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { - wvComplete(context_, err_, func() Outer { - return wvDecode(result_ptr, result_len, wvReadOuter) - }) -} - -// Fetch: Async method with a struct result -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func (s *Widget) Fetch(ctx context.Context, id int64) (Outer, error) { - if err := ctx.Err(); err != nil { - return Outer{}, err - } - cSelf := s.native() - defer s.ref.release() - call := wvNewAsyncCall[Outer]() - C.edge_cases_edge_cases_Widget_fetch(cSelf, C.int64_t(id), C.edge_cases_edge_cases_Widget_fetch_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_Widget_fetch_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return Outer{}, err - } - if fail != nil { - return Outer{}, wvEdgeErrorsError(*fail) - } - return res, nil -} - -// Deprecated: Use rename instead -func (s *Widget) OldName() string { - cSelf := s.native() - defer s.ref.release() - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Widget_old_name(cSelf, &cRetLen, &cErr) - wvTrap(&cErr) - return wvTakeString(cRet, cRetLen) -} - -// Close_: Spelled like the release method of several wrappers -func (s *Widget) Close_() bool { - cSelf := s.native() - defer s.ref.release() - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Widget_close(cSelf, &cErr) - wvTrap(&cErr) - return bool(cRet) -} - -// Dispose: Spelled like the release method of the Dart and .NET wrappers -func (s *Widget) Dispose(force bool) { - cSelf := s.native() - defer s.ref.release() - var cErr C.edge_cases_error - C.edge_cases_edge_cases_Widget_dispose(cSelf, C._Bool(force), &cErr) - wvTrap(&cErr) -} - -// Vault: Deprecated interface with a throwing constructor -// -// Each wrapper holds one strong reference; Close releases it, and a finalizer -// releases it if the wrapper is garbage collected first. -// -// Deprecated: Use Widget instead -type Vault struct { - ref wvRef -} - -func wvDestroyVault(ptr unsafe.Pointer) { - C.edge_cases_edge_cases_Vault_destroy((*C.edge_cases_edge_cases_Vault)(ptr)) -} - -// wvAdoptVault wraps one owned strong reference. A null pointer adopts to nil. -func wvAdoptVault(ptr *C.edge_cases_edge_cases_Vault) *Vault { - if ptr == nil { - return nil - } - s := &Vault{} - s.ref.init(unsafe.Pointer(ptr), wvDestroyVault) - runtime.SetFinalizer(s, (*Vault).Close) - return s -} - -// native borrows the wrapper's pointer for one call; pair it with a -// deferred s.ref.release(). -func (s *Vault) native() *C.edge_cases_edge_cases_Vault { - if s == nil { - panic("edge_cases: nil *Vault") - } - return (*C.edge_cases_edge_cases_Vault)(s.ref.acquire("Vault")) -} - -// share returns a new strong reference to the wrapper's object for the -// native library to adopt (the wrapper keeps its own), or null for nil. -func (s *Vault) share() *C.edge_cases_edge_cases_Vault { - if s == nil { - return nil - } - ptr := s.native() - defer s.ref.release() - return C.edge_cases_edge_cases_Vault_clone(ptr) -} - -// Close releases the wrapper's strong reference and always returns nil. -// It's idempotent and safe to call from any goroutine, even while a call -// on the wrapper is in flight: the reference is then released when that -// call returns. The object itself is dropped once its last reference, from -// any wrapper, record, or the native library, is gone. -func (s *Vault) Close() error { - runtime.SetFinalizer(s, nil) - s.ref.close() - return nil -} - -// OpenVault: Fails when the vault is busy -func OpenVault(path string) (*Vault, error) { - cPathPtr, cPathLen := wvStr(path) - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Vault_open(cPathPtr, cPathLen, &cErr) - if cErr.code != 0 { - return nil, wvEdgeErrorsError(wvTakeError(&cErr)) - } - return wvAdoptVault(cRet), nil -} - -func (s *Vault) Peek() *Widget { - cSelf := s.native() - defer s.ref.release() - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Vault_peek(cSelf, &cErr) - wvTrap(&cErr) - return wvAdoptWidget(cRet) -} - -// Handle: Spelled like the handle accessor of several wrappers -func (s *Vault) Handle() int64 { - cSelf := s.native() - defer s.ref.release() - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Vault_handle(cSelf, &cErr) - wvTrap(&cErr) - return int64(cRet) -} - -// VaultProbe: Throwing static -func VaultProbe() (bool, error) { - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Vault_probe(&cErr) - if cErr.code != 0 { - return false, wvEdgeErrorsError(wvTakeError(&cErr)) - } - return bool(cRet), nil -} - -// VaultToString: Static spelled like an inherited object member -func VaultToString() string { - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_Vault_to_string(&cRetLen, &cErr) - wvTrap(&cErr) - return wvTakeString(cRet, cRetLen) -} - -// Noop: No parameters and no result -func Noop() { - var cErr C.edge_cases_error - C.edge_cases_edge_cases_noop(&cErr) - wvTrap(&cErr) -} - -// Select: Function named after a keyword whose parameters are all keywords -func Select(class int32, from string, func_ []string, range_ *int32, type_ bool) int32 { - cFromPtr, cFromLen := wvStr(from) - cFuncPtr, cFuncLen := wvBytes(wvEncode(func_, wvWriteListString)) - cRangePtr, cRangeLen := wvBytes(wvEncode(range_, wvWriteOptI32)) - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_select(C.int32_t(class), cFromPtr, cFromLen, cFuncPtr, cFuncLen, cRangePtr, cRangeLen, C._Bool(type_), &cErr) - wvTrap(&cErr) - return int32(cRet) -} - -// NestedOptional: Optional list of optional structs, the deepest legal nesting -func NestedOptional(items []*Outer) []*Outer { - cItemsPtr, cItemsLen := wvBytes(wvEncode(items, wvWriteOptListOptOuter)) - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_nested_optional(cItemsPtr, cItemsLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadOptListOptOuter) -} - -func OptionalParams(text *string, count *int32, outer *Outer, tags []string, blob []byte, mode *Mode) *Mode { - cTextPtr, cTextLen := wvBytes(wvEncode(text, wvWriteOptString)) - cCountPtr, cCountLen := wvBytes(wvEncode(count, wvWriteOptI32)) - cOuterPtr, cOuterLen := wvBytes(wvEncode(outer, wvWriteOptOuter)) - cTagsPtr, cTagsLen := wvBytes(wvEncode(tags, wvWriteOptListString)) - cBlobPtr, cBlobLen := wvBytes(wvEncode(blob, wvWriteOptBytes)) - cModePtr, cModeLen := wvBytes(wvEncode(mode, wvWriteOptMode)) - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_optional_params(cTextPtr, cTextLen, cCountPtr, cCountLen, cOuterPtr, cOuterLen, cTagsPtr, cTagsLen, cBlobPtr, cBlobLen, cModePtr, cModeLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadOptMode) -} - -// RegisterBatch: Two callback interface parameters on one function -func RegisterBatch(listener BatchListener, keyword Keyword) { - var cErr C.edge_cases_error - C.edge_cases_edge_cases_register_batch(wvNewCallback(listener), C.wvVtablePtr_edge_cases_edge_cases_BatchListener_vtable(), wvNewCallback(keyword), C.wvVtablePtr_edge_cases_edge_cases_Keyword_vtable(), &cErr) - wvTrap(&cErr) -} - -// WidgetsEverywhere: Objects in every buffered position -func WidgetsEverywhere(list []*Widget, byName map[string]*Widget, sparse []*Widget, holder WidgetHolder) WidgetHolder { - cListPtr, cListLen := wvBytes(wvEncode(list, wvWriteListWidget)) - cByNamePtr, cByNameLen := wvBytes(wvEncode(byName, wvWriteMapStringWidget)) - cSparsePtr, cSparseLen := wvBytes(wvEncode(sparse, wvWriteListOptWidget)) - cHolderPtr, cHolderLen := wvBytes(wvEncode(holder, wvWriteWidgetHolder)) - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_widgets_everywhere(cListPtr, cListLen, cByNamePtr, cByNameLen, cSparsePtr, cSparseLen, cHolderPtr, cHolderLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadWidgetHolder) -} - -// WidgetStream: Iterator of objects -// -// The sequence launches the native iterator each time it's ranged over and -// pulls one element per step; the iterator is released when the range ends, -// early or not. -func WidgetStream() iter.Seq[*Widget] { - return func(yield func(*Widget) bool) { - var cErr C.edge_cases_error - cIter := C.edge_cases_edge_cases_widget_stream(&cErr) - wvTrap(&cErr) - defer C.edge_cases_edge_cases_WidgetStreamIterator_destroy(cIter) - for { - var cItem *C.edge_cases_edge_cases_Widget - var cIterErr C.edge_cases_error - more := C.edge_cases_edge_cases_WidgetStreamIterator_next(cIter, &cItem, &cIterErr) != 0 - wvTrap(&cIterErr) - if !more { - return - } - if !yield(wvAdoptWidget(cItem)) { - return - } - } - } -} - -//export goWv_edge_cases_edge_cases_async_widget_callback -func goWv_edge_cases_edge_cases_async_widget_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result *C.edge_cases_edge_cases_Widget) { - wvComplete(context_, err_, func() *Widget { - return wvAdoptWidget(result) - }) -} - -// AsyncWidget: Async returning an object -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncWidget(ctx context.Context) (*Widget, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - call := wvNewAsyncCall[*Widget]() - C.edge_cases_edge_cases_async_widget(C.edge_cases_edge_cases_async_widget_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_widget_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return nil, err - } - if fail != nil { - return nil, wvEdgeErrorsError(*fail) - } - return res, nil -} - -// MakeWidget: Factory for an interface with no constructor -func MakeWidget(label string) *Widget { - cLabelPtr, cLabelLen := wvStr(label) - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_make_widget(cLabelPtr, cLabelLen, &cErr) - wvTrap(&cErr) - return wvAdoptWidget(cRet) -} - -// FindWidget: Optional interface return -func FindWidget(id int64) (*Widget, error) { - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_find_widget(C.int64_t(id), &cErr) - if cErr.code != 0 { - return nil, wvEdgeErrorsError(wvTakeError(&cErr)) - } - return wvAdoptWidget(cRet), nil -} - -// InspectWidget: Interface parameter, including an optional one -func InspectWidget(widget *Widget, fallback *Widget) string { - cWidget := widget.native() - defer widget.ref.release() - var cFallback *C.edge_cases_edge_cases_Widget - if fallback != nil { - cFallback = fallback.native() - defer fallback.ref.release() - } - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_inspect_widget(cWidget, cFallback, &cRetLen, &cErr) - wvTrap(&cErr) - return wvTakeString(cRet, cRetLen) -} - -// StringStream: Iterator of strings -// -// The sequence launches the native iterator each time it's ranged over and -// pulls one element per step; the iterator is released when the range ends, -// early or not. -func StringStream() iter.Seq[string] { - return func(yield func(string) bool) { - var cErr C.edge_cases_error - cIter := C.edge_cases_edge_cases_string_stream(&cErr) - wvTrap(&cErr) - defer C.edge_cases_edge_cases_StringStreamIterator_destroy(cIter) - for { - var cItem *C.uint8_t - var cItemLen C.size_t - var cIterErr C.edge_cases_error - more := C.edge_cases_edge_cases_StringStreamIterator_next(cIter, &cItem, &cItemLen, &cIterErr) != 0 - wvTrap(&cIterErr) - if !more { - return - } - if !yield(wvTakeString(cItem, cItemLen)) { - return - } - } - } -} - -// ScalarStream: Iterator of scalars -// -// The sequence launches the native iterator each time it's ranged over and -// pulls one element per step; the iterator is released when the range ends, -// early or not. -func ScalarStream() iter.Seq[int32] { - return func(yield func(int32) bool) { - var cErr C.edge_cases_error - cIter := C.edge_cases_edge_cases_scalar_stream(&cErr) - wvTrap(&cErr) - defer C.edge_cases_edge_cases_ScalarStreamIterator_destroy(cIter) - for { - var cItem C.int32_t - var cIterErr C.edge_cases_error - more := C.edge_cases_edge_cases_ScalarStreamIterator_next(cIter, &cItem, &cIterErr) != 0 - wvTrap(&cIterErr) - if !more { - return - } - if !yield(int32(cItem)) { - return - } - } - } -} - -// ModeStream: Iterator of enums -// -// The sequence launches the native iterator each time it's ranged over and -// pulls one element per step; the iterator is released when the range ends, -// early or not. -func ModeStream() iter.Seq[Mode] { - return func(yield func(Mode) bool) { - var cErr C.edge_cases_error - cIter := C.edge_cases_edge_cases_mode_stream(&cErr) - wvTrap(&cErr) - defer C.edge_cases_edge_cases_ModeStreamIterator_destroy(cIter) - for { - var cItem C.edge_cases_edge_cases_Mode - var cIterErr C.edge_cases_error - more := C.edge_cases_edge_cases_ModeStreamIterator_next(cIter, &cItem, &cIterErr) != 0 - wvTrap(&cIterErr) - if !more { - return - } - if !yield(Mode(cItem)) { - return - } - } - } -} - -//export goWv_edge_cases_edge_cases_async_void_callback -func goWv_edge_cases_edge_cases_async_void_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error) { - wvComplete(context_, err_, func() struct{} { - return struct{}{} - }) -} - -// AsyncVoid: Async with no result -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncVoid(ctx context.Context) error { - if err := ctx.Err(); err != nil { - return err - } - call := wvNewAsyncCall[struct{}]() - C.edge_cases_edge_cases_async_void(C.edge_cases_edge_cases_async_void_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_void_callback)), call.context()) - _, fail, err := call.wait(ctx, nil) - if err != nil { - return err - } - if fail != nil { - panic(fail.err()) - } - return nil -} - -//export goWv_edge_cases_edge_cases_async_bytes_callback -func goWv_edge_cases_edge_cases_async_bytes_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { - wvComplete(context_, err_, func() []byte { - return wvTakeBytes(result_ptr, result_len) - }) -} - -// AsyncBytes calls the native async function. -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncBytes(ctx context.Context, size uint32) ([]byte, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - call := wvNewAsyncCall[[]byte]() - C.edge_cases_edge_cases_async_bytes(C.uint32_t(size), C.edge_cases_edge_cases_async_bytes_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_bytes_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return nil, err - } - if fail != nil { - panic(fail.err()) - } - return res, nil -} - -//export goWv_edge_cases_edge_cases_async_struct_callback -func goWv_edge_cases_edge_cases_async_struct_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { - wvComplete(context_, err_, func() Outer { - return wvDecode(result_ptr, result_len, wvReadOuter) - }) -} - -// AsyncStruct calls the native async function. -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncStruct(ctx context.Context) (Outer, error) { - if err := ctx.Err(); err != nil { - return Outer{}, err - } - call := wvNewAsyncCall[Outer]() - C.edge_cases_edge_cases_async_struct(C.edge_cases_edge_cases_async_struct_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_struct_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return Outer{}, err - } - if fail != nil { - return Outer{}, wvEdgeErrorsError(*fail) - } - return res, nil -} - -//export goWv_edge_cases_edge_cases_async_list_callback -func goWv_edge_cases_edge_cases_async_list_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { - wvComplete(context_, err_, func() []Outer { - return wvDecode(result_ptr, result_len, wvReadListOuter) - }) -} - -// AsyncList calls the native async function. -// -// It blocks until the call completes. Cancelling ctx cancels the native call, -// which then returns ctx.Err(). -func AsyncList(ctx context.Context) ([]Outer, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - token := C.edge_cases_cancel_token_create() - defer C.edge_cases_cancel_token_destroy(token) - call := wvNewAsyncCall[[]Outer]() - C.edge_cases_edge_cases_async_list(token, C.edge_cases_edge_cases_async_list_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_list_callback)), call.context()) - res, fail, err := call.wait(ctx, token) - if err != nil { - return nil, err - } - if fail != nil { - return nil, wvEdgeErrorsError(*fail) - } - return res, nil -} - -//export goWv_edge_cases_edge_cases_async_optional_callback -func goWv_edge_cases_edge_cases_async_optional_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { - wvComplete(context_, err_, func() *int64 { - return wvDecode(result_ptr, result_len, wvReadOptI64) - }) -} - -// AsyncOptional calls the native async function. -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncOptional(ctx context.Context) (*int64, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - call := wvNewAsyncCall[*int64]() - C.edge_cases_edge_cases_async_optional(C.edge_cases_edge_cases_async_optional_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_optional_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return nil, err - } - if fail != nil { - panic(fail.err()) - } - return res, nil -} - -//export goWv_edge_cases_edge_cases_async_enum_callback -func goWv_edge_cases_edge_cases_async_enum_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result C.edge_cases_edge_cases_Mode) { - wvComplete(context_, err_, func() Mode { - return Mode(result) - }) -} - -// AsyncEnum calls the native async function. -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncEnum(ctx context.Context) (Mode, error) { - if err := ctx.Err(); err != nil { - return 0, err - } - call := wvNewAsyncCall[Mode]() - C.edge_cases_edge_cases_async_enum(C.edge_cases_edge_cases_async_enum_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_enum_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return 0, err - } - if fail != nil { - panic(fail.err()) - } - return res, nil -} - -//export goWv_edge_cases_edge_cases_async_keywords_callback -func goWv_edge_cases_edge_cases_async_keywords_callback(context_ unsafe.Pointer, err_ *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { - wvComplete(context_, err_, func() string { - return wvTakeString(result_ptr, result_len) - }) -} - -// AsyncKeywords calls the native async function. -// -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. -func AsyncKeywords(ctx context.Context, class string, import_ int32) (string, error) { - if err := ctx.Err(); err != nil { - return "", err - } - cClassPtr, cClassLen := wvStr(class) - call := wvNewAsyncCall[string]() - C.edge_cases_edge_cases_async_keywords(cClassPtr, cClassLen, C.int32_t(import_), C.edge_cases_edge_cases_async_keywords_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_keywords_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return "", err - } - if fail != nil { - panic(fail.err()) - } - return res, nil -} - -func KeywordsRoundtrip(value Keywords) Keywords { - cValuePtr, cValueLen := wvBytes(wvEncode(value, wvWriteKeywords)) - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_keywords_roundtrip(cValuePtr, cValueLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadKeywords) -} - -// Deprecated: Use Mode-based APIs -func LegacyValue(value Legacy) Retired { - var cRetLen C.size_t - var cErr C.edge_cases_error - cRet := C.edge_cases_edge_cases_legacy_value(C.edge_cases_edge_cases_Legacy(value), &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadRetired) -} - -func wvWriteListI32(w *wvWriter, v []int32) { - wvWriteList(w, v, (*wvWriter).writeI32) -} - -func wvReadListI32(r *wvReader) []int32 { - return wvReadList(r, (*wvReader).readI32) -} - -func wvWriteListLeaf(w *wvWriter, v []Leaf) { - wvWriteList(w, v, wvWriteLeaf) -} - -func wvReadListLeaf(r *wvReader) []Leaf { - return wvReadList(r, wvReadLeaf) -} - -func wvWriteListListI32(w *wvWriter, v [][]int32) { - wvWriteList(w, v, wvWriteListI32) -} - -func wvReadListListI32(r *wvReader) [][]int32 { - return wvReadList(r, wvReadListI32) -} - -func wvWriteListMode(w *wvWriter, v []Mode) { - wvWriteList(w, v, wvWriteMode) -} - -func wvReadListMode(r *wvReader) []Mode { - return wvReadList(r, wvReadMode) -} - -func wvWriteListOptI64(w *wvWriter, v []*int64) { - wvWriteList(w, v, wvWriteOptI64) -} - -func wvReadListOptI64(r *wvReader) []*int64 { - return wvReadList(r, wvReadOptI64) -} - -func wvWriteListOptOuter(w *wvWriter, v []*Outer) { - wvWriteList(w, v, wvWriteOptOuter) -} - -func wvReadListOptOuter(r *wvReader) []*Outer { - return wvReadList(r, wvReadOptOuter) -} - -func wvWriteListOptWidget(w *wvWriter, v []*Widget) { - wvWriteList(w, v, wvWriteOptWidget) -} - -func wvReadListOptWidget(r *wvReader) []*Widget { - return wvReadList(r, wvReadOptWidget) -} - -func wvWriteListOuter(w *wvWriter, v []Outer) { - wvWriteList(w, v, wvWriteOuter) -} - -func wvReadListOuter(r *wvReader) []Outer { - return wvReadList(r, wvReadOuter) -} - -func wvWriteListString(w *wvWriter, v []string) { - wvWriteList(w, v, (*wvWriter).writeString) -} - -func wvReadListString(r *wvReader) []string { - return wvReadList(r, (*wvReader).readString) -} - -func wvWriteListWidget(w *wvWriter, v []*Widget) { - wvWriteList(w, v, wvWriteWidget) -} - -func wvReadListWidget(r *wvReader) []*Widget { - return wvReadList(r, wvReadWidget) -} - -func wvWriteMapI64String(w *wvWriter, v map[int64]string) { - wvWriteMap(w, v, (*wvWriter).writeI64, (*wvWriter).writeString) -} - -func wvReadMapI64String(r *wvReader) map[int64]string { - return wvReadMap(r, (*wvReader).readI64, (*wvReader).readString) -} - -func wvWriteMapStringInner(w *wvWriter, v map[string]Inner) { - wvWriteMap(w, v, (*wvWriter).writeString, wvWriteInner) -} - -func wvReadMapStringInner(r *wvReader) map[string]Inner { - return wvReadMap(r, (*wvReader).readString, wvReadInner) -} - -func wvWriteMapStringWidget(w *wvWriter, v map[string]*Widget) { - wvWriteMap(w, v, (*wvWriter).writeString, wvWriteWidget) -} - -func wvReadMapStringWidget(r *wvReader) map[string]*Widget { - return wvReadMap(r, (*wvReader).readString, wvReadWidget) -} - -func wvWriteOptBytes(w *wvWriter, v []byte) { - wvWriteNilable(w, v, v != nil, (*wvWriter).writeBytes) -} - -func wvReadOptBytes(r *wvReader) []byte { - return wvReadNilable(r, (*wvReader).readBytes) -} - -func wvWriteOptI32(w *wvWriter, v *int32) { - wvWriteOptional(w, v, (*wvWriter).writeI32) -} - -func wvReadOptI32(r *wvReader) *int32 { - return wvReadOptional(r, (*wvReader).readI32) -} - -func wvWriteOptI64(w *wvWriter, v *int64) { - wvWriteOptional(w, v, (*wvWriter).writeI64) -} - -func wvReadOptI64(r *wvReader) *int64 { - return wvReadOptional(r, (*wvReader).readI64) -} - -func wvWriteOptInner(w *wvWriter, v *Inner) { - wvWriteOptional(w, v, wvWriteInner) -} - -func wvReadOptInner(r *wvReader) *Inner { - return wvReadOptional(r, wvReadInner) -} - -func wvWriteOptListOptOuter(w *wvWriter, v []*Outer) { - wvWriteNilable(w, v, v != nil, wvWriteListOptOuter) -} - -func wvReadOptListOptOuter(r *wvReader) []*Outer { - return wvReadNilable(r, wvReadListOptOuter) -} - -func wvWriteOptListString(w *wvWriter, v []string) { - wvWriteNilable(w, v, v != nil, wvWriteListString) -} - -func wvReadOptListString(r *wvReader) []string { - return wvReadNilable(r, wvReadListString) -} - -func wvWriteOptMode(w *wvWriter, v *Mode) { - wvWriteOptional(w, v, wvWriteMode) -} - -func wvReadOptMode(r *wvReader) *Mode { - return wvReadOptional(r, wvReadMode) -} - -func wvWriteOptOuter(w *wvWriter, v *Outer) { - wvWriteOptional(w, v, wvWriteOuter) -} - -func wvReadOptOuter(r *wvReader) *Outer { - return wvReadOptional(r, wvReadOuter) -} - -func wvWriteOptString(w *wvWriter, v *string) { - wvWriteOptional(w, v, (*wvWriter).writeString) -} - -func wvReadOptString(r *wvReader) *string { - return wvReadOptional(r, (*wvReader).readString) -} - -func wvWriteOptWidget(w *wvWriter, v *Widget) { - wvWriteNilable(w, v, v != nil, wvWriteWidget) -} - -func wvReadOptWidget(r *wvReader) *Widget { - return wvReadNilable(r, wvReadWidget) -} - -// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__README_md.snap deleted file mode 100644 index d4e7d167..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__README_md.snap +++ /dev/null @@ -1,59 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink (Go) - -cgo bindings for the `kitchen_sink` native library. - -## Build - -The module ships its own copy of the C header (`kitchen_sink.h`) and links with -`-lkitchen_sink`. Point the linker at the directory holding `libkitchen_sink.so`, -`libkitchen_sink.dylib`, or `kitchen_sink.dll`, and make the library findable at -run time: - -```sh -export CGO_LDFLAGS="-L/path/to/lib" -export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS -go build ./... -``` - -cgo links the library when the program is built and the platform loader -finds it when the program starts, so the library-path environment variable -the runtime-loading bindings (Python, .NET, and others) honor doesn't apply. -Add `-Wl,-rpath,/path/to/lib` to `CGO_LDFLAGS` to record the directory in the -binary instead of setting the loader path. - -Depend on the module from your own project with a `require` (and, for a -local checkout, a `replace`) of `kitchen_sink`, then import it: - -```go -import "kitchen_sink" -``` - -Go 1.23 or newer is required. - -## Usage notes - -- Records are plain structs and rich enums are sealed interfaces; both cross - the boundary by value. -- Interfaces are reference-counted objects. Call `Close` when you're done - with one (a finalizer releases it otherwise); it's safe to call twice or - while a call is in flight. -- A call that declares errors returns `error` values you match with - `errors.As`; any other call panics with an `*Error` when the library - reports a failure, since that's a bug. -- Async functions take a `context.Context` first. Cancellable ones cancel - the native call when the context is done. -- Callback interfaces are Go interfaces you implement. The library may call - them from any thread; a panic is reported to the native caller as a - callback failure instead of crashing. -- Importing the package checks the library's ABI revision and every - module's contract table, and panics naming the first declaration that's - missing from the library or changed since the bindings were generated. - - diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap index d2ad043f..d0b06ae6 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap @@ -5,8 +5,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs // DO NOT EDIT. Your changes will be overwritten. // To regenerate: weaveffi generate -// Package kitchen_sink binds the kitchen_sink native library. -package kitchen_sink +// Code generated by WeaveFFI. DO NOT EDIT. + +// Package kitchensink binds the kitchen_sink native library. +// +// Call [Check] at startup to find out whether the linked library is the +// one these bindings were generated for. +package kitchensink /* #cgo LDFLAGS: -lkitchen_sink @@ -30,63 +35,116 @@ static const kitchen_sink_kitchen_ReadyListener_vtable wvVtable_kitchen_sink_kit .favorite = goWv_kitchen_sink_kitchen_ReadyListener_favorite, }; __attribute__((unused)) static const kitchen_sink_kitchen_ReadyListener_vtable* wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable(void) { return &wvVtable_kitchen_sink_kitchen_ReadyListener_vtable; } +extern bool goWv_kitchen_sink_kitchen_Sampler_threshold(void* ctx, bool has_level, uint32_t level, double* out_value, kitchen_sink_error* out_err); +extern void goWv_kitchen_sink_kitchen_Sampler_samples(void* ctx, bool has_limit, uint32_t limit, double* weights_ptr, size_t weights_len, int16_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); +extern void goWv_kitchen_sink_kitchen_Sampler_describe(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); +extern void goWv_kitchen_sink_kitchen_Sampler_free(void* ctx); +static const kitchen_sink_kitchen_Sampler_vtable wvVtable_kitchen_sink_kitchen_Sampler_vtable = { + .size = sizeof(kitchen_sink_kitchen_Sampler_vtable), + .flags = 0, + .free = goWv_kitchen_sink_kitchen_Sampler_free, + .threshold = goWv_kitchen_sink_kitchen_Sampler_threshold, + .samples = (void (*)(void*, bool, uint32_t, const double*, size_t, int16_t**, size_t*, kitchen_sink_error*))goWv_kitchen_sink_kitchen_Sampler_samples, + .describe = goWv_kitchen_sink_kitchen_Sampler_describe, +}; +__attribute__((unused)) static const kitchen_sink_kitchen_Sampler_vtable* wvVtablePtr_kitchen_sink_kitchen_Sampler_vtable(void) { return &wvVtable_kitchen_sink_kitchen_Sampler_vtable; } extern void goWv_kitchen_sink_kitchen_do_async_callback(void* context, kitchen_sink_error* err, uint8_t* result_ptr, size_t result_len); extern void goWv_kitchen_sink_kitchen_do_cancellable_callback(void* context, kitchen_sink_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_kitchen_sink_kitchen_async_maybe_count_callback(void* context, kitchen_sink_error* err, bool has_result, int64_t result); +extern void goWv_kitchen_sink_kitchen_async_samples_callback(void* context, kitchen_sink_error* err, float* result_ptr, size_t result_len); */ import "C" import ( "context" "iter" - "runtime" "unsafe" ) -func init() { - wvCheckABI() +// wvCheckLibrary checks the linked library's ABI revision and every +// top-level module's contract table. +func wvCheckLibrary() error { + if err := wvCheckABI(); err != nil { + return err + } var n C.size_t table := C.kitchen_sink_shared_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ + if err := wvCheckContract(table, n, []wvContractEntry{ {0x709e234caa962446, 0x3894f2ca452e9f98, "shared.ping"}, - {0xcfb306fa5801ee5b, 0x1cbd9e10983cac2c, "shared.Token"}, - }) + {0xcfb306fa5801ee5b, 0x18e9fc204a13df6f, "shared.Token"}, + }); err != nil { + return err + } table = C.kitchen_sink_kitchen_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0x01d4f09bbddc986f, 0x29eb88fecc66544d, "kitchen.Gadget.new"}, - {0x10472d8fe748c2ee, 0xb265832b6a759fc4, "kitchen.maybe_gadget"}, + if err := wvCheckContract(table, n, []wvContractEntry{ + {0x01d4f09bbddc986f, 0xffcf2bd3b04aa002, "kitchen.Gadget.new"}, + {0x02bbd5f31c953704, 0x2cfa679ea1373e09, "kitchen.Sampler.samples"}, + {0x10472d8fe748c2ee, 0xcc570a2649bbf7dc, "kitchen.maybe_gadget"}, {0x11368eec07a74078, 0xfa049f78b77e146a, "kitchen.gadgets"}, {0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, "kitchen.stream_gadgets"}, - {0x2111b54980826dd7, 0x452783f3e6ca6e1d, "kitchen.do_cancellable"}, + {0x184bfc4ab0e3fe45, 0xb64587477022696b, "kitchen.ids_u64"}, + {0x2111b54980826dd7, 0x627bf01ab365b209, "kitchen.do_cancellable"}, + {0x21c70f9371374d74, 0x25d5d88a30fe0a79, "kitchen.ReadyListener.favorite"}, + {0x23de5bd597372f46, 0xcf1bc6bb37872798, "kitchen.stream_chunks"}, + {0x249f1ddaec6a6cbe, 0x0710d6c9a4de93da, "kitchen.take_from_pantry"}, {0x29dddbc60bf8d7db, 0x452a3e573d57f6db, "kitchen.Priority"}, - {0x2b227003b74a0d16, 0x12332957470e9782, "kitchen.maybe_subscribe"}, - {0x2dd66cb0064f0494, 0x60197363fefd92dd, "kitchen.Item"}, - {0x33c265bbf079b0e9, 0xc3dcd0927e6c1deb, "kitchen.u32_id"}, - {0x33e6381d01660dfe, 0x077c63c31fd7f90a, "kitchen.KitchenErrors"}, - {0x3f5fc99d73504b76, 0x2df88d710f1ea10e, "kitchen.Gadget.poke"}, - {0x4c5764cf109528d1, 0x2a65529af4338a58, "kitchen.maybe_item"}, - {0x4de17dfb93fd494d, 0xba0ad0dd52ed74af, "kitchen.i32_id"}, + {0x2b227003b74a0d16, 0x5dae036b8a6aed30, "kitchen.maybe_subscribe"}, + {0x2dd66cb0064f0494, 0x74b131724dd0d867, "kitchen.Item"}, + {0x33c265bbf079b0e9, 0x97c32b764693f2cb, "kitchen.u32_id"}, + {0x33e6381d01660dfe, 0x05e35a563089bf7d, "kitchen.KitchenErrors"}, + {0x3d969ae76db6cb23, 0x65a325cfc772296b, "kitchen.ReadyListener.latest"}, + {0x3f5fc99d73504b76, 0x417e4a2acd3210b3, "kitchen.Gadget.poke"}, + {0x4082f0085db28781, 0x18848e701962912e, "kitchen.Sampler"}, + {0x4340a6b1e7ec8c50, 0xa59d0f32a66dc7a2, "kitchen.Sampler.describe"}, + {0x4c5764cf109528d1, 0xb374accf14676d90, "kitchen.maybe_item"}, + {0x4de17dfb93fd494d, 0x78be3203bcd10727, "kitchen.i32_id"}, {0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, "kitchen.stream_items"}, - {0x68cdb9a4bd3e3dd5, 0xc678b9164348d497, "kitchen.bool_id"}, - {0x69e7cd703d16eb85, 0xf3b28dd22ae9e689, "kitchen.do_async"}, + {0x5dafabb7775fcb2d, 0x6c4b78c7d3f858c1, "kitchen.PantryError.Spoiled"}, + {0x5e0a8f6be4b00600, 0x18ea1a42036c96a0, "kitchen.Sampler.threshold"}, + {0x5ec0af1d4ecc6fed, 0x6540bdb74246420c, "kitchen.async_samples"}, + {0x61a358b0571e0a4e, 0x11a24c4b50d11996, "kitchen.async_maybe_count"}, + {0x68cdb9a4bd3e3dd5, 0xcde022fe6bc3b1bf, "kitchen.bool_id"}, + {0x69e7cd703d16eb85, 0xb13cb8d39e702c79, "kitchen.do_async"}, {0x70865079ce41be82, 0xbe3b69464672af46, "kitchen.Gadget.describe"}, - {0x71c5a1116cb7c49a, 0xcba1837108a89b84, "kitchen.echo_bytes"}, - {0x735ee1e0049ee91a, 0xca74320bc53eddab, "kitchen.echo_string"}, - {0x76d7fc58f4e41d84, 0x7373a0e47d30e690, "kitchen.i64_id"}, + {0x71c5a1116cb7c49a, 0x1506cfe6f548fac4, "kitchen.echo_bytes"}, + {0x735ee1e0049ee91a, 0xa713dbd1be707752, "kitchen.echo_string"}, + {0x76d7fc58f4e41d84, 0x6bdb4ccea2841d5e, "kitchen.i64_id"}, + {0x78d91fd617e162b8, 0xd88e2753503b36df, "kitchen.stream_maybe"}, {0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, "kitchen.nested.hello"}, + {0x7b3865b536da998c, 0x1c4e79d60a20e74f, "kitchen.mixed_slices"}, + {0x7ba266dc26a58989, 0x0f86001cac0c21e8, "kitchen.KitchenErrors.InvalidInput"}, {0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, "kitchen.new_op"}, - {0x89a5e664bc25c67b, 0x5764a95e81ecfd31, "kitchen.f64_id"}, + {0x81ded69adee0abf2, 0x8c71141a931906f6, "kitchen.scale_i32"}, + {0x81fa72769330fed6, 0x89681e305b55a4f4, "kitchen.ReadyListener.label"}, + {0x89a5e664bc25c67b, 0x357fd9b65ad879a9, "kitchen.f64_id"}, {0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, "kitchen.Gadget.version"}, + {0x8dc42397e47614b9, 0xcddf6fede09b2e7f, "kitchen.opt_priority"}, + {0x92f060ef68961fc1, 0x4af6ce5b4f9fa508, "kitchen.ReadyListener.on_item"}, {0x974b9f3e823c1738, 0xb81b91e975695f27, "kitchen.cross_module_token"}, + {0x9f0f001aa85f97d5, 0x55815247f895bd45, "kitchen.sum_f64"}, + {0xab2a29fd1ef5458b, 0x471a34aa4c09ae2b, "kitchen.KitchenErrors.NotFound"}, {0xaca0655fac15d230, 0x11c4d2b28abe07bb, "kitchen.list_items"}, + {0xb57c8e6ae3fb4383, 0x3f857851ab75366c, "kitchen.PantryError"}, + {0xbdc795b195edd428, 0x36127462f8b0969f, "kitchen.opt_mixed"}, + {0xbe4821ab3c340e71, 0xe1d32e38b766e623, "kitchen.ReadyListener.on_ready"}, + {0xc2ad53f2a6a36323, 0x071e29abee823f17, "kitchen.opt_i32_id"}, + {0xc7ab409554fd3d7b, 0xfea12b6bc219fc46, "kitchen.parse_any"}, + {0xc82a7f2d85f99f4e, 0xa9bcb8bacedb036c, "kitchen.sample_with"}, {0xc883b4afae20543e, 0xc40560e5411ff392, "kitchen.legacy_op"}, + {0xd180bf646bff59e2, 0xa4515a6d00d7c600, "kitchen.PantryError.OutOfStock"}, + {0xe6778df0f5c1b82b, 0x38eaafdb350559e1, "kitchen.opt_bool_id"}, {0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, "kitchen.get_attrs"}, - {0xfae25d3391922ead, 0x1c7f95739f3210b8, "kitchen.subscribe"}, + {0xfae25d3391922ead, 0xcc034cf506cae182, "kitchen.subscribe"}, {0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, "kitchen.Gadget"}, - {0xfed9c551fb9fd2d6, 0x59aec980ef3f3291, "kitchen.ReadyListener"}, - }) + {0xfed9c551fb9fd2d6, 0xe1020573f084dc1d, "kitchen.ReadyListener"}, + {0xff3a99b52597dee2, 0x14c4ecac97bdfc9b, "kitchen.Gadget.readings"}, + }); err != nil { + return err + } + return nil } -// Token is a reusable token referenced across modules +// Token: A reusable token referenced across modules type Token struct { ID int64 Label string @@ -106,6 +164,7 @@ func wvReadToken(r *wvReader) Token { // Ping: Trivial cross-module helper func Ping() string { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_shared_ping(&cRetLen, &cErr) @@ -113,20 +172,21 @@ func Ping() string { return wvTakeString(cRet, cRetLen) } -// KitchenErrorsError is an error the kitchen module reports: *NotFoundError, -// or *InvalidInputError. +// KitchenError is an error of the KitchenErrors domain: *NotFoundError, +// *InvalidInputError, or *UnknownKitchenError for a code these bindings don't +// declare. // -// Match one code with errors.As and its type, or any of them with a -// KitchenErrorsError. A throwing callback method returns one to report that -// code. -type KitchenErrorsError interface { +// Match one code with errors.As and its type, or any code of the domain with a +// KitchenError. A callback method that declares the domain returns one to +// report that code. +type KitchenError interface { error // Code returns the error's numeric code. Code() int32 - isKitchenErrorsError() + isKitchenError() } -// NotFoundError is the KitchenErrorsError code NotFound (1): Item not found. +// NotFoundError is the KitchenError code NotFound (1): Item not found. type NotFoundError struct { // Message describes the failure; when it's empty, Error returns the // code's default message, "Item not found". @@ -146,14 +206,13 @@ func (*NotFoundError) Code() int32 { return 1 } -func (*NotFoundError) isKitchenErrorsError() {} +func (*NotFoundError) isKitchenError() {} func (*NotFoundError) wvPayload() []byte { return nil } -// InvalidInputError is the KitchenErrorsError code InvalidInput (2): Invalid -// input. +// InvalidInputError is the KitchenError code InvalidInput (2): Invalid input. type InvalidInputError struct { // Message describes the failure; when it's empty, Error returns the // code's default message, "Invalid input". @@ -173,19 +232,164 @@ func (*InvalidInputError) Code() int32 { return 2 } -func (*InvalidInputError) isKitchenErrorsError() {} +func (*InvalidInputError) isKitchenError() {} func (*InvalidInputError) wvPayload() []byte { return nil } -func wvKitchenErrorsError(f wvFailure) error { +// UnknownKitchenError is a KitchenError code these bindings don't declare, +// which a newer library may report. +type UnknownKitchenError struct { + code int32 + // Message is the library's message. + Message string +} + +// Error returns the error's message. +func (e *UnknownKitchenError) Error() string { + if e.Message == "" { + return wvCodeMessage("KitchenErrors", e.code) + } + return e.Message +} + +// Code returns the code the library reported. +func (e *UnknownKitchenError) Code() int32 { + return e.code +} + +func (*UnknownKitchenError) isKitchenError() {} + +func (*UnknownKitchenError) wvPayload() []byte { + return nil +} + +func wvKitchenError(f wvFailure) error { switch f.code { case 1: return &NotFoundError{Message: f.message} case 2: return &InvalidInputError{Message: f.message} } + if f.code > 0 { + return &UnknownKitchenError{code: f.code, Message: f.message} + } + return f.err() +} + +// PantryError is an error of the PantryError domain: *OutOfStockError, +// *SpoiledError, or *UnknownPantryError for a code these bindings don't +// declare. +// +// Match one code with errors.As and its type, or any code of the domain with a +// PantryError. A callback method that declares the domain returns one to +// report that code. +type PantryError interface { + error + // Code returns the error's numeric code. + Code() int32 + isPantryError() +} + +// OutOfStockError is the PantryError code OutOfStock (1): Out of stock. +type OutOfStockError struct { + Item string + // Message describes the failure; when it's empty, Error returns the + // code's default message, "Out of stock". + Message string +} + +// Error returns the error's message. +func (e *OutOfStockError) Error() string { + if e.Message == "" { + return "Out of stock" + } + return e.Message +} + +// Code returns 1. +func (*OutOfStockError) Code() int32 { + return 1 +} + +func (*OutOfStockError) isPantryError() {} + +func (e *OutOfStockError) wvPayload() []byte { + w := &wvWriter{} + w.writeString(e.Item) + return w.buf +} + +// SpoiledError is the PantryError code Spoiled (2): Spoiled. +type SpoiledError struct { + // Message describes the failure; when it's empty, Error returns the + // code's default message, "Spoiled". + Message string +} + +// Error returns the error's message. +func (e *SpoiledError) Error() string { + if e.Message == "" { + return "Spoiled" + } + return e.Message +} + +// Code returns 2. +func (*SpoiledError) Code() int32 { + return 2 +} + +func (*SpoiledError) isPantryError() {} + +func (*SpoiledError) wvPayload() []byte { + return nil +} + +// UnknownPantryError is a PantryError code these bindings don't declare, which +// a newer library may report. +type UnknownPantryError struct { + code int32 + // Message is the library's message. + Message string +} + +// Error returns the error's message. +func (e *UnknownPantryError) Error() string { + if e.Message == "" { + return wvCodeMessage("PantryError", e.code) + } + return e.Message +} + +// Code returns the code the library reported. +func (e *UnknownPantryError) Code() int32 { + return e.code +} + +func (*UnknownPantryError) isPantryError() {} + +func (*UnknownPantryError) wvPayload() []byte { + return nil +} + +func wvPantryError(f wvFailure) error { + switch f.code { + case 1: + e := &OutOfStockError{Message: f.message} + if f.payload != nil { + r := &wvReader{buf: f.payload} + e.Item = r.readString() + r.expectEnd() + } + return e + case 2: + return &SpoiledError{Message: f.message} + } + if f.code > 0 { + return &UnknownPantryError{code: f.code, Message: f.message} + } return f.err() } @@ -193,11 +397,11 @@ func wvKitchenErrorsError(f wvFailure) error { type Priority int32 const ( - // PriorityLow: Low priority + // Low priority PriorityLow Priority = 0 - // PriorityNormal: Normal priority + // Normal priority PriorityNormal Priority = 1 - // PriorityHigh: High priority + // High priority PriorityHigh Priority = 2 ) @@ -211,25 +415,25 @@ func wvReadPriority(r *wvReader) Priority { // Item: Kitchen sink struct exercising every field feature type Item struct { - // ID: Stable identifier + // Stable identifier ID int64 - // Name: Display name + // Display name Name string - // Count: Initial count + // Initial count Count int32 - // Enabled reports whether the item is enabled + // Whether the item is enabled Enabled bool - // Ratio: Scaling ratio + // Scaling ratio Ratio float64 Nick uint32 Payload []byte Tags []string Attrs map[string]string Parent *int64 - // Token: Cross-module struct reference + // Cross-module struct reference Token Token Priority Priority - // Gadget is an optional object inside a record + // An optional object inside a record Gadget *Gadget } @@ -241,12 +445,12 @@ func wvWriteItem(w *wvWriter, v Item) { w.writeF64(v.Ratio) w.writeU32(v.Nick) w.writeBytes(v.Payload) - wvWriteListString(w, v.Tags) - wvWriteMapStringString(w, v.Attrs) - wvWriteOptI64(w, v.Parent) + wvWrite_list_string(w, v.Tags) + wvWrite_map_string_string(w, v.Attrs) + wvWrite_opt_i64(w, v.Parent) wvWriteToken(w, v.Token) wvWritePriority(w, v.Priority) - wvWriteOptGadget(w, v.Gadget) + wvWrite_opt_Gadget(w, v.Gadget) } func wvReadItem(r *wvReader) Item { @@ -258,12 +462,12 @@ func wvReadItem(r *wvReader) Item { v.Ratio = r.readF64() v.Nick = r.readU32() v.Payload = r.readBytes() - v.Tags = wvReadListString(r) - v.Attrs = wvReadMapStringString(r) - v.Parent = wvReadOptI64(r) + v.Tags = wvRead_list_string(r) + v.Attrs = wvRead_map_string_string(r) + v.Parent = wvRead_opt_i64(r) v.Token = wvReadToken(r) v.Priority = wvReadPriority(r) - v.Gadget = wvReadOptGadget(r) + v.Gadget = wvRead_opt_Gadget(r) return v } @@ -271,22 +475,21 @@ func wvReadItem(r *wvReader) Item { // // Implement ReadyListener in Go and pass the value to the functions that take // it. The native library may call its methods from any thread until it -// releases the implementation. A method that panics, or a throwing method that -// returns an error outside its domain, fails the native call in progress as a -// callback failure (code -4). +// releases the implementation. A method that panics fails the native call in +// progress as a callback failure (code -4). type ReadyListener interface { - // OnReady fires when an item is ready + // Fires when an item is ready OnReady(code int32, msg string) - // OnItem receives the item itself and says whether to keep listening + // Receives the item itself and says whether to keep listening OnItem(item Item, gadget *Gadget) bool - // Label returns a display label, failing with a kitchen error + // A display label, failing with a kitchen error // - // Return a KitchenErrorsError to report that code, with its fields, to the - // native caller. + // Return a KitchenError to report one of its codes, with its fields, to the + // native caller; any other error reports code -1 with its text. Label() (string, error) - // Latest returns the most recent item the listener kept, if any + // The most recent item the listener kept, if any Latest() *Item - // Favorite returns the listener's favorite gadget + // The listener's favorite gadget Favorite() *Gadget } @@ -308,7 +511,7 @@ func goWv_kitchen_sink_kitchen_ReadyListener_label(ctx unsafe.Pointer, out_ptr * defer wvRecoverCallback(out_err) ret, err := wvCallback[ReadyListener](ctx).Label() if err != nil { - wvCallbackFailed[KitchenErrorsError](out_err, err) + wvCallbackFailed[KitchenError](out_err, err) return } wvHandOverString(ret, out_ptr, out_len) @@ -318,7 +521,7 @@ func goWv_kitchen_sink_kitchen_ReadyListener_label(ctx unsafe.Pointer, out_ptr * func goWv_kitchen_sink_kitchen_ReadyListener_latest(ctx unsafe.Pointer, out_ptr **C.uint8_t, out_len *C.size_t, out_err *C.kitchen_sink_error) { defer wvRecoverCallback(out_err) ret := wvCallback[ReadyListener](ctx).Latest() - wvHandOverBytes(wvEncode(ret, wvWriteOptItem), out_ptr, out_len) + wvHandOverBytes(wvEncode(ret, wvWrite_opt_Item), out_ptr, out_len) } //export goWv_kitchen_sink_kitchen_ReadyListener_favorite @@ -333,12 +536,69 @@ func goWv_kitchen_sink_kitchen_ReadyListener_free(ctx unsafe.Pointer) { wvFreeCallback(ctx) } -// Gadget is a pocket-sized interface exercising the object surface +// Sampler: Consumer-implemented source of scalar samples +// +// Implement Sampler in Go and pass the value to the functions that take it. +// The native library may call its methods from any thread until it releases +// the implementation. A method that panics fails the native call in progress +// as a callback failure (code -4). +type Sampler interface { + // The cutoff for a level, if the sampler has one + Threshold(level *uint32) *float64 + // Up to limit samples, failing when the pantry is empty + // + // Return a PantryError to report one of its codes, with its fields, to the + // native caller; any other error reports code -1 with its text. + Samples(limit *uint32, weights []float64) ([]int16, error) + // A free-form description, failing with an untyped error + // + // A returned error reaches the native caller as code -1 with its text. + Describe() (string, error) +} + +//export goWv_kitchen_sink_kitchen_Sampler_threshold +func goWv_kitchen_sink_kitchen_Sampler_threshold(ctx unsafe.Pointer, has_level C._Bool, level C.uint32_t, out_value *C.double, out_err *C.kitchen_sink_error) (cRet C._Bool) { + defer wvRecoverCallback(out_err) + ret := wvCallback[Sampler](ctx).Threshold(wvOptional(bool(has_level), uint32(level))) + cPresent, cValue := wvPresent(ret) + *out_value = C.double(cValue) + return C._Bool(cPresent) +} + +//export goWv_kitchen_sink_kitchen_Sampler_samples +func goWv_kitchen_sink_kitchen_Sampler_samples(ctx unsafe.Pointer, has_limit C._Bool, limit C.uint32_t, weights_ptr *C.double, weights_len C.size_t, out_ptr **C.int16_t, out_len *C.size_t, out_err *C.kitchen_sink_error) { + defer wvRecoverCallback(out_err) + ret, err := wvCallback[Sampler](ctx).Samples(wvOptional(bool(has_limit), uint32(limit)), wvBorrowSlice[float64](weights_ptr, weights_len)) + if err != nil { + wvCallbackFailed[PantryError](out_err, err) + return + } + wvHandOverSlice(ret, out_ptr, out_len) +} + +//export goWv_kitchen_sink_kitchen_Sampler_describe +func goWv_kitchen_sink_kitchen_Sampler_describe(ctx unsafe.Pointer, out_ptr **C.uint8_t, out_len *C.size_t, out_err *C.kitchen_sink_error) { + defer wvRecoverCallback(out_err) + ret, err := wvCallback[Sampler](ctx).Describe() + if err != nil { + wvCallbackError(out_err, err) + return + } + wvHandOverString(ret, out_ptr, out_len) +} + +//export goWv_kitchen_sink_kitchen_Sampler_free +func goWv_kitchen_sink_kitchen_Sampler_free(ctx unsafe.Pointer) { + wvFreeCallback(ctx) +} + +// Gadget: A pocket-sized interface exercising the object surface // -// Each wrapper holds one strong reference; Close releases it, and a finalizer -// releases it if the wrapper is garbage collected first. +// Each wrapper holds one strong reference to the object. Close releases it; a +// wrapper that's never closed releases it some time after it becomes +// unreachable. type Gadget struct { - ref wvRef + wvObject } func wvDestroyGadget(ptr unsafe.Pointer) { @@ -351,18 +611,17 @@ func wvAdoptGadget(ptr *C.kitchen_sink_kitchen_Gadget) *Gadget { return nil } s := &Gadget{} - s.ref.init(unsafe.Pointer(ptr), wvDestroyGadget) - runtime.SetFinalizer(s, (*Gadget).Close) + s.adopt(unsafe.Pointer(ptr), wvDestroyGadget) return s } // native borrows the wrapper's pointer for one call; pair it with a -// deferred s.ref.release(). +// deferred s.release(). func (s *Gadget) native() *C.kitchen_sink_kitchen_Gadget { if s == nil { - panic("kitchen_sink: nil *Gadget") + panic("kitchensink: nil *Gadget") } - return (*C.kitchen_sink_kitchen_Gadget)(s.ref.acquire("Gadget")) + return (*C.kitchen_sink_kitchen_Gadget)(s.acquire("Gadget")) } // share returns a new strong reference to the wrapper's object for the @@ -372,7 +631,7 @@ func (s *Gadget) share() *C.kitchen_sink_kitchen_Gadget { return nil } ptr := s.native() - defer s.ref.release() + defer s.release() return C.kitchen_sink_kitchen_Gadget_clone(ptr) } @@ -388,19 +647,9 @@ func wvReadGadget(r *wvReader) *Gadget { return wvAdoptGadget((*C.kitchen_sink_kitchen_Gadget)(C.wvHandlePtr(C.uintptr_t(token)))) } -// Close releases the wrapper's strong reference and always returns nil. -// It's idempotent and safe to call from any goroutine, even while a call -// on the wrapper is in flight: the reference is then released when that -// call returns. The object itself is dropped once its last reference, from -// any wrapper, record, or the native library, is gone. -func (s *Gadget) Close() error { - runtime.SetFinalizer(s, nil) - s.ref.close() - return nil -} - // NewGadget: Create a gadget with the given id func NewGadget(id int64) *Gadget { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_Gadget_new(C.int64_t(id), &cErr) wvTrap(&cErr) @@ -410,7 +659,7 @@ func NewGadget(id int64) *Gadget { // Describe: Render the gadget as a human-readable string func (s *Gadget) Describe() string { cSelf := s.native() - defer s.ref.release() + defer s.release() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_Gadget_describe(cSelf, &cRetLen, &cErr) @@ -421,17 +670,32 @@ func (s *Gadget) Describe() string { // Poke the gadget a number of times, failing on a negative count func (s *Gadget) Poke(times int32) (int32, error) { cSelf := s.native() - defer s.ref.release() + defer s.release() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_Gadget_poke(cSelf, C.int32_t(times), &cErr) if cErr.code != 0 { - return 0, wvKitchenErrorsError(wvTakeError(&cErr)) + return 0, wvKitchenError(wvTakeError(&cErr)) } return int32(cRet), nil } -// GadgetVersion returns the gadget subsystem version string +// Readings: The gadget's sensor readings, optionally limited +func (s *Gadget) Readings(limit *uint32) ([]float64, error) { + cSelf := s.native() + defer s.release() + cLimitHas, cLimit := wvPresent(limit) + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_Gadget_readings(cSelf, C._Bool(cLimitHas), C.uint32_t(cLimit), &cRetLen, &cErr) + if cErr.code != 0 { + return nil, wvTakeError(&cErr).err() + } + return wvTakeSlice[float64](cRet, cRetLen), nil +} + +// GadgetVersion: The gadget subsystem version string func GadgetVersion() string { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_Gadget_version(&cRetLen, &cErr) @@ -441,6 +705,7 @@ func GadgetVersion() string { // BoolID: Identity for bool func BoolID(x bool) bool { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_bool_id(C._Bool(x), &cErr) wvTrap(&cErr) @@ -448,6 +713,7 @@ func BoolID(x bool) bool { } func I32ID(x int32) int32 { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_i32_id(C.int32_t(x), &cErr) wvTrap(&cErr) @@ -455,6 +721,7 @@ func I32ID(x int32) int32 { } func U32ID(x uint32) uint32 { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_u32_id(C.uint32_t(x), &cErr) wvTrap(&cErr) @@ -462,6 +729,7 @@ func U32ID(x uint32) uint32 { } func I64ID(x int64) int64 { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_i64_id(C.int64_t(x), &cErr) wvTrap(&cErr) @@ -469,6 +737,7 @@ func I64ID(x int64) int64 { } func F64ID(x float64) float64 { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_f64_id(C.double(x), &cErr) wvTrap(&cErr) @@ -476,6 +745,7 @@ func F64ID(x float64) float64 { } func EchoString(s string) string { + wvLoaded() cSPtr, cSLen := wvStr(s) var cRetLen C.size_t var cErr C.kitchen_sink_error @@ -485,6 +755,7 @@ func EchoString(s string) string { } func EchoBytes(b []byte) []byte { + wvLoaded() cBPtr, cBLen := wvBytes(b) var cRetLen C.size_t var cErr C.kitchen_sink_error @@ -495,19 +766,17 @@ func EchoBytes(b []byte) []byte { // Subscribe: Register a listener, returning how many are attached func Subscribe(listener ReadyListener) int32 { + wvLoaded() var cErr C.kitchen_sink_error - cRet := C.kitchen_sink_kitchen_subscribe(wvNewCallback(listener), C.wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable(), &cErr) + cRet := C.kitchen_sink_kitchen_subscribe(wvNewCallback(listener, "ReadyListener"), C.wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable(), &cErr) wvTrap(&cErr) return int32(cRet) } // MaybeSubscribe: Register a listener if one is given func MaybeSubscribe(listener ReadyListener) bool { - var cListenerCtx unsafe.Pointer - var cListenerVtable *C.kitchen_sink_kitchen_ReadyListener_vtable - if listener != nil { - cListenerCtx, cListenerVtable = wvNewCallback(listener), C.wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable() - } + wvLoaded() + cListenerCtx, cListenerVtable := wvOptionalCallback(listener, C.wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable()) var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_maybe_subscribe(cListenerCtx, cListenerVtable, &cErr) wvTrap(&cErr) @@ -516,10 +785,11 @@ func MaybeSubscribe(listener ReadyListener) bool { // MaybeGadget: Optional object in both directions func MaybeGadget(fallback *Gadget) *Gadget { + wvLoaded() var cFallback *C.kitchen_sink_kitchen_Gadget if fallback != nil { cFallback = fallback.native() - defer fallback.ref.release() + defer fallback.release() } var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_maybe_gadget(cFallback, &cErr) @@ -527,99 +797,96 @@ func MaybeGadget(fallback *Gadget) *Gadget { return wvAdoptGadget(cRet) } -// Gadgets returns a list of objects +// Gadgets: A list of objects func Gadgets() []*Gadget { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_gadgets(&cRetLen, &cErr) wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadListGadget) + return wvDecode(cRet, cRetLen, wvRead_list_Gadget) } -// StreamGadgets returns an iterator over objects +// StreamGadgets: An iterator over objects // -// The sequence launches the native iterator each time it's ranged over and -// pulls one element per step; the iterator is released when the range ends, -// early or not. +// StreamGadgets returns a lazy sequence: each range over it launches the +// native iterator, pulls one element per step, and releases the iterator when +// the loop ends, early or not. func StreamGadgets() iter.Seq[*Gadget] { - return func(yield func(*Gadget) bool) { - var cErr C.kitchen_sink_error - cIter := C.kitchen_sink_kitchen_stream_gadgets(&cErr) - wvTrap(&cErr) - defer C.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(cIter) - for { - var cItem *C.kitchen_sink_kitchen_Gadget - var cIterErr C.kitchen_sink_error - more := C.kitchen_sink_kitchen_StreamGadgetsIterator_next(cIter, &cItem, &cIterErr) != 0 - wvTrap(&cIterErr) - if !more { - return - } - if !yield(wvAdoptGadget(cItem)) { - return - } + wvLoaded() + return wvSeq(func(cErr *C.kitchen_sink_error) wvCursor[*Gadget] { + it := C.kitchen_sink_kitchen_stream_gadgets(cErr) + return wvCursor[*Gadget]{ + next: func(cErr *C.kitchen_sink_error) (*Gadget, bool) { + var cItem *C.kitchen_sink_kitchen_Gadget + if C.kitchen_sink_kitchen_StreamGadgetsIterator_next(it, &cItem, cErr) == 0 { + return nil, false + } + return wvAdoptGadget(cItem), true + }, + destroy: func() { C.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(it) }, } - } + }) } // MaybeItem: Optional struct return func MaybeItem(id int64) (*Item, error) { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_maybe_item(C.int64_t(id), &cRetLen, &cErr) if cErr.code != 0 { - return nil, wvKitchenErrorsError(wvTakeError(&cErr)) + return nil, wvKitchenError(wvTakeError(&cErr)) } - return wvDecode(cRet, cRetLen, wvReadOptItem), nil + return wvDecode(cRet, cRetLen, wvRead_opt_Item), nil } // ListItems: List of structs func ListItems() []Item { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_list_items(&cRetLen, &cErr) wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadListItem) + return wvDecode(cRet, cRetLen, wvRead_list_Item) } // GetAttrs: Map return type func GetAttrs() map[string]int32 { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_get_attrs(&cRetLen, &cErr) wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadMapStringI32) + return wvDecode(cRet, cRetLen, wvRead_map_string_i32) } // StreamItems: Iterator return type // -// The sequence launches the native iterator each time it's ranged over and -// pulls one element per step; the iterator is released when the range ends, -// early or not. +// StreamItems returns a lazy sequence: each range over it launches the native +// iterator, pulls one element per step, and releases the iterator when the +// loop ends, early or not. func StreamItems() iter.Seq[Item] { - return func(yield func(Item) bool) { - var cErr C.kitchen_sink_error - cIter := C.kitchen_sink_kitchen_stream_items(&cErr) - wvTrap(&cErr) - defer C.kitchen_sink_kitchen_StreamItemsIterator_destroy(cIter) - for { - var cItem *C.uint8_t - var cItemLen C.size_t - var cIterErr C.kitchen_sink_error - more := C.kitchen_sink_kitchen_StreamItemsIterator_next(cIter, &cItem, &cItemLen, &cIterErr) != 0 - wvTrap(&cIterErr) - if !more { - return - } - if !yield(wvDecode(cItem, cItemLen, wvReadItem)) { - return - } + wvLoaded() + return wvSeq(func(cErr *C.kitchen_sink_error) wvCursor[Item] { + it := C.kitchen_sink_kitchen_stream_items(cErr) + return wvCursor[Item]{ + next: func(cErr *C.kitchen_sink_error) (Item, bool) { + var cItem *C.uint8_t + var cItemLen C.size_t + if C.kitchen_sink_kitchen_StreamItemsIterator_next(it, &cItem, &cItemLen, cErr) == 0 { + return Item{}, false + } + return wvDecode(cItem, cItemLen, wvReadItem), true + }, + destroy: func() { C.kitchen_sink_kitchen_StreamItemsIterator_destroy(it) }, } - } + }) } -// CrossModuleToken returns the shared Token type from another module +// CrossModuleToken: Returns the shared Token type from another module func CrossModuleToken() Token { + wvLoaded() var cRetLen C.size_t var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_cross_module_token(&cRetLen, &cErr) @@ -636,23 +903,17 @@ func goWv_kitchen_sink_kitchen_do_async_callback(context_ unsafe.Pointer, err_ * // DoAsync: Async operation // -// It blocks until the call completes or ctx is done; in the latter case it -// returns ctx.Err() at once and the native result is discarded. +// DoAsync blocks until the call completes or ctx is done; in the latter case +// it returns ctx.Err() at once and the native result is discarded. func DoAsync(ctx context.Context, input string) (string, error) { + wvLoaded() if err := ctx.Err(); err != nil { return "", err } cInputPtr, cInputLen := wvStr(input) call := wvNewAsyncCall[string]() C.kitchen_sink_kitchen_do_async(cInputPtr, cInputLen, C.kitchen_sink_kitchen_do_async_callback(unsafe.Pointer(C.goWv_kitchen_sink_kitchen_do_async_callback)), call.context()) - res, fail, err := call.wait(ctx, nil) - if err != nil { - return "", err - } - if fail != nil { - panic(fail.err()) - } - return res, nil + return wvAwait(ctx, call, nil, wvFailure.err) } //export goWv_kitchen_sink_kitchen_do_cancellable_callback @@ -664,9 +925,10 @@ func goWv_kitchen_sink_kitchen_do_cancellable_callback(context_ unsafe.Pointer, // DoCancellable: Cancellable async operation // -// It blocks until the call completes. Cancelling ctx cancels the native call, -// which then returns ctx.Err(). +// DoCancellable blocks until the call completes. Cancelling ctx cancels the +// native call, which then returns ctx.Err(). func DoCancellable(ctx context.Context, input string) (string, error) { + wvLoaded() if err := ctx.Err(); err != nil { return "", err } @@ -675,105 +937,328 @@ func DoCancellable(ctx context.Context, input string) (string, error) { defer C.kitchen_sink_cancel_token_destroy(token) call := wvNewAsyncCall[string]() C.kitchen_sink_kitchen_do_cancellable(cInputPtr, cInputLen, token, C.kitchen_sink_kitchen_do_cancellable_callback(unsafe.Pointer(C.goWv_kitchen_sink_kitchen_do_cancellable_callback)), call.context()) - res, fail, err := call.wait(ctx, token) - if err != nil { - return "", err - } - if fail != nil { - panic(fail.err()) - } - return res, nil + return wvAwait(ctx, call, token, wvFailure.err) } // LegacyOp: Legacy operation kept for compatibility // -// Deprecated: Use new_op instead +// Deprecated: Use [NewOp] instead func LegacyOp() int32 { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_legacy_op(&cErr) wvTrap(&cErr) return int32(cRet) } -// NewOp: Replacement for legacy_op +// NewOp: Replacement for [LegacyOp] func NewOp() int32 { + wvLoaded() var cErr C.kitchen_sink_error cRet := C.kitchen_sink_kitchen_new_op(&cErr) wvTrap(&cErr) return int32(cRet) } -// Hello: Trivial nested-module function -func Hello() string { +// OptI32ID: Optional scalar in both directions +func OptI32ID(x *int32) *int32 { + wvLoaded() + cXHas, cX := wvPresent(x) + var cOut C.int32_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_opt_i32_id(C._Bool(cXHas), C.int32_t(cX), &cOut, &cErr) + wvTrap(&cErr) + return wvOptional(bool(cRet), int32(cOut)) +} + +func OptBoolID(x *bool) *bool { + wvLoaded() + cXHas, cX := wvPresent(x) + var cOut C._Bool + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_opt_bool_id(C._Bool(cXHas), C._Bool(cX), &cOut, &cErr) + wvTrap(&cErr) + return wvOptional(bool(cRet), bool(cOut)) +} + +// OptPriority: Optional C-style enum in both directions +func OptPriority(p *Priority) *Priority { + wvLoaded() + cPHas, cP := wvPresent(p) + var cOut C.kitchen_sink_kitchen_Priority + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_opt_priority(C._Bool(cPHas), C.kitchen_sink_kitchen_Priority(cP), &cOut, &cErr) + wvTrap(&cErr) + return wvOptional(bool(cRet), Priority(cOut)) +} + +// OptMixed: Optional scalars next to ordinary parameters +func OptMixed(base float64, scale *float32, offset *int64, label *string) *uint16 { + wvLoaded() + cScaleHas, cScale := wvPresent(scale) + cOffsetHas, cOffset := wvPresent(offset) + cLabelPtr, cLabelLen := wvBytes(wvEncode(label, wvWrite_opt_string)) + var cOut C.uint16_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_opt_mixed(C.double(base), C._Bool(cScaleHas), C.float(cScale), C._Bool(cOffsetHas), C.int64_t(cOffset), cLabelPtr, cLabelLen, &cOut, &cErr) + wvTrap(&cErr) + return wvOptional(bool(cRet), uint16(cOut)) +} + +// SumF64: A typed-array parameter +func SumF64(xs []float64) float64 { + wvLoaded() + cXsPtr, cXsLen := wvSliceIn[C.double](xs) + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_sum_f64(cXsPtr, cXsLen, &cErr) + wvTrap(&cErr) + return float64(cRet) +} + +// ScaleI32: Typed arrays in both directions +func ScaleI32(xs []int32, factor int32) []int32 { + wvLoaded() + cXsPtr, cXsLen := wvSliceIn[C.int32_t](xs) var cRetLen C.size_t var cErr C.kitchen_sink_error - cRet := C.kitchen_sink_kitchen_nested_hello(&cRetLen, &cErr) + cRet := C.kitchen_sink_kitchen_scale_i32(cXsPtr, cXsLen, C.int32_t(factor), &cRetLen, &cErr) wvTrap(&cErr) - return wvTakeString(cRet, cRetLen) + return wvTakeSlice[int32](cRet, cRetLen) } -func wvWriteListGadget(w *wvWriter, v []*Gadget) { - wvWriteList(w, v, wvWriteGadget) +func IdsU64(ids []uint64) []uint64 { + wvLoaded() + cIdsPtr, cIdsLen := wvSliceIn[C.uint64_t](ids) + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_ids_u64(cIdsPtr, cIdsLen, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeSlice[uint64](cRet, cRetLen) +} + +// MixedSlices: Every slice element type +func MixedSlices(a []int8, b []int16, c []uint16, d []uint32, e []int64, f []float32) []uint32 { + wvLoaded() + cAPtr, cALen := wvSliceIn[C.int8_t](a) + cBPtr, cBLen := wvSliceIn[C.int16_t](b) + cCPtr, cCLen := wvSliceIn[C.uint16_t](c) + cDPtr, cDLen := wvSliceIn[C.uint32_t](d) + cEPtr, cELen := wvSliceIn[C.int64_t](e) + cFPtr, cFLen := wvSliceIn[C.float](f) + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_mixed_slices(cAPtr, cALen, cBPtr, cBLen, cCPtr, cCLen, cDPtr, cDLen, cEPtr, cELen, cFPtr, cFLen, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeSlice[uint32](cRet, cRetLen) } -func wvReadListGadget(r *wvReader) []*Gadget { - return wvReadList(r, wvReadGadget) +//export goWv_kitchen_sink_kitchen_async_maybe_count_callback +func goWv_kitchen_sink_kitchen_async_maybe_count_callback(context_ unsafe.Pointer, err_ *C.kitchen_sink_error, has_result C._Bool, result C.int64_t) { + wvComplete(context_, err_, func() *int64 { + return wvOptional(bool(has_result), int64(result)) + }) } -func wvWriteListItem(w *wvWriter, v []Item) { - wvWriteList(w, v, wvWriteItem) +// AsyncMaybeCount: Async returning an optional scalar +// +// AsyncMaybeCount blocks until the call completes or ctx is done; in the +// latter case it returns ctx.Err() at once and the native result is discarded. +func AsyncMaybeCount(ctx context.Context) (*int64, error) { + wvLoaded() + if err := ctx.Err(); err != nil { + return nil, err + } + call := wvNewAsyncCall[*int64]() + C.kitchen_sink_kitchen_async_maybe_count(C.kitchen_sink_kitchen_async_maybe_count_callback(unsafe.Pointer(C.goWv_kitchen_sink_kitchen_async_maybe_count_callback)), call.context()) + return wvAwait(ctx, call, nil, wvFailure.err) } -func wvReadListItem(r *wvReader) []Item { - return wvReadList(r, wvReadItem) +//export goWv_kitchen_sink_kitchen_async_samples_callback +func goWv_kitchen_sink_kitchen_async_samples_callback(context_ unsafe.Pointer, err_ *C.kitchen_sink_error, result_ptr *C.float, result_len C.size_t) { + wvComplete(context_, err_, func() []float32 { + return wvTakeSlice[float32](result_ptr, result_len) + }) } -func wvWriteListString(w *wvWriter, v []string) { - wvWriteList(w, v, (*wvWriter).writeString) +// AsyncSamples: Async returning a typed array +// +// AsyncSamples blocks until the call completes. Cancelling ctx cancels the +// native call, which then returns ctx.Err(). +func AsyncSamples(ctx context.Context, n uint32) ([]float32, error) { + wvLoaded() + if err := ctx.Err(); err != nil { + return nil, err + } + token := C.kitchen_sink_cancel_token_create() + defer C.kitchen_sink_cancel_token_destroy(token) + call := wvNewAsyncCall[[]float32]() + C.kitchen_sink_kitchen_async_samples(C.uint32_t(n), token, C.kitchen_sink_kitchen_async_samples_callback(unsafe.Pointer(C.goWv_kitchen_sink_kitchen_async_samples_callback)), call.context()) + return wvAwait(ctx, call, token, wvFailure.err) } -func wvReadListString(r *wvReader) []string { - return wvReadList(r, (*wvReader).readString) +// StreamMaybe: Iterator of optional scalars +// +// StreamMaybe returns a lazy sequence: each range over it launches the native +// iterator, pulls one element per step, and releases the iterator when the +// loop ends, early or not. +func StreamMaybe() iter.Seq[*int32] { + wvLoaded() + return wvSeq(func(cErr *C.kitchen_sink_error) wvCursor[*int32] { + it := C.kitchen_sink_kitchen_stream_maybe(cErr) + return wvCursor[*int32]{ + next: func(cErr *C.kitchen_sink_error) (*int32, bool) { + var cHas C._Bool + var cItem C.int32_t + if C.kitchen_sink_kitchen_StreamMaybeIterator_next(it, &cHas, &cItem, cErr) == 0 { + return nil, false + } + return wvOptional(bool(cHas), int32(cItem)), true + }, + destroy: func() { C.kitchen_sink_kitchen_StreamMaybeIterator_destroy(it) }, + } + }) } -func wvWriteMapStringI32(w *wvWriter, v map[string]int32) { - wvWriteMap(w, v, (*wvWriter).writeString, (*wvWriter).writeI32) +// StreamChunks: Iterator of typed arrays +// +// StreamChunks returns a lazy sequence: each range over it launches the native +// iterator, pulls one element per step, and releases the iterator when the +// loop ends, early or not. +func StreamChunks() iter.Seq[[]int32] { + wvLoaded() + return wvSeq(func(cErr *C.kitchen_sink_error) wvCursor[[]int32] { + it := C.kitchen_sink_kitchen_stream_chunks(cErr) + return wvCursor[[]int32]{ + next: func(cErr *C.kitchen_sink_error) ([]int32, bool) { + var cItem *C.int32_t + var cItemLen C.size_t + if C.kitchen_sink_kitchen_StreamChunksIterator_next(it, &cItem, &cItemLen, cErr) == 0 { + return nil, false + } + return wvTakeSlice[int32](cItem, cItemLen), true + }, + destroy: func() { C.kitchen_sink_kitchen_StreamChunksIterator_destroy(it) }, + } + }) } -func wvReadMapStringI32(r *wvReader) map[string]int32 { - return wvReadMap(r, (*wvReader).readString, (*wvReader).readI32) +// SampleWith: Drive a consumer-implemented sampler +func SampleWith(sampler Sampler, level *uint32) ([]int16, error) { + wvLoaded() + cLevelHas, cLevel := wvPresent(level) + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_sample_with(wvNewCallback(sampler, "Sampler"), C.wvVtablePtr_kitchen_sink_kitchen_Sampler_vtable(), C._Bool(cLevelHas), C.uint32_t(cLevel), &cRetLen, &cErr) + if cErr.code != 0 { + return nil, wvPantryError(wvTakeError(&cErr)) + } + return wvTakeSlice[int16](cRet, cRetLen), nil +} + +// TakeFromPantry: Fails with the second domain +func TakeFromPantry(item string) (int32, error) { + wvLoaded() + cItemPtr, cItemLen := wvStr(item) + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_take_from_pantry(cItemPtr, cItemLen, &cErr) + if cErr.code != 0 { + return 0, wvPantryError(wvTakeError(&cErr)) + } + return int32(cRet), nil +} + +// ParseAny: Fails with an untyped error +func ParseAny(text string) (int64, error) { + wvLoaded() + cTextPtr, cTextLen := wvStr(text) + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_parse_any(cTextPtr, cTextLen, &cErr) + if cErr.code != 0 { + return 0, wvTakeError(&cErr).err() + } + return int64(cRet), nil +} + +// Hello: Trivial nested-module function +func Hello() string { + wvLoaded() + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_nested_hello(&cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +func wvWrite_list_string(w *wvWriter, v []string) { + wvWriteList(w, v, (*wvWriter).writeString) +} + +func wvRead_list_string(r *wvReader) []string { + return wvReadList(r, (*wvReader).readString) } -func wvWriteMapStringString(w *wvWriter, v map[string]string) { +func wvWrite_map_string_string(w *wvWriter, v map[string]string) { wvWriteMap(w, v, (*wvWriter).writeString, (*wvWriter).writeString) } -func wvReadMapStringString(r *wvReader) map[string]string { +func wvRead_map_string_string(r *wvReader) map[string]string { return wvReadMap(r, (*wvReader).readString, (*wvReader).readString) } -func wvWriteOptGadget(w *wvWriter, v *Gadget) { +func wvWrite_opt_i64(w *wvWriter, v *int64) { + wvWriteOptional(w, v, (*wvWriter).writeI64) +} + +func wvRead_opt_i64(r *wvReader) *int64 { + return wvReadOptional(r, (*wvReader).readI64) +} + +func wvWrite_opt_Gadget(w *wvWriter, v *Gadget) { wvWriteNilable(w, v, v != nil, wvWriteGadget) } -func wvReadOptGadget(r *wvReader) *Gadget { +func wvRead_opt_Gadget(r *wvReader) *Gadget { return wvReadNilable(r, wvReadGadget) } -func wvWriteOptI64(w *wvWriter, v *int64) { - wvWriteOptional(w, v, (*wvWriter).writeI64) +func wvWrite_list_Gadget(w *wvWriter, v []*Gadget) { + wvWriteList(w, v, wvWriteGadget) } -func wvReadOptI64(r *wvReader) *int64 { - return wvReadOptional(r, (*wvReader).readI64) +func wvRead_list_Gadget(r *wvReader) []*Gadget { + return wvReadList(r, wvReadGadget) } -func wvWriteOptItem(w *wvWriter, v *Item) { +func wvWrite_opt_Item(w *wvWriter, v *Item) { wvWriteOptional(w, v, wvWriteItem) } -func wvReadOptItem(r *wvReader) *Item { +func wvRead_opt_Item(r *wvReader) *Item { return wvReadOptional(r, wvReadItem) } +func wvWrite_list_Item(w *wvWriter, v []Item) { + wvWriteList(w, v, wvWriteItem) +} + +func wvRead_list_Item(r *wvReader) []Item { + return wvReadList(r, wvReadItem) +} + +func wvWrite_map_string_i32(w *wvWriter, v map[string]int32) { + wvWriteMap(w, v, (*wvWriter).writeString, (*wvWriter).writeI32) +} + +func wvRead_map_string_i32(r *wvReader) map[string]int32 { + return wvReadMap(r, (*wvReader).readString, (*wvReader).readI32) +} + +func wvWrite_opt_string(w *wvWriter, v *string) { + wvWriteOptional(w, v, (*wvWriter).writeString) +} + +func wvRead_opt_string(r *wvReader) *string { + return wvReadOptional(r, (*wvReader).readString) +} + // END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__codec_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__codec_go.snap deleted file mode 100644 index bdeeb6a1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__codec_go.snap +++ /dev/null @@ -1,312 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package kitchen_sink - -/* -#include "kitchen_sink.h" -*/ -import "C" - -import ( - "encoding/binary" - "math" - "unicode/utf8" -) - -// wvEncode encodes v with write into a new value buffer. -func wvEncode[T any](v T, write func(*wvWriter, T)) []byte { - var w wvWriter - write(&w, v) - return w.buf -} - -// wvDecode decodes one value from a returned value buffer, checks that -// nothing trails it, and releases the native allocation. -func wvDecode[T any](ptr *C.uint8_t, n C.size_t, read func(*wvReader) T) T { - return wvDecodeBytes(wvTakeBytes(ptr, n), read) -} - -// wvDecodeBorrowed is wvDecode for a buffer the native library only lends -// (a callback argument). -func wvDecodeBorrowed[T any](ptr *C.uint8_t, n C.size_t, read func(*wvReader) T) T { - return wvDecodeBytes(wvBorrowBytes(ptr, n), read) -} - -func wvDecodeBytes[T any](b []byte, read func(*wvReader) T) T { - r := wvReader{buf: b} - v := read(&r) - r.expectEnd() - return v -} - -// wvWriteList writes a u32 count, then each element. -func wvWriteList[T any](w *wvWriter, v []T, elem func(*wvWriter, T)) { - w.writeLen(len(v)) - for _, e := range v { - elem(w, e) - } -} - -// wvReadList reads a u32 count, then that many elements. -func wvReadList[T any](r *wvReader, elem func(*wvReader) T) []T { - n := r.readLen() - v := make([]T, 0, r.capHint(n)) - for range n { - v = append(v, elem(r)) - } - return v -} - -// wvWriteMap writes a u32 count, then each key and value. Entry order -// carries no meaning. -func wvWriteMap[K comparable, V any](w *wvWriter, v map[K]V, key func(*wvWriter, K), val func(*wvWriter, V)) { - w.writeLen(len(v)) - for k, e := range v { - key(w, k) - val(w, e) - } -} - -// wvReadMap reads a u32 count, then that many keys and values. A repeated -// key is a malformed buffer. -func wvReadMap[K comparable, V any](r *wvReader, key func(*wvReader) K, val func(*wvReader) V) map[K]V { - n := r.readLen() - v := make(map[K]V, r.capHint(n)) - for range n { - k := key(r) - if _, dup := v[k]; dup { - wvMalformed("repeated map key") - } - v[k] = val(r) - } - return v -} - -// wvWriteOptional writes an optional held as a pointer: a presence flag, -// then the value when present. -func wvWriteOptional[T any](w *wvWriter, v *T, elem func(*wvWriter, T)) { - w.writeBool(v != nil) - if v != nil { - elem(w, *v) - } -} - -// wvReadOptional reads an optional held as a pointer. -func wvReadOptional[T any](r *wvReader, elem func(*wvReader) T) *T { - if !r.readOptionFlag() { - return nil - } - v := elem(r) - return &v -} - -// wvWriteNilable writes an optional whose Go type is already nil-able (a -// slice, map, interface, or wrapper pointer): present says it isn't nil. -func wvWriteNilable[T any](w *wvWriter, v T, present bool, elem func(*wvWriter, T)) { - w.writeBool(present) - if present { - elem(w, v) - } -} - -// wvReadNilable reads an optional whose Go type is already nil-able; absent -// is the zero value (nil). -func wvReadNilable[T any](r *wvReader, elem func(*wvReader) T) T { - if !r.readOptionFlag() { - var zero T - return zero - } - return elem(r) -} - -// wvWriter serializes values into the value-buffer format: little-endian, -// packed, u32 length prefixes. -type wvWriter struct { - buf []byte -} - -func (w *wvWriter) writeBool(v bool) { - if v { - w.buf = append(w.buf, 1) - } else { - w.buf = append(w.buf, 0) - } -} - -func (w *wvWriter) writeI8(v int8) { - w.buf = append(w.buf, byte(v)) -} - -func (w *wvWriter) writeU8(v uint8) { - w.buf = append(w.buf, v) -} - -func (w *wvWriter) writeI16(v int16) { - w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) -} - -func (w *wvWriter) writeU16(v uint16) { - w.buf = binary.LittleEndian.AppendUint16(w.buf, v) -} - -func (w *wvWriter) writeI32(v int32) { - w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) -} - -func (w *wvWriter) writeU32(v uint32) { - w.buf = binary.LittleEndian.AppendUint32(w.buf, v) -} - -func (w *wvWriter) writeI64(v int64) { - w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) -} - -func (w *wvWriter) writeU64(v uint64) { - w.buf = binary.LittleEndian.AppendUint64(w.buf, v) -} - -func (w *wvWriter) writeF32(v float32) { - w.writeU32(math.Float32bits(v)) -} - -func (w *wvWriter) writeF64(v float64) { - w.writeU64(math.Float64bits(v)) -} - -func (w *wvWriter) writeLen(n int) { - if n < 0 || uint64(n) > math.MaxUint32 { - panic("kitchen_sink: value-buffer length exceeds u32 range") - } - w.writeU32(uint32(n)) -} - -func (w *wvWriter) writeString(v string) { - w.writeLen(len(v)) - w.buf = append(w.buf, v...) -} - -func (w *wvWriter) writeBytes(v []byte) { - w.writeLen(len(v)) - w.buf = append(w.buf, v...) -} - -// wvReader decodes values from the value-buffer format. A malformed buffer -// is a producer/consumer contract violation, so every read panics instead of -// returning a typed domain error. -type wvReader struct { - buf []byte - pos int -} - -func wvMalformed(context string) { - panic("kitchen_sink: malformed value buffer: " + context) -} - -func (r *wvReader) take(n int, context string) []byte { - if n < 0 || len(r.buf)-r.pos < n { - wvMalformed(context) - } - b := r.buf[r.pos : r.pos+n] - r.pos += n - return b -} - -func (r *wvReader) readBool() bool { - switch r.take(1, "bool")[0] { - case 0: - return false - case 1: - return true - } - wvMalformed("bool byte out of range") - return false -} - -func (r *wvReader) readI8() int8 { - return int8(r.take(1, "i8")[0]) -} - -func (r *wvReader) readU8() uint8 { - return r.take(1, "u8")[0] -} - -func (r *wvReader) readI16() int16 { - return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) -} - -func (r *wvReader) readU16() uint16 { - return binary.LittleEndian.Uint16(r.take(2, "u16")) -} - -func (r *wvReader) readI32() int32 { - return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) -} - -func (r *wvReader) readU32() uint32 { - return binary.LittleEndian.Uint32(r.take(4, "u32")) -} - -func (r *wvReader) readI64() int64 { - return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) -} - -func (r *wvReader) readU64() uint64 { - return binary.LittleEndian.Uint64(r.take(8, "u64")) -} - -func (r *wvReader) readF32() float32 { - return math.Float32frombits(r.readU32()) -} - -func (r *wvReader) readF64() float64 { - return math.Float64frombits(r.readU64()) -} - -// readLen reads a u32 element count. Elements may encode to zero bytes, so -// the count isn't checked against the remaining buffer; callers cap their -// preallocation with capHint instead. -func (r *wvReader) readLen() int { - return int(r.readU32()) -} - -// capHint bounds a preallocation for n decoded elements by the bytes left in -// the buffer, so a corrupt count can't force a huge allocation. -func (r *wvReader) capHint(n int) int { - return min(n, len(r.buf)-r.pos) -} - -func (r *wvReader) readString() string { - b := r.take(r.readLen(), "string bytes") - if !utf8.Valid(b) { - wvMalformed("string is not valid UTF-8") - } - return string(b) -} - -func (r *wvReader) readBytes() []byte { - return append([]byte{}, r.take(r.readLen(), "byte buffer")...) -} - -func (r *wvReader) readOptionFlag() bool { - switch r.take(1, "option flag")[0] { - case 0: - return false - case 1: - return true - } - wvMalformed("option flag byte out of range") - return false -} - -func (r *wvReader) expectEnd() { - if r.pos != len(r.buf) { - wvMalformed("trailing bytes after value") - } -} - -// END codec.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap deleted file mode 100644 index 895b9e79..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -module kitchen_sink - -go 1.23 - -// END go.mod diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__runtime_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__runtime_go.snap deleted file mode 100644 index 8b78a350..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__runtime_go.snap +++ /dev/null @@ -1,426 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package kitchen_sink - -/* -#include -#include "kitchen_sink.h" - -static void* wvRuntimeHandlePtr(uintptr_t h) { return (void*)h; } -*/ -import "C" - -import ( - "cmp" - "context" - "errors" - "fmt" - "runtime/cgo" - "slices" - "sync/atomic" - "unsafe" -) - -// wvABIVersion is the C ABI revision these bindings were generated against. -const wvABIVersion uint32 = 4 - -// The runtime error codes this package produces or interprets itself. -const ( - wvCodeForeign int32 = -4 - wvCodeCancelled int32 = -5 -) - -// Error is a failure no error domain claims: a generic failure (code -1), a -// panic in the native library (-2), an argument or result that couldn't be -// marshalled (-3), a callback implementation that failed (-4), or a code -// outside the domain. A call that doesn't declare errors panics with an -// *Error, since its failure is a bug; a call that does returns one. -type Error struct { - // Code is the runtime code. - Code int32 - // Message is the native library's message. - Message string -} - -func (e *Error) Error() string { - return fmt.Sprintf("kitchen_sink: %s (code %d)", e.Message, e.Code) -} - -// DebugLive reports how many resources of one kind the native library holds: -// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 byte runs. Kind -1 -// reports 1 when the library counts at all. Every count is 0 unless the -// library was built with leak checks enabled; tests use it to assert that -// nothing leaked. -func DebugLive(kind int32) uint64 { - return uint64(C.kitchen_sink_debug_live(C.int32_t(kind))) -} - -// ── Load-time checks ── - -// wvCheckABI panics when the linked library implements a different C ABI -// revision than these bindings were generated for. -func wvCheckABI() { - if found := uint32(C.kitchen_sink_abi_version()); found != wvABIVersion { - panic(fmt.Errorf("kitchen_sink: the linked library implements C ABI revision %d, but these bindings were generated for revision %d", found, wvABIVersion)) - } -} - -// wvContractEntry is one declaration these bindings were generated with: -// the FNV-1a hashes of its dotted path and of its signature. -type wvContractEntry struct { - id, hash uint64 - path string -} - -// wvCheckContract panics unless every entry in want is in the library's -// contract table for one top-level module (n entries sorted by id) with an -// equal signature hash. Entries only the library has are fine. -func wvCheckContract(table *C.kitchen_sink_contract_entry, n C.size_t, want []wvContractEntry) { - have := unsafe.Slice(table, int(n)) - for _, w := range want { - i, found := slices.BinarySearchFunc(have, w.id, func(e C.kitchen_sink_contract_entry, id uint64) int { - return cmp.Compare(uint64(e.id), id) - }) - if !found { - panic(fmt.Errorf("kitchen_sink: %s is missing from the library", w.path)) - } - if uint64(have[i].hash) != w.hash { - panic(fmt.Errorf("kitchen_sink: %s changed since these bindings were generated", w.path)) - } - } -} - -// ── Errors ── - -// wvFailure is a non-zero error slot copied into Go memory. -type wvFailure struct { - code int32 - message string - payload []byte -} - -// err converts the failure into the generic *Error. -func (f wvFailure) err() error { - return &Error{Code: f.code, Message: f.message} -} - -func wvCopyFailure(cErr *C.kitchen_sink_error) wvFailure { - f := wvFailure{code: int32(cErr.code)} - if cErr.message != nil { - f.message = C.GoString(cErr.message) - } - if cErr.payload_ptr != nil { - f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) - } - return f -} - -// wvTakeError copies a non-zero error slot and clears it. -func wvTakeError(cErr *C.kitchen_sink_error) wvFailure { - f := wvCopyFailure(cErr) - C.kitchen_sink_error_clear(cErr) - return f -} - -// wvTakeBoxedError copies the heap-boxed error an async completion receives -// and frees the box. -func wvTakeBoxedError(cErr *C.kitchen_sink_error) wvFailure { - f := wvCopyFailure(cErr) - C.kitchen_sink_error_free(cErr) - return f -} - -// wvTrap panics with an *Error when the slot reports a failure. Wrappers of -// calls that declare no errors check their slot with it: a failure there is -// a bug in the native library, an argument it couldn't take, or a callback -// failure it let through. -func wvTrap(cErr *C.kitchen_sink_error) { - if cErr.code != 0 { - panic(wvTakeError(cErr).err()) - } -} - -// wvDomainError is implemented by every error-code type: its code and its -// fields encoded as a value buffer (nil when it has none). -type wvDomainError interface { - error - Code() int32 - wvPayload() []byte -} - -// wvSetError reports a failure through a callback's error slot. The native -// library copies the message and payload, so both are only borrowed. -func wvSetError(outErr *C.kitchen_sink_error, code int32, message string, payload []byte) { - msg := C.CString(message) - defer C.free(unsafe.Pointer(msg)) - C.kitchen_sink_error_set(outErr, C.int32_t(code), msg) - if len(payload) > 0 { - ptr, n := wvBytes(payload) - C.kitchen_sink_error_set_payload(outErr, ptr, n) - } -} - -// wvCallbackFailed reports the error a throwing callback method returned: -// an error of the domain D in scope for the method reports its code and -// payload, and anything else is a callback failure (code -4). -func wvCallbackFailed[D error](outErr *C.kitchen_sink_error, err error) { - var domain D - if errors.As(err, &domain) { - if d, ok := any(domain).(wvDomainError); ok { - wvSetError(outErr, d.Code(), d.Error(), d.wvPayload()) - return - } - } - wvSetError(outErr, wvCodeForeign, err.Error(), nil) -} - -// wvRecoverCallback, deferred by every callback trampoline, reports a panic -// in the implementation as a callback failure (code -4) instead of letting -// it unwind into the native caller. -func wvRecoverCallback(outErr *C.kitchen_sink_error) { - if r := recover(); r != nil { - msg := fmt.Sprint(r) - if err, ok := r.(error); ok { - msg = err.Error() - } - wvSetError(outErr, wvCodeForeign, msg, nil) - } -} - -// ── Callback contexts ── - -// wvNewCallback keeps impl alive in a handle table for as long as the -// native library holds it and returns the handle as the callback's context -// pointer; the vtable's free entry deletes it (see wvFreeCallback). A nil -// implementation of a required callback panics. -func wvNewCallback(impl any) unsafe.Pointer { - if impl == nil { - panic("kitchen_sink: nil callback implementation") - } - return C.wvRuntimeHandlePtr(C.uintptr_t(cgo.NewHandle(impl))) -} - -// wvCallback recovers the implementation behind a callback context. -func wvCallback[T any](ctx unsafe.Pointer) T { - return cgo.Handle(uintptr(ctx)).Value().(T) -} - -// wvFreeCallback releases the implementation behind a callback context once -// the native library is done with it. -func wvFreeCallback(ctx unsafe.Pointer) { - cgo.Handle(uintptr(ctx)).Delete() -} - -// ── Strings and bytes ── - -// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the -// duration of one call. Strings may contain NUL bytes. -func wvStr(s string) (*C.uint8_t, C.size_t) { - return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) -} - -// wvBytes returns a borrowed (pointer, length) view of b for the duration of -// one call. -func wvBytes(b []byte) (*C.uint8_t, C.size_t) { - return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) -} - -// wvBorrowString copies a borrowed UTF-8 run into a Go string. -func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { - if ptr == nil { - return "" - } - return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) -} - -// wvTakeString copies a returned UTF-8 run into a Go string and releases the -// native allocation. -func wvTakeString(ptr *C.uint8_t, n C.size_t) string { - s := wvBorrowString(ptr, n) - C.kitchen_sink_free_bytes(ptr, n) - return s -} - -// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer -// copies to an empty slice. -func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { - if ptr == nil { - return []byte{} - } - return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) -} - -// wvTakeBytes copies a returned byte run into Go memory and releases the -// native allocation. -func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { - b := wvBorrowBytes(ptr, n) - C.kitchen_sink_free_bytes(ptr, n) - return b -} - -// wvHandOverString copies s into a run allocated with the native library's -// allocator and writes it to a callback's return slots; the library adopts -// the run. -func wvHandOverString(s string, outPtr **C.uint8_t, outLen *C.size_t) { - ptr, n := wvStr(s) - wvHandOver(ptr, n, outPtr, outLen) -} - -// wvHandOverBytes is wvHandOverString for a byte slice (raw bytes or an -// encoded value buffer). -func wvHandOverBytes(b []byte, outPtr **C.uint8_t, outLen *C.size_t) { - ptr, n := wvBytes(b) - wvHandOver(ptr, n, outPtr, outLen) -} - -func wvHandOver(ptr *C.uint8_t, n C.size_t, outPtr **C.uint8_t, outLen *C.size_t) { - run := C.kitchen_sink_alloc(n) - if n > 0 { - copy(unsafe.Slice((*byte)(unsafe.Pointer(run)), n), unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) - } - *outPtr, *outLen = run, n -} - -// ── Objects ── - -// wvRef owns one strong reference to a native object on behalf of a wrapper. -// Calls borrow the pointer between acquire and release. Close may race them: -// the reference is destroyed exactly once, by close when no call is in -// flight, or else by the last in-flight call to finish. -type wvRef struct { - ptr unsafe.Pointer - destroy func(unsafe.Pointer) - // state counts in-flight calls in its upper bits; the low bit is set - // once close has run. - state atomic.Uint64 -} - -func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { - r.ptr = ptr - r.destroy = destroy -} - -// acquire borrows the pointer for one call, panicking when the wrapper was -// already closed. -func (r *wvRef) acquire(typeName string) unsafe.Pointer { - for { - s := r.state.Load() - if s&1 != 0 { - panic("kitchen_sink: " + typeName + " used after Close") - } - if r.state.CompareAndSwap(s, s+2) { - return r.ptr - } - } -} - -// release ends one call's borrow. -func (r *wvRef) release() { - if r.state.Add(^uint64(1)) == 1 { - r.destroy(r.ptr) - } -} - -// close releases the wrapper's reference once no call is in flight. Calling -// it again is a no-op. -func (r *wvRef) close() { - for { - s := r.state.Load() - if s&1 != 0 { - return - } - if r.state.CompareAndSwap(s, s|1) { - if s == 0 { - r.destroy(r.ptr) - } - return - } - } -} - -// ── Async ── - -// wvOutcome is one async completion: a converted result, or the failure the -// native library reported. -type wvOutcome[T any] struct { - val T - fail *wvFailure -} - -// wvAsyncCall is one pending async call: the channel its completion is -// delivered on, kept alive in a handle table while the call is in flight. -type wvAsyncCall[T any] struct { - done chan wvOutcome[T] - handle cgo.Handle -} - -func wvNewAsyncCall[T any]() *wvAsyncCall[T] { - c := &wvAsyncCall[T]{done: make(chan wvOutcome[T], 1)} - c.handle = cgo.NewHandle(c.done) - return c -} - -// context is the completion's context pointer. -func (c *wvAsyncCall[T]) context() unsafe.Pointer { - return C.wvRuntimeHandlePtr(C.uintptr_t(c.handle)) -} - -// wvComplete delivers one async completion to the waiting wrapper. It runs on -// a native thread inside an exported trampoline, so it never panics: a result -// that fails to decode is delivered as a marshalling failure. -func wvComplete[T any](context unsafe.Pointer, cErr *C.kitchen_sink_error, convert func() T) { - h := cgo.Handle(uintptr(context)) - done := h.Value().(chan wvOutcome[T]) - h.Delete() - done <- wvSettle(cErr, convert) -} - -func wvSettle[T any](cErr *C.kitchen_sink_error, convert func() T) (o wvOutcome[T]) { - if cErr != nil { - if f := wvTakeBoxedError(cErr); f.code != 0 { - o.fail = &f - return o - } - } - defer func() { - if r := recover(); r != nil { - o = wvOutcome[T]{fail: &wvFailure{code: -3, message: fmt.Sprint(r)}} - } - }() - o.val = convert() - return o -} - -// wait blocks until the call completes or ctx is done. With a cancel token, -// ctx's cancellation cancels the native call and waits for its completion, -// which the native library delivers promptly; without one, the call is -// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) -// returns ctx.Err(), or context.Canceled when ctx itself isn't done. -func (c *wvAsyncCall[T]) wait(ctx context.Context, token *C.kitchen_sink_cancel_token) (T, *wvFailure, error) { - var o wvOutcome[T] - select { - case o = <-c.done: - case <-ctx.Done(): - if token == nil { - var zero T - return zero, nil, ctx.Err() - } - C.kitchen_sink_cancel_token_cancel(token) - o = <-c.done - } - if o.fail != nil && o.fail.code == wvCodeCancelled { - var zero T - if err := ctx.Err(); err != nil { - return zero, nil, err - } - return zero, nil, context.Canceled - } - return o.val, o.fail, nil -} - -// END runtime.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__bindings_go.snap deleted file mode 100644 index 38d54ed4..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__bindings_go.snap +++ /dev/null @@ -1,192 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// Package nested_modules binds the nested_modules native library. -package nested_modules - -/* -#cgo LDFLAGS: -lnested_modules -#include "nested_modules.h" -*/ -import "C" - -func init() { - wvCheckABI() - var n C.size_t - table := C.nested_modules_graphics_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0x2c29eef9327a6bd4, 0x12983d5271651389, "graphics.Unit"}, - {0x304088bbc28f0015, 0x97e6e809913be6b6, "graphics.new_canvas"}, - {0x49f8837532df9ac6, 0x32e5b27e81ee0957, "graphics.Canvas"}, - {0x55717fa027336b47, 0xb2bd3a6bdcce48c8, "graphics.shapes.describe"}, - {0x7626b91b8f1f53cd, 0x867bbf7cf0f41c74, "graphics.shapes.geometry.compute"}, - {0xb3efcb50d9a61ab7, 0x3cb0207dfad2a149, "graphics.GraphicsError"}, - {0xc62b16a9591b744b, 0x7895610c8487eb1b, "graphics.shapes.Shape"}, - {0xe13001f4983ccd35, 0x16e7de070b4a4cfc, "graphics.shapes.geometry.Detail"}, - {0xf3f658bcfc5dba62, 0x2665ccb47356e746, "graphics.shapes.make_shape"}, - }) -} - -// GraphicsError is an error the graphics module reports: *OutOfBoundsError. -// -// Match one code with errors.As and its type, or any of them with a -// GraphicsError. -type GraphicsError interface { - error - // Code returns the error's numeric code. - Code() int32 - isGraphicsError() -} - -// OutOfBoundsError is the GraphicsError code OutOfBounds (1): coordinate out -// of bounds. -type OutOfBoundsError struct { - // Message describes the failure; when it's empty, Error returns the - // code's default message, "coordinate out of bounds". - Message string -} - -// Error returns the error's message. -func (e *OutOfBoundsError) Error() string { - if e.Message == "" { - return "coordinate out of bounds" - } - return e.Message -} - -// Code returns 1. -func (*OutOfBoundsError) Code() int32 { - return 1 -} - -func (*OutOfBoundsError) isGraphicsError() {} - -func (*OutOfBoundsError) wvPayload() []byte { - return nil -} - -func wvGraphicsError(f wvFailure) error { - switch f.code { - case 1: - return &OutOfBoundsError{Message: f.message} - } - return f.err() -} - -// Unit is a measurement unit shared with nested submodules. -type Unit int32 - -const ( - UnitPixels Unit = 0 - UnitPoints Unit = 1 -) - -func wvWriteUnit(w *wvWriter, v Unit) { - w.writeI32(int32(v)) -} - -func wvReadUnit(r *wvReader) Unit { - return Unit(r.readI32()) -} - -// Canvas: Top-level struct that references a struct defined one level deeper. -type Canvas struct { - Width int32 - // Root: Reference down into a nested module - Root Shape -} - -func wvWriteCanvas(w *wvWriter, v Canvas) { - w.writeI32(v.Width) - wvWriteShape(w, v.Root) -} - -func wvReadCanvas(r *wvReader) Canvas { - var v Canvas - v.Width = r.readI32() - v.Root = wvReadShape(r) - return v -} - -// NewCanvas: Construct a Canvas. -func NewCanvas(width int32) Canvas { - var cRetLen C.size_t - var cErr C.nested_modules_error - cRet := C.nested_modules_graphics_new_canvas(C.int32_t(width), &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadCanvas) -} - -// Shape is a struct one level deep that references both an ancestor enum and a deeper struct. -type Shape struct { - Sides int32 - // Unit: Reference up to the parent module's enum - Unit Unit - // Detail: Reference down into a deeper module - Detail Detail -} - -func wvWriteShape(w *wvWriter, v Shape) { - w.writeI32(v.Sides) - wvWriteUnit(w, v.Unit) - wvWriteDetail(w, v.Detail) -} - -func wvReadShape(r *wvReader) Shape { - var v Shape - v.Sides = r.readI32() - v.Unit = wvReadUnit(r) - v.Detail = wvReadDetail(r) - return v -} - -// MakeShape: Construct a Shape. -func MakeShape(sides int32) Shape { - var cRetLen C.size_t - var cErr C.nested_modules_error - cRet := C.nested_modules_graphics_shapes_make_shape(C.int32_t(sides), &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadShape) -} - -// Describe: Nested function returning a struct owned by an ancestor module. -func Describe() Canvas { - var cRetLen C.size_t - var cErr C.nested_modules_error - cRet := C.nested_modules_graphics_shapes_describe(&cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadCanvas) -} - -// Detail is a struct two levels deep. -type Detail struct { - Area float64 -} - -func wvWriteDetail(w *wvWriter, v Detail) { - w.writeF64(v.Area) -} - -func wvReadDetail(r *wvReader) Detail { - var v Detail - v.Area = r.readF64() - return v -} - -// Compute: Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. -func Compute(shape Shape) (Detail, error) { - cShapePtr, cShapeLen := wvBytes(wvEncode(shape, wvWriteShape)) - var cRetLen C.size_t - var cErr C.nested_modules_error - cRet := C.nested_modules_graphics_shapes_geometry_compute(cShapePtr, cShapeLen, &cRetLen, &cErr) - if cErr.code != 0 { - return Detail{}, wvGraphicsError(wvTakeError(&cErr)) - } - return wvDecode(cRet, cRetLen, wvReadDetail), nil -} - -// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__bindings_go.snap deleted file mode 100644 index 61c62ec8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_shapes__bindings_go.snap +++ /dev/null @@ -1,221 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// Package shapes binds the shapes native library. -package shapes - -/* -#cgo LDFLAGS: -lshapes -#include "shapes.h" -*/ -import "C" - -func init() { - wvCheckABI() - var n C.size_t - table := C.shapes_shapes_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0x0d7b3eec49243e7a, 0x5f232311808e1b1f, "shapes.Channel"}, - {0x183fa8198100860e, 0xe40c6993cb31d359, "shapes.describe"}, - {0x2d1395eb837b848e, 0x42202a0c12f3a16e, "shapes.sum_bytes"}, - {0x7f9a5b87ba09b86b, 0xc5b32fb0efbbaa17, "shapes.scale"}, - {0xc8edd779240fda17, 0x68e1be78fddb20e4, "shapes.Numerics"}, - {0xd3d3881b2fd18a62, 0xf24d0eb76a99d918, "shapes.make_numerics"}, - {0xf010844afff5ac3c, 0x50de28ae9b41ff15, "shapes.Shape"}, - }) -} - -// Shape is an algebraic shape (sum type with associated data) -// -// Shape is a sealed sum type: exactly one of its variant structs is the value -// at a time. -type Shape interface { - isShape() -} - -// ShapeEmpty: The empty shape (a unit variant) -type ShapeEmpty struct{} - -func (ShapeEmpty) isShape() {} - -// ShapeCircle: A circle with a radius -type ShapeCircle struct { - // Radius in points - Radius float64 -} - -func (ShapeCircle) isShape() {} - -// ShapeRectangle: An axis-aligned rectangle -type ShapeRectangle struct { - Width float32 - Height float32 -} - -func (ShapeRectangle) isShape() {} - -// ShapeBounded: An integer-bounded shape (exercises narrow + wide ints) -type ShapeBounded struct { - Min int16 - Max int16 - Weight uint64 -} - -func (ShapeBounded) isShape() {} - -// ShapeLabeled: A labeled shape with a small count -type ShapeLabeled struct { - Label string - Count uint8 -} - -func (ShapeLabeled) isShape() {} - -func wvWriteShape(w *wvWriter, v Shape) { - switch x := v.(type) { - case ShapeEmpty: - w.writeI32(0) - case ShapeCircle: - w.writeI32(1) - w.writeF64(x.Radius) - case ShapeRectangle: - w.writeI32(2) - w.writeF32(x.Width) - w.writeF32(x.Height) - case ShapeBounded: - w.writeI32(3) - w.writeI16(x.Min) - w.writeI16(x.Max) - w.writeU64(x.Weight) - case ShapeLabeled: - w.writeI32(4) - w.writeString(x.Label) - w.writeU8(x.Count) - default: - panic("shapes: Shape value is not one of its variants") - } -} - -func wvReadShape(r *wvReader) Shape { - switch r.readI32() { - case 0: - return ShapeEmpty{} - case 1: - var x ShapeCircle - x.Radius = r.readF64() - return x - case 2: - var x ShapeRectangle - x.Width = r.readF32() - x.Height = r.readF32() - return x - case 3: - var x ShapeBounded - x.Min = r.readI16() - x.Max = r.readI16() - x.Weight = r.readU64() - return x - case 4: - var x ShapeLabeled - x.Label = r.readString() - x.Count = r.readU8() - return x - } - wvMalformed("Shape tag out of range") - return nil -} - -// Channel is a plain C-style enum (no payloads) -type Channel int32 - -const ( - ChannelRed Channel = 0 - ChannelGreen Channel = 1 - ChannelBlue Channel = 2 -) - -// Numerics: One field per supported numeric primitive -type Numerics struct { - A int8 - B int16 - C int32 - D int64 - E uint8 - F uint16 - G uint32 - H uint64 - I float32 - J float64 -} - -func wvWriteNumerics(w *wvWriter, v Numerics) { - w.writeI8(v.A) - w.writeI16(v.B) - w.writeI32(v.C) - w.writeI64(v.D) - w.writeU8(v.E) - w.writeU16(v.F) - w.writeU32(v.G) - w.writeU64(v.H) - w.writeF32(v.I) - w.writeF64(v.J) -} - -func wvReadNumerics(r *wvReader) Numerics { - var v Numerics - v.A = r.readI8() - v.B = r.readI16() - v.C = r.readI32() - v.D = r.readI64() - v.E = r.readU8() - v.F = r.readU16() - v.G = r.readU32() - v.H = r.readU64() - v.I = r.readF32() - v.J = r.readF64() - return v -} - -// Describe: Render a shape to a string -func Describe(shape Shape) string { - cShapePtr, cShapeLen := wvBytes(wvEncode(shape, wvWriteShape)) - var cRetLen C.size_t - var cErr C.shapes_error - cRet := C.shapes_shapes_describe(cShapePtr, cShapeLen, &cRetLen, &cErr) - wvTrap(&cErr) - return wvTakeString(cRet, cRetLen) -} - -// Scale a shape by a factor, returning a new shape -func Scale(shape Shape, factor float64) Shape { - cShapePtr, cShapeLen := wvBytes(wvEncode(shape, wvWriteShape)) - var cRetLen C.size_t - var cErr C.shapes_error - cRet := C.shapes_shapes_scale(cShapePtr, cShapeLen, C.double(factor), &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadShape) -} - -// SumBytes: Sum a list of bytes into a wide integer (numerics smoke) -func SumBytes(values []byte) uint64 { - cValuesPtr, cValuesLen := wvBytes(values) - var cErr C.shapes_error - cRet := C.shapes_shapes_sum_bytes(cValuesPtr, cValuesLen, &cErr) - wvTrap(&cErr) - return uint64(cRet) -} - -// MakeNumerics: Build a Numerics struct from every numeric primitive -func MakeNumerics(a int8, b int16, c int32, d int64, e uint8, f uint16, g uint32, h uint64, i float32, j float64) Numerics { - var cRetLen C.size_t - var cErr C.shapes_error - cRet := C.shapes_shapes_make_numerics(C.int8_t(a), C.int16_t(b), C.int32_t(c), C.int64_t(d), C.uint8_t(e), C.uint16_t(f), C.uint32_t(g), C.uint64_t(h), C.float(i), C.double(j), &cRetLen, &cErr) - wvTrap(&cErr) - return wvDecode(cRet, cRetLen, wvReadNumerics) -} - -// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_jni_c.snap deleted file mode 100644 index 3b5e714b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_jni_c.snap +++ /dev/null @@ -1,614 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#include -#include -#include -#include -#include -#include -#include - -/* The shim forwards deprecated functions too, so it doesn't warn about them. */ -#define DOCS_EVERYWHERE_DEPRECATED(msg) -#include "docs_everywhere.h" - -/* Every static name in this file starts with `Jni_`, which no C ABI symbol - (all lowercase `docs_everywhere_...`) and no JNI export (`Java_...`) can - spell. */ - -static JavaVM* Jni_vm = NULL; -static jclass Jni_bridge = NULL; -static jmethodID Jni_error = NULL; - -/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value - buffers all cross as one. */ -typedef struct { - jbyteArray array; - jbyte* elems; - const uint8_t* ptr; - size_t len; -} Jni_bytes; - -static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { - Jni_bytes b = {array, NULL, NULL, 0}; - if (array == NULL) { - return b; - } - b.len = (size_t)(*env)->GetArrayLength(env, array); - b.elems = (*env)->GetByteArrayElements(env, array, NULL); - b.ptr = (const uint8_t*)b.elems; - if (b.elems == NULL) { - b.len = 0; - } - return b; -} - -static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { - if (b->elems != NULL) { - (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); - } -} - -/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid - input, so NULL + 0 is the empty array). */ -static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = (*env)->NewByteArray(env, (jsize)len); - if (out != NULL && len > 0 && ptr != NULL) { - (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); - } - return out; -} - -/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ -static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = Jni_new_bytes(env, ptr, len); - if (ptr != NULL) { - docs_everywhere_free_bytes((uint8_t*)ptr, len); - } - return out; -} - -/* Throws the Kotlin exception for `err` (mapped through error domain - `domain`, or 0 for a call that can't fail, which raises the trap) and - clears `err`. */ -static inline void Jni_throw(JNIEnv* env, docs_everywhere_error* err, jint domain) { - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; - if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); - } - jint code = (jint)err->code; - docs_everywhere_error_clear(err); - if ((*env)->ExceptionCheck(env)) { - return; - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { - (*env)->Throw(env, ex); - } -} - -static jint Jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { - (*env)->ThrowNew(env, cls, message); - } - return JNI_ERR; -} - -/* One contract entry these bindings were generated with: a declaration's - id and signature hash, plus its dotted path for the load error. */ -typedef struct { - uint64_t id; - uint64_t hash; - const char* path; -} Jni_contract_entry; - -/* Checks that the library's contract table (from `contract`, sorted by id) - has every expected entry with an equal hash; entries only the library - has are fine. Fails loading with an error naming the first declaration - that's missing or changed. */ -static inline jint Jni_check_contract(JNIEnv* env, const docs_everywhere_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { - size_t len = 0; - const docs_everywhere_contract_entry* table = contract(&len); - if (table == NULL) { - len = 0; - } - for (size_t i = 0; i < count; i++) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) { - lo = mid + 1; - } else { - hi = mid; - } - } - const char* problem = NULL; - if (lo == len || table[lo].id != expected[i].id) { - problem = "is missing from the library"; - } else if (table[lo].hash != expected[i].hash) { - problem = "changed since these bindings were generated"; - } - if (problem != NULL) { - char message[512]; - snprintf(message, sizeof message, "%s: %s %s", "docs_everywhere", expected[i].path, problem); - return Jni_load_error(env, message); - } - } - return JNI_OK; -} - -/* Generated below: the contract checks and the method IDs the shim caches. */ -static jint Jni_load(JNIEnv* env); - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env = NULL; - (void)reserved; - Jni_vm = vm; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - if (docs_everywhere_abi_version() != 4u) { - return Jni_load_error(env, "docs_everywhere: the native library implements a different C ABI revision than these bindings (expected 4)"); - } - jclass bridge = (*env)->FindClass(env, "docs_everywhere/JniBridge"); - if (bridge == NULL) { - return JNI_ERR; - } - Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); - Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ljava/lang/Throwable;"); - if (Jni_error == NULL) { - return JNI_ERR; - } - if (Jni_load(env) != JNI_OK) { - return JNI_ERR; - } - return JNI_VERSION_1_6; -} - -JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { - (void)env; - (void)cls; - return (jlong)docs_everywhere_debug_live((int32_t)kind); -} - -/* Producer threads (async completions, callback calls, vtable `free`) are - usually not JVM threads. The first time one calls in it attaches as a - daemon, and a thread-local destructor (a pthread key, or a fiber-local - slot on Windows) detaches it when the thread exits, so a busy producer - thread pays for one attach, not one per call. If the destructor can't be - registered, the thread detaches again as soon as the call ends. */ -#if defined(_WIN32) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include - -static DWORD Jni_env_slot = FLS_OUT_OF_INDEXES; -static INIT_ONCE Jni_env_once = INIT_ONCE_STATIC_INIT; - -static VOID WINAPI Jni_thread_exit(PVOID value) { - if (value != NULL && Jni_vm != NULL) { - (*Jni_vm)->DetachCurrentThread(Jni_vm); - } -} - -static BOOL CALLBACK Jni_make_env_slot(PINIT_ONCE once, PVOID param, PVOID* context) { - (void)once; - (void)param; - (void)context; - Jni_env_slot = FlsAlloc(Jni_thread_exit); - return TRUE; -} - -/* Arranges for the calling thread to detach when it exits; 0 if it can't. */ -static int Jni_remember_attach(void) { - InitOnceExecuteOnce(&Jni_env_once, Jni_make_env_slot, NULL, NULL); - return Jni_env_slot != FLS_OUT_OF_INDEXES && FlsSetValue(Jni_env_slot, (PVOID)Jni_vm); -} -#else -#include - -static pthread_key_t Jni_env_key; -static int Jni_env_key_ok = 0; -static pthread_once_t Jni_env_once = PTHREAD_ONCE_INIT; - -static void Jni_thread_exit(void* value) { - if (value != NULL && Jni_vm != NULL) { - (*Jni_vm)->DetachCurrentThread(Jni_vm); - } -} - -static void Jni_make_env_key(void) { - Jni_env_key_ok = pthread_key_create(&Jni_env_key, Jni_thread_exit) == 0; -} - -/* Arranges for the calling thread to detach when it exits; 0 if it can't. */ -static int Jni_remember_attach(void) { - pthread_once(&Jni_env_once, Jni_make_env_key); - return Jni_env_key_ok && pthread_setspecific(Jni_env_key, (void*)Jni_vm) == 0; -} -#endif - -/* The calling thread's JNIEnv, attaching the thread if needed; NULL when - the JVM is unavailable. `*detach` is set when the caller must hand it - back with Jni_env_done because the thread can't detach itself at exit. */ -static JNIEnv* Jni_env(int* detach) { - JNIEnv* env = NULL; - *detach = 0; - if (Jni_vm == NULL) { - return NULL; - } - if ((*Jni_vm)->GetEnv(Jni_vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK) { - return env; - } - /* `void*` converts to the `JNIEnv**` Android declares and the `void**` - the JDK declares. */ - if ((*Jni_vm)->AttachCurrentThreadAsDaemon(Jni_vm, (void*)&env, NULL) != JNI_OK) { - return NULL; - } - *detach = !Jni_remember_attach(); - return env; -} - -/* Ends a use of Jni_env's JNIEnv, detaching the thread if Jni_env said so. */ -static void Jni_env_done(int detach) { - if (detach) { - (*Jni_vm)->DetachCurrentThread(Jni_vm); - } -} - -/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ -static jmethodID Jni_on_unit = NULL; -static jmethodID Jni_on_boolean = NULL; -static jmethodID Jni_on_byte = NULL; -static jmethodID Jni_on_short = NULL; -static jmethodID Jni_on_int = NULL; -static jmethodID Jni_on_long = NULL; -static jmethodID Jni_on_float = NULL; -static jmethodID Jni_on_double = NULL; -static jmethodID Jni_on_bytes = NULL; -static jmethodID Jni_on_error = NULL; - -static jint Jni_load_async(JNIEnv* env) { - jclass cls = (*env)->FindClass(env, "docs_everywhere/NativeCompletion"); - if (cls == NULL) { - return JNI_ERR; - } - Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); - Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); - Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); - Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); - Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); - Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); - Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); - Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); - Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); - Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); - if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { - return JNI_ERR; - } - return JNI_OK; -} - -/* Starts a completion on the producer thread: returns the JNIEnv with a - local frame pushed, or NULL when the call failed (the failure, or the - loss of the JVM, has then been handled and `context` released). */ -static JNIEnv* Jni_complete_begin(void* context, docs_everywhere_error* err, int* detach) { - JNIEnv* env = Jni_env(detach); - if (env == NULL) { - docs_everywhere_error_free(err); - return NULL; - } - if ((*env)->PushLocalFrame(env, 8) != 0) { - (*env)->ExceptionClear(env); - docs_everywhere_error_free(err); - Jni_env_done(*detach); - return NULL; - } - if (err == NULL) { - return env; - } - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; - if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); - } - jint code = (jint)err->code; - docs_everywhere_error_free(err); - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(*detach); - return NULL; -} - -/* Finishes a successful completion: reports anything the Kotlin side threw - (it has no caller to propagate to) and releases `context`. */ -static void Jni_complete_end(JNIEnv* env, void* context, int detach) { - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(detach); -} - -JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_cancel_1token_1create(JNIEnv* env, jclass cls) { - (void)env; - (void)cls; - return (jlong)(intptr_t)docs_everywhere_cancel_token_create(); -} - -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_cancel_1token_1cancel(JNIEnv* env, jclass cls, jlong token) { - (void)env; - (void)cls; - docs_everywhere_cancel_token_cancel((docs_everywhere_cancel_token*)(intptr_t)token); -} - -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_cancel_1token_1destroy(JNIEnv* env, jclass cls, jlong token) { - (void)env; - (void)cls; - docs_everywhere_cancel_token_destroy((docs_everywhere_cancel_token*)(intptr_t)token); -} - -/* Callback interfaces: the producer holds a global reference to the Kotlin - implementation as `ctx`; trampolines call the matching static dispatch - shim on JniBridge from whatever thread the producer uses. The shim never - lets an exception escape: it reports a failure itself through `out_err` - (with the two exports below) and returns a zero value. */ - -/* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after - reporting the failure through `out_err`. */ -static JNIEnv* Jni_callback_begin(docs_everywhere_error* out_err, int* detach) { - JNIEnv* env = Jni_env(detach); - if (env == NULL) { - docs_everywhere_error_set(out_err, -4, "could not attach the calling thread to the JVM"); - return NULL; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - docs_everywhere_error_set(out_err, -4, "JNI local frame exhausted"); - Jni_env_done(*detach); - return NULL; - } - return env; -} - -/* Leaves a trampoline. An exception still pending means the dispatch itself - failed (the shim catches everything the implementation throws), which - reaches the producer as code -4; nothing ever unwinds through its frames. */ -static void Jni_callback_end(JNIEnv* env, docs_everywhere_error* out_err, int detach) { - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionClear(env); - docs_everywhere_error_set(out_err, -4, "the JVM failed to dispatch the callback"); - } - (*env)->PopLocalFrame(env, NULL); - Jni_env_done(detach); -} - -/* The vtable `free` entry: drops the producer's reference to the Kotlin - implementation, from whatever thread the producer releases it on. */ -static void Jni_release_callback(void* ctx) { - int detach = 0; - JNIEnv* env = Jni_env(&detach); - if (env != NULL) { - if (ctx != NULL) { - (*env)->DeleteGlobalRef(env, (jobject)ctx); - } - Jni_env_done(detach); - } -} - -/* `JniBridge.error_set`: reports a failed callback through the trampoline's - `out_err` (the message is cut at its first NUL). */ -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_error_1set(JNIEnv* env, jclass cls, jlong err, jint code, jbyteArray message) { - (void)cls; - Jni_bytes m = Jni_borrow_bytes(env, message); - char* text = (char*)malloc(m.len + 1u); - if (text != NULL) { - if (m.len > 0) { - memcpy(text, m.ptr, m.len); - } - text[m.len] = '\0'; - } - Jni_release_bytes(env, &m); - docs_everywhere_error_set((docs_everywhere_error*)(intptr_t)err, (int32_t)code, text != NULL ? text : "callback failed"); - free(text); -} - -/* `JniBridge.error_set_payload`: attaches a typed error's fields (a value - buffer) to the failure `error_set` reported. */ -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_error_1set_1payload(JNIEnv* env, jclass cls, jlong err, jbyteArray payload) { - (void)cls; - Jni_bytes p = Jni_borrow_bytes(env, payload); - docs_everywhere_error_set_payload((docs_everywhere_error*)(intptr_t)err, p.ptr, p.len); - Jni_release_bytes(env, &p); -} - -static jmethodID Jni_mid_docs_SavedListener_on_saved = NULL; - -static void Jni_docs_SavedListener_on_saved(void* ctx, int64_t p_doc_id, int32_t p_revision, docs_everywhere_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return; - } - (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_docs_SavedListener_on_saved, (jobject)ctx, (jlong)(intptr_t)out_err, (jlong)p_doc_id, (jint)p_revision); - Jni_callback_end(env, out_err, detach); -} - -static const docs_everywhere_docs_SavedListener_vtable Jni_docs_SavedListener_vtable = {sizeof(docs_everywhere_docs_SavedListener_vtable), 0, Jni_release_callback, Jni_docs_SavedListener_on_saved}; - -static const Jni_contract_entry Jni_contract_docs[] = { - {UINT64_C(0x00b2187fd75634ec), UINT64_C(0x2263c6f787bf6081), "docs.publish_async"}, - {UINT64_C(0x12fc242b9dc6444c), UINT64_C(0x96405f484a642b31), "docs.Notebook.insert"}, - {UINT64_C(0x2213822ed05cd5bd), UINT64_C(0x26c5833392aa63ce), "docs.Notebook.new"}, - {UINT64_C(0x2bd762bcdaa207ff), UINT64_C(0x1b0ebab2d04c49b0), "docs.SavedListener"}, - {UINT64_C(0x2e086c45e9d617de), UINT64_C(0x4d43bf32358c01ff), "docs.Notebook.find"}, - {UINT64_C(0x4817dc98197ad960), UINT64_C(0xf8017b5a7d58b2c7), "docs.Notebook.size"}, - {UINT64_C(0x614de47452d5a55c), UINT64_C(0xd9aae1472494ffef), "docs.get_document"}, - {UINT64_C(0x67c5cb9fc119bcd5), UINT64_C(0x08d5b25a1a22a7fa), "docs.Document"}, - {UINT64_C(0x8d484bf2d78c9b70), UINT64_C(0xe9093cf96912194c), "docs.Notebook.max_documents"}, - {UINT64_C(0xaea61d6451691c67), UINT64_C(0xe4704128b3524b3d), "docs.watch"}, - {UINT64_C(0xd68d6ad1540cdec5), UINT64_C(0x01afeb40b0a8bfe1), "docs.Notebook"}, - {UINT64_C(0xdb0601444a4760cc), UINT64_C(0xe56c69629b444797), "docs.create_document"}, - {UINT64_C(0xe12d6f4838347b3e), UINT64_C(0xa9bab86646d84f51), "docs.Status"}, - {UINT64_C(0xee2f8d25b3313b50), UINT64_C(0x9691232f35439a2b), "docs.DocsErrors"}, -}; - -static jint Jni_load(JNIEnv* env) { - (void)env; - if (Jni_check_contract(env, docs_everywhere_docs_contract, Jni_contract_docs, sizeof Jni_contract_docs / sizeof Jni_contract_docs[0]) != JNI_OK) { - return JNI_ERR; - } - if (Jni_load_async(env) != JNI_OK) { - return JNI_ERR; - } - Jni_mid_docs_SavedListener_on_saved = (*env)->GetStaticMethodID(env, Jni_bridge, "docs_SavedListener_on_saved", "(Ldocs_everywhere/SavedListener;JJI)V"); - if (Jni_mid_docs_SavedListener_on_saved == NULL) { - return JNI_ERR; - } - return JNI_OK; -} - -JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1clone(JNIEnv* env, jclass cls, jlong self) { - (void)env; - (void)cls; - return (jlong)(intptr_t)docs_everywhere_docs_Notebook_clone((const docs_everywhere_docs_Notebook*)(intptr_t)self); -} - -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1destroy(JNIEnv* env, jclass cls, jlong self) { - (void)env; - (void)cls; - docs_everywhere_docs_Notebook_destroy((docs_everywhere_docs_Notebook*)(intptr_t)self); -} - -JNIEXPORT jbyteArray JNICALL Java_docs_1everywhere_JniBridge_docs_1create_1document(JNIEnv* env, jclass cls, jbyteArray p_title, jbyteArray p_tags) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_title_b = Jni_borrow_bytes(env, p_title); - Jni_bytes p_tags_b = Jni_borrow_bytes(env, p_tags); - size_t out_len = 0; - const uint8_t* rv = docs_everywhere_docs_create_document(p_title_b.ptr, p_title_b.len, p_tags_b.ptr, p_tags_b.len, &out_len, &err); - Jni_release_bytes(env, &p_title_b); - Jni_release_bytes(env, &p_tags_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_docs_1everywhere_JniBridge_docs_1get_1document(JNIEnv* env, jclass cls, jlong p_id) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = docs_everywhere_docs_get_document((int64_t)p_id, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -static void Jni_done_docs_publish_async(void* context, docs_everywhere_error* err, bool result) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_boolean, result ? JNI_TRUE : JNI_FALSE); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_docs_1publish_1async(JNIEnv* env, jclass cls, jlong p_id, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - docs_everywhere_docs_publish_async((int64_t)p_id, Jni_done_docs_publish_async, context); -} - -JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_docs_1watch(JNIEnv* env, jclass cls, jobject p_listener) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - docs_everywhere_docs_watch((void*)(*env)->NewGlobalRef(env, p_listener), &Jni_docs_SavedListener_vtable, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return; - } -} - -JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1new(JNIEnv* env, jclass cls) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - docs_everywhere_docs_Notebook* rv = docs_everywhere_docs_Notebook_new(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jint JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1insert(JNIEnv* env, jclass cls, jlong self, jbyteArray p_document) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_document_b = Jni_borrow_bytes(env, p_document); - int32_t rv = docs_everywhere_docs_Notebook_insert((const docs_everywhere_docs_Notebook*)(intptr_t)self, p_document_b.ptr, p_document_b.len, &err); - Jni_release_bytes(env, &p_document_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jint)rv; -} - -JNIEXPORT jbyteArray JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1find(JNIEnv* env, jclass cls, jlong self, jlong p_id) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = docs_everywhere_docs_Notebook_find((const docs_everywhere_docs_Notebook*)(intptr_t)self, (int64_t)p_id, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 1); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jint JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1size(JNIEnv* env, jclass cls, jlong self) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - int32_t rv = docs_everywhere_docs_Notebook_size((const docs_everywhere_docs_Notebook*)(intptr_t)self, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jint)rv; -} - -JNIEXPORT jint JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1max_1documents(JNIEnv* env, jclass cls) { - docs_everywhere_error err = {0, NULL, NULL, 0}; - (void)cls; - int32_t rv = docs_everywhere_docs_Notebook_max_documents(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jint)rv; -} - - -// END docs_everywhere_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Codecs_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Codecs_kt.snap deleted file mode 100644 index 59e7bd23..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Codecs_kt.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package docs_everywhere - -internal fun packListOfString(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeString(it) } - -internal fun unpackListOfString(_r: BufferReader): List = _r.readList { _r.readString() } - -internal fun packOptionalOfDocument(_w: BufferWriter, _v: Document?) = _w.writeOptional(_v) { packDocument(_w, it) } - -internal fun unpackOptionalOfDocument(_r: BufferReader): Document? = _r.readOptional { unpackDocument(_r) } - -// END Codecs.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Docs_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Docs_kt.snap deleted file mode 100644 index aa433be9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Docs_kt.snap +++ /dev/null @@ -1,176 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package docs_everywhere - -/** Errors of the `DocsErrors` domain. */ -sealed class DocsErrorsException(code: Int, message: String) : FfiException(code, message) { - /** Returned when no document matches the requested id */ - class NotFound(message: String = "Document not found") : DocsErrorsException(1, message) - /** Raised when attempting to move into a forbidden state */ - class InvalidStatus(message: String = "Invalid status transition") : DocsErrorsException(2, message) - - internal companion object { - /** The exception for a raw code and its serialized payload. */ - fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { - 1 -> NotFound(message) - 2 -> InvalidStatus(message) - else -> FfiException(code, message) - } - } -} - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -enum class Status(val value: Int) { - /** Document is being edited */ - Draft(0), - /** Document is publicly visible */ - Published(1), - /** Document is hidden from listings */ - Archived(2); - - companion object { - /** The variant whose ABI value is [value]. */ - fun fromValue(value: Int): Status = entries.firstOrNull { it.value == value } - ?: throw NativeBugException(-3, "unknown Status value $value") - } -} - -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -data class Document( - /** Stable opaque identifier */ - val id: Long, - /** Human-readable title shown in lists */ - val title: String, - /** Current lifecycle status */ - val status: Status, - /** Free-form tags for grouping */ - val tags: List, -) - -internal fun packDocument(_w: BufferWriter, _v: Document) { - _w.writeI64(_v.id) - _w.writeString(_v.title) - _w.writeI32(_v.status.value) - packListOfString(_w, _v.tags) -} - -internal fun unpackDocument(_r: BufferReader): Document = Document( - _r.readI64(), - _r.readString(), - Status.fromValue(_r.readI32()), - unpackListOfString(_r), -) - -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - */ -interface SavedListener { - /** - * Fires after a document has been persisted successfully - * @param docId Identifier of the saved document - * @param revision Monotonically increasing revision - */ - fun onSaved(docId: Long, revision: Int) -} - -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -class Notebook private constructor(address: Long) : AutoCloseable { - internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::docs_Notebook_destroy)) - - /** A new strong reference to this object, as written into a value buffer. */ - internal fun cloneHandle(): Long = handle.borrow { JniBridge.docs_Notebook_clone(it) } - - /** - * Add a document, returning its position in the notebook - * @param document The document to add - */ - fun insert(document: Document): Int = handle.borrow { _self -> - JniBridge.docs_Notebook_insert(_self, encodeBuffer { _w -> packDocument(_w, document) }) - } - - /** - * Look up a document by identifier, failing when none matches - * @param id Identifier of the document to find - */ - fun find(id: Long): Document = handle.borrow { _self -> - decodeBuffer(JniBridge.docs_Notebook_find(_self, id)) { _r -> unpackDocument(_r) } - } - - /** The number of documents currently held */ - fun size(): Int = handle.borrow { _self -> - JniBridge.docs_Notebook_size(_self) - } - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() = handle.close() - - companion object { - /** Adopts one strong reference (a return value or a buffer token). */ - internal fun fromHandle(address: Long): Notebook = Notebook(address) - - /** [fromHandle], with address 0 meaning no object. */ - internal fun fromHandleOrNull(address: Long): Notebook? = if (address == 0L) null else Notebook(address) - - /** Create an empty notebook */ - operator fun invoke(): Notebook = Notebook.fromHandle(JniBridge.docs_Notebook_new()) - - /** The largest number of documents one notebook will hold */ - fun maxDocuments(): Int = JniBridge.docs_Notebook_max_documents() - } -} - -/** Document storage: create, publish, and observe documents. */ -object Docs { - /** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - */ - fun createDocument(title: String, tags: List): Document = decodeBuffer(JniBridge.docs_create_document(encodeUtf8(title), encodeBuffer { _w -> packListOfString(_w, tags) })) { _r -> unpackDocument(_r) } - - /** - * Look up a document by identifier - * @param id Identifier returned from create_document - */ - fun getDocument(id: Long): Document? = decodeBuffer(JniBridge.docs_get_document(id)) { _r -> unpackOptionalOfDocument(_r) } - - /** - * Publish a document in the background - * @param id Identifier of the document to publish - */ - suspend fun publishAsync(id: Long): Boolean = awaitNative(false, 0, { _raw -> _raw as Boolean }) { _, _done -> - JniBridge.docs_publish_async(id, _done) - } - - /** - * Register a listener for saved notifications - * @param listener Receiver of saved notifications - */ - fun watch(listener: SavedListener) { - JniBridge.docs_watch(listener) - } -} - -// END Docs.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_JniBridge_kt.snap deleted file mode 100644 index b671a538..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_JniBridge_kt.snap +++ /dev/null @@ -1,76 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package docs_everywhere - -/** - * The JNI entry points of `libdocs_everywhere_jni`: one native per C symbol, named after - * it, plus the hooks the shim calls back into. Internal plumbing for the - * public wrappers. - */ -internal object JniBridge { - init { - NativeLibrary.ensureLoaded() - } - - /** - * The exception for a native failure: through error domain [domain] for a - * throwing call, or a [NativeBugException] (domain 0) for one that can't fail. - */ - @JvmStatic - fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): Throwable { - val text = decodeUtf8(message) - return when (domain) { - 1 -> DocsErrorsException.fromCode(code, text, payload) - else -> NativeBugException(code, text) - } - } - - /** - * Reports a failed callback through its vtable entry's `out_err` ([err]): - * [typed], a domain error the method is declared to throw, with its code and - * payload fields, and anything else as code -4 with its message. - */ - private fun fail(err: Long, error: Throwable, typed: FfiException?) { - if (typed != null) { - error_set(err, typed.code, encodeUtf8(typed.message ?: "")) - typed.encodePayload()?.let { error_set_payload(err, it) } - } else { - error_set(err, -4, encodeUtf8(error.message ?: error.toString())) - } - } - - @JvmStatic external fun error_set(err: Long, code: Int, message: ByteArray) - @JvmStatic external fun error_set_payload(err: Long, payload: ByteArray) - - @JvmStatic external fun debug_live(kind: Int): Long - @JvmStatic external fun cancel_token_create(): Long - @JvmStatic external fun cancel_token_cancel(token: Long) - @JvmStatic external fun cancel_token_destroy(token: Long) - @JvmStatic external fun docs_Notebook_clone(_self: Long): Long - @JvmStatic external fun docs_Notebook_destroy(_self: Long) - @JvmStatic external fun docs_create_document(title: ByteArray, tags: ByteArray): ByteArray - @JvmStatic external fun docs_get_document(id: Long): ByteArray - @JvmStatic external fun docs_publish_async(id: Long, _done: NativeCompletion<*>) - @JvmStatic external fun docs_watch(listener: SavedListener) - @JvmStatic external fun docs_Notebook_new(): Long - @JvmStatic external fun docs_Notebook_insert(_self: Long, document: ByteArray): Int - @JvmStatic external fun docs_Notebook_find(_self: Long, id: Long): ByteArray - @JvmStatic external fun docs_Notebook_size(_self: Long): Int - @JvmStatic external fun docs_Notebook_max_documents(): Int - - @JvmStatic - fun docs_SavedListener_on_saved(_impl: SavedListener, _err: Long, docId: Long, revision: Int) { - try { - _impl.onSaved(docId, revision) - } catch (_e: Throwable) { - fail(_err, _e, null) - } - } -} - -// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_jni_c.snap deleted file mode 100644 index 70b317e5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_jni_c.snap +++ /dev/null @@ -1,1200 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#include -#include -#include -#include -#include -#include -#include - -/* The shim forwards deprecated functions too, so it doesn't warn about them. */ -#define EDGE_CASES_DEPRECATED(msg) -#include "edge_cases.h" - -/* Every static name in this file starts with `Jni_`, which no C ABI symbol - (all lowercase `edge_cases_...`) and no JNI export (`Java_...`) can - spell. */ - -static JavaVM* Jni_vm = NULL; -static jclass Jni_bridge = NULL; -static jmethodID Jni_error = NULL; - -/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value - buffers all cross as one. */ -typedef struct { - jbyteArray array; - jbyte* elems; - const uint8_t* ptr; - size_t len; -} Jni_bytes; - -static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { - Jni_bytes b = {array, NULL, NULL, 0}; - if (array == NULL) { - return b; - } - b.len = (size_t)(*env)->GetArrayLength(env, array); - b.elems = (*env)->GetByteArrayElements(env, array, NULL); - b.ptr = (const uint8_t*)b.elems; - if (b.elems == NULL) { - b.len = 0; - } - return b; -} - -static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { - if (b->elems != NULL) { - (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); - } -} - -/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid - input, so NULL + 0 is the empty array). */ -static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = (*env)->NewByteArray(env, (jsize)len); - if (out != NULL && len > 0 && ptr != NULL) { - (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); - } - return out; -} - -/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ -static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = Jni_new_bytes(env, ptr, len); - if (ptr != NULL) { - edge_cases_free_bytes((uint8_t*)ptr, len); - } - return out; -} - -/* Throws the Kotlin exception for `err` (mapped through error domain - `domain`, or 0 for a call that can't fail, which raises the trap) and - clears `err`. */ -static inline void Jni_throw(JNIEnv* env, edge_cases_error* err, jint domain) { - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; - if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); - } - jint code = (jint)err->code; - edge_cases_error_clear(err); - if ((*env)->ExceptionCheck(env)) { - return; - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { - (*env)->Throw(env, ex); - } -} - -static jint Jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { - (*env)->ThrowNew(env, cls, message); - } - return JNI_ERR; -} - -/* One contract entry these bindings were generated with: a declaration's - id and signature hash, plus its dotted path for the load error. */ -typedef struct { - uint64_t id; - uint64_t hash; - const char* path; -} Jni_contract_entry; - -/* Checks that the library's contract table (from `contract`, sorted by id) - has every expected entry with an equal hash; entries only the library - has are fine. Fails loading with an error naming the first declaration - that's missing or changed. */ -static inline jint Jni_check_contract(JNIEnv* env, const edge_cases_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { - size_t len = 0; - const edge_cases_contract_entry* table = contract(&len); - if (table == NULL) { - len = 0; - } - for (size_t i = 0; i < count; i++) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) { - lo = mid + 1; - } else { - hi = mid; - } - } - const char* problem = NULL; - if (lo == len || table[lo].id != expected[i].id) { - problem = "is missing from the library"; - } else if (table[lo].hash != expected[i].hash) { - problem = "changed since these bindings were generated"; - } - if (problem != NULL) { - char message[512]; - snprintf(message, sizeof message, "%s: %s %s", "edge_cases", expected[i].path, problem); - return Jni_load_error(env, message); - } - } - return JNI_OK; -} - -/* Generated below: the contract checks and the method IDs the shim caches. */ -static jint Jni_load(JNIEnv* env); - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env = NULL; - (void)reserved; - Jni_vm = vm; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - if (edge_cases_abi_version() != 4u) { - return Jni_load_error(env, "edge_cases: the native library implements a different C ABI revision than these bindings (expected 4)"); - } - jclass bridge = (*env)->FindClass(env, "edge_cases/JniBridge"); - if (bridge == NULL) { - return JNI_ERR; - } - Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); - Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ljava/lang/Throwable;"); - if (Jni_error == NULL) { - return JNI_ERR; - } - if (Jni_load(env) != JNI_OK) { - return JNI_ERR; - } - return JNI_VERSION_1_6; -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { - (void)env; - (void)cls; - return (jlong)edge_cases_debug_live((int32_t)kind); -} - -/* Producer threads (async completions, callback calls, vtable `free`) are - usually not JVM threads. The first time one calls in it attaches as a - daemon, and a thread-local destructor (a pthread key, or a fiber-local - slot on Windows) detaches it when the thread exits, so a busy producer - thread pays for one attach, not one per call. If the destructor can't be - registered, the thread detaches again as soon as the call ends. */ -#if defined(_WIN32) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include - -static DWORD Jni_env_slot = FLS_OUT_OF_INDEXES; -static INIT_ONCE Jni_env_once = INIT_ONCE_STATIC_INIT; - -static VOID WINAPI Jni_thread_exit(PVOID value) { - if (value != NULL && Jni_vm != NULL) { - (*Jni_vm)->DetachCurrentThread(Jni_vm); - } -} - -static BOOL CALLBACK Jni_make_env_slot(PINIT_ONCE once, PVOID param, PVOID* context) { - (void)once; - (void)param; - (void)context; - Jni_env_slot = FlsAlloc(Jni_thread_exit); - return TRUE; -} - -/* Arranges for the calling thread to detach when it exits; 0 if it can't. */ -static int Jni_remember_attach(void) { - InitOnceExecuteOnce(&Jni_env_once, Jni_make_env_slot, NULL, NULL); - return Jni_env_slot != FLS_OUT_OF_INDEXES && FlsSetValue(Jni_env_slot, (PVOID)Jni_vm); -} -#else -#include - -static pthread_key_t Jni_env_key; -static int Jni_env_key_ok = 0; -static pthread_once_t Jni_env_once = PTHREAD_ONCE_INIT; - -static void Jni_thread_exit(void* value) { - if (value != NULL && Jni_vm != NULL) { - (*Jni_vm)->DetachCurrentThread(Jni_vm); - } -} - -static void Jni_make_env_key(void) { - Jni_env_key_ok = pthread_key_create(&Jni_env_key, Jni_thread_exit) == 0; -} - -/* Arranges for the calling thread to detach when it exits; 0 if it can't. */ -static int Jni_remember_attach(void) { - pthread_once(&Jni_env_once, Jni_make_env_key); - return Jni_env_key_ok && pthread_setspecific(Jni_env_key, (void*)Jni_vm) == 0; -} -#endif - -/* The calling thread's JNIEnv, attaching the thread if needed; NULL when - the JVM is unavailable. `*detach` is set when the caller must hand it - back with Jni_env_done because the thread can't detach itself at exit. */ -static JNIEnv* Jni_env(int* detach) { - JNIEnv* env = NULL; - *detach = 0; - if (Jni_vm == NULL) { - return NULL; - } - if ((*Jni_vm)->GetEnv(Jni_vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK) { - return env; - } - /* `void*` converts to the `JNIEnv**` Android declares and the `void**` - the JDK declares. */ - if ((*Jni_vm)->AttachCurrentThreadAsDaemon(Jni_vm, (void*)&env, NULL) != JNI_OK) { - return NULL; - } - *detach = !Jni_remember_attach(); - return env; -} - -/* Ends a use of Jni_env's JNIEnv, detaching the thread if Jni_env said so. */ -static void Jni_env_done(int detach) { - if (detach) { - (*Jni_vm)->DetachCurrentThread(Jni_vm); - } -} - -/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ -static jmethodID Jni_on_unit = NULL; -static jmethodID Jni_on_boolean = NULL; -static jmethodID Jni_on_byte = NULL; -static jmethodID Jni_on_short = NULL; -static jmethodID Jni_on_int = NULL; -static jmethodID Jni_on_long = NULL; -static jmethodID Jni_on_float = NULL; -static jmethodID Jni_on_double = NULL; -static jmethodID Jni_on_bytes = NULL; -static jmethodID Jni_on_error = NULL; - -static jint Jni_load_async(JNIEnv* env) { - jclass cls = (*env)->FindClass(env, "edge_cases/NativeCompletion"); - if (cls == NULL) { - return JNI_ERR; - } - Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); - Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); - Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); - Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); - Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); - Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); - Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); - Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); - Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); - Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); - if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { - return JNI_ERR; - } - return JNI_OK; -} - -/* Starts a completion on the producer thread: returns the JNIEnv with a - local frame pushed, or NULL when the call failed (the failure, or the - loss of the JVM, has then been handled and `context` released). */ -static JNIEnv* Jni_complete_begin(void* context, edge_cases_error* err, int* detach) { - JNIEnv* env = Jni_env(detach); - if (env == NULL) { - edge_cases_error_free(err); - return NULL; - } - if ((*env)->PushLocalFrame(env, 8) != 0) { - (*env)->ExceptionClear(env); - edge_cases_error_free(err); - Jni_env_done(*detach); - return NULL; - } - if (err == NULL) { - return env; - } - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; - if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); - } - jint code = (jint)err->code; - edge_cases_error_free(err); - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(*detach); - return NULL; -} - -/* Finishes a successful completion: reports anything the Kotlin side threw - (it has no caller to propagate to) and releases `context`. */ -static void Jni_complete_end(JNIEnv* env, void* context, int detach) { - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(detach); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_cancel_1token_1create(JNIEnv* env, jclass cls) { - (void)env; - (void)cls; - return (jlong)(intptr_t)edge_cases_cancel_token_create(); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_cancel_1token_1cancel(JNIEnv* env, jclass cls, jlong token) { - (void)env; - (void)cls; - edge_cases_cancel_token_cancel((edge_cases_cancel_token*)(intptr_t)token); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_cancel_1token_1destroy(JNIEnv* env, jclass cls, jlong token) { - (void)env; - (void)cls; - edge_cases_cancel_token_destroy((edge_cases_cancel_token*)(intptr_t)token); -} - -/* Callback interfaces: the producer holds a global reference to the Kotlin - implementation as `ctx`; trampolines call the matching static dispatch - shim on JniBridge from whatever thread the producer uses. The shim never - lets an exception escape: it reports a failure itself through `out_err` - (with the two exports below) and returns a zero value. */ - -/* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after - reporting the failure through `out_err`. */ -static JNIEnv* Jni_callback_begin(edge_cases_error* out_err, int* detach) { - JNIEnv* env = Jni_env(detach); - if (env == NULL) { - edge_cases_error_set(out_err, -4, "could not attach the calling thread to the JVM"); - return NULL; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - edge_cases_error_set(out_err, -4, "JNI local frame exhausted"); - Jni_env_done(*detach); - return NULL; - } - return env; -} - -/* Leaves a trampoline. An exception still pending means the dispatch itself - failed (the shim catches everything the implementation throws), which - reaches the producer as code -4; nothing ever unwinds through its frames. */ -static void Jni_callback_end(JNIEnv* env, edge_cases_error* out_err, int detach) { - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionClear(env); - edge_cases_error_set(out_err, -4, "the JVM failed to dispatch the callback"); - } - (*env)->PopLocalFrame(env, NULL); - Jni_env_done(detach); -} - -/* The vtable `free` entry: drops the producer's reference to the Kotlin - implementation, from whatever thread the producer releases it on. */ -static void Jni_release_callback(void* ctx) { - int detach = 0; - JNIEnv* env = Jni_env(&detach); - if (env != NULL) { - if (ctx != NULL) { - (*env)->DeleteGlobalRef(env, (jobject)ctx); - } - Jni_env_done(detach); - } -} - -/* `JniBridge.error_set`: reports a failed callback through the trampoline's - `out_err` (the message is cut at its first NUL). */ -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_error_1set(JNIEnv* env, jclass cls, jlong err, jint code, jbyteArray message) { - (void)cls; - Jni_bytes m = Jni_borrow_bytes(env, message); - char* text = (char*)malloc(m.len + 1u); - if (text != NULL) { - if (m.len > 0) { - memcpy(text, m.ptr, m.len); - } - text[m.len] = '\0'; - } - Jni_release_bytes(env, &m); - edge_cases_error_set((edge_cases_error*)(intptr_t)err, (int32_t)code, text != NULL ? text : "callback failed"); - free(text); -} - -/* `JniBridge.error_set_payload`: attaches a typed error's fields (a value - buffer) to the failure `error_set` reported. */ -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_error_1set_1payload(JNIEnv* env, jclass cls, jlong err, jbyteArray payload) { - (void)cls; - Jni_bytes p = Jni_borrow_bytes(env, payload); - edge_cases_error_set_payload((edge_cases_error*)(intptr_t)err, p.ptr, p.len); - Jni_release_bytes(env, &p); -} - -static jmethodID Jni_mid_edge_cases_BatchListener_on_batch = NULL; -static jmethodID Jni_mid_edge_cases_BatchListener_on_widget = NULL; -static jmethodID Jni_mid_edge_cases_BatchListener_on_tick = NULL; -static jmethodID Jni_mid_edge_cases_BatchListener_should_continue = NULL; -static jmethodID Jni_mid_edge_cases_BatchListener_pick_mode = NULL; - -static void Jni_edge_cases_BatchListener_on_batch(void* ctx, const uint8_t* p_items_ptr, size_t p_items_len, const uint8_t* p_raw_ptr, size_t p_raw_len, const uint8_t* p_first_ptr, size_t p_first_len, edge_cases_edge_cases_Mode p_mode, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return; - } - (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_on_batch, (jobject)ctx, (jlong)(intptr_t)out_err, Jni_new_bytes(env, p_items_ptr, p_items_len), Jni_new_bytes(env, p_raw_ptr, p_raw_len), Jni_new_bytes(env, p_first_ptr, p_first_len), (jint)p_mode); - Jni_callback_end(env, out_err, detach); -} - -static void Jni_edge_cases_BatchListener_on_widget(void* ctx, edge_cases_edge_cases_Widget* p_widget, edge_cases_edge_cases_Widget* p_fallback, const uint8_t* p_children_ptr, size_t p_children_len, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - edge_cases_edge_cases_Widget_destroy(p_widget); - edge_cases_edge_cases_Widget_destroy(p_fallback); - return; - } - (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_on_widget, (jobject)ctx, (jlong)(intptr_t)out_err, (jlong)(intptr_t)p_widget, (jlong)(intptr_t)p_fallback, Jni_new_bytes(env, p_children_ptr, p_children_len)); - Jni_callback_end(env, out_err, detach); -} - -static void Jni_edge_cases_BatchListener_on_tick(void* ctx, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return; - } - (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_on_tick, (jobject)ctx, (jlong)(intptr_t)out_err); - Jni_callback_end(env, out_err, detach); -} - -static bool Jni_edge_cases_BatchListener_should_continue(void* ctx, int32_t p_class, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return (bool)0; - } - jboolean rv = (*env)->CallStaticBooleanMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_should_continue, (jobject)ctx, (jlong)(intptr_t)out_err, (jint)p_class); - Jni_callback_end(env, out_err, detach); - return rv == JNI_TRUE; -} - -static edge_cases_edge_cases_Mode Jni_edge_cases_BatchListener_pick_mode(void* ctx, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return (edge_cases_edge_cases_Mode)0; - } - jint rv = (*env)->CallStaticIntMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_pick_mode, (jobject)ctx, (jlong)(intptr_t)out_err); - Jni_callback_end(env, out_err, detach); - return (edge_cases_edge_cases_Mode)rv; -} - -static const edge_cases_edge_cases_BatchListener_vtable Jni_edge_cases_BatchListener_vtable = {sizeof(edge_cases_edge_cases_BatchListener_vtable), 0, Jni_release_callback, Jni_edge_cases_BatchListener_on_batch, Jni_edge_cases_BatchListener_on_widget, Jni_edge_cases_BatchListener_on_tick, Jni_edge_cases_BatchListener_should_continue, Jni_edge_cases_BatchListener_pick_mode}; - -static jmethodID Jni_mid_edge_cases_Keyword_delete = NULL; -static jmethodID Jni_mid_edge_cases_Keyword_import = NULL; - -static void Jni_edge_cases_Keyword_delete(void* ctx, int32_t p_self, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return; - } - (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_Keyword_delete, (jobject)ctx, (jlong)(intptr_t)out_err, (jint)p_self); - Jni_callback_end(env, out_err, detach); -} - -static int32_t Jni_edge_cases_Keyword_import(void* ctx, edge_cases_error* out_err) { - int detach = 0; - JNIEnv* env = Jni_callback_begin(out_err, &detach); - if (env == NULL) { - return (int32_t)0; - } - jint rv = (*env)->CallStaticIntMethod(env, Jni_bridge, Jni_mid_edge_cases_Keyword_import, (jobject)ctx, (jlong)(intptr_t)out_err); - Jni_callback_end(env, out_err, detach); - return (int32_t)rv; -} - -static const edge_cases_edge_cases_Keyword_vtable Jni_edge_cases_Keyword_vtable = {sizeof(edge_cases_edge_cases_Keyword_vtable), 0, Jni_release_callback, Jni_edge_cases_Keyword_delete, Jni_edge_cases_Keyword_import}; - -static const Jni_contract_entry Jni_contract_edge_cases[] = { - {UINT64_C(0x056a86511785ba2b), UINT64_C(0x5cba3f16d57c6cdb), "edge_cases.make_widget"}, - {UINT64_C(0x07bfa9ecb2b720d2), UINT64_C(0x3813b3f57c64bf35), "edge_cases.EdgeErrors"}, - {UINT64_C(0x0aaa11867e37f991), UINT64_C(0xf090dd52146ebdf0), "edge_cases.Widget.refresh"}, - {UINT64_C(0x0f9701493898ae4c), UINT64_C(0x6944f47a275c96e8), "edge_cases.Inner"}, - {UINT64_C(0x1226b1adac179f7d), UINT64_C(0x0e95a00ab0bef637), "edge_cases.async_keywords"}, - {UINT64_C(0x16d36bef1541136c), UINT64_C(0x5ea7021ec324298f), "edge_cases.find_widget"}, - {UINT64_C(0x1b7324c55e980feb), UINT64_C(0xabf87fe372358f42), "edge_cases.Widget.old_name"}, - {UINT64_C(0x1cbd731467c2bab8), UINT64_C(0x4915b9989c2c9074), "edge_cases.Vault"}, - {UINT64_C(0x2c2a6de67d894b0a), UINT64_C(0x50a4496b1e7191ca), "edge_cases.Vault.open"}, - {UINT64_C(0x322b4cfcd2d6861c), UINT64_C(0xca7527a379ae8465), "edge_cases.Keywords"}, - {UINT64_C(0x3f8e23a5f32906f4), UINT64_C(0x8d4c908d796f130d), "edge_cases.Leaf"}, - {UINT64_C(0x42ab8a685457b97c), UINT64_C(0xe5ce30500cadb32d), "edge_cases.Vault.handle"}, - {UINT64_C(0x4a68540e2836d3d0), UINT64_C(0xa20e44c71e420ed2), "edge_cases.mode_stream"}, - {UINT64_C(0x4e030580ac0b111d), UINT64_C(0xb68b7a18d910e175), "edge_cases.legacy_value"}, - {UINT64_C(0x4f5e4e5291864ef3), UINT64_C(0x55a1a88b21a08f8b), "edge_cases.Legacy"}, - {UINT64_C(0x4f84b06171b4213d), UINT64_C(0xbaedcfdebb6a2820), "edge_cases.scalar_stream"}, - {UINT64_C(0x5067a5ff2d9ef910), UINT64_C(0xbfc79b18fc4714fc), "edge_cases.Widget"}, - {UINT64_C(0x59aff159112fbf5b), UINT64_C(0x18e4def57a8cfa49), "edge_cases.optional_params"}, - {UINT64_C(0x5daf65b98fa77c48), UINT64_C(0xc817a8cb202e59c2), "edge_cases.keywords_roundtrip"}, - {UINT64_C(0x5de8a5c9726c2c5a), UINT64_C(0x7c50b5b65f7a896d), "edge_cases.register_batch"}, - {UINT64_C(0x618a620309994c3c), UINT64_C(0x826ad0b1d208d723), "edge_cases.async_bytes"}, - {UINT64_C(0x648285cdba0a7b8e), UINT64_C(0xcf6e80e783a4d8be), "edge_cases.WidgetHolder"}, - {UINT64_C(0x649c78079824b10d), UINT64_C(0x5b12dd26e0634b69), "edge_cases.async_widget"}, - {UINT64_C(0x7da7a7357d122a37), UINT64_C(0xf544e025ffaa3d20), "edge_cases.async_void"}, - {UINT64_C(0x7e4015bc3a472e2d), UINT64_C(0xfe22291d90b82e4a), "edge_cases.widget_stream"}, - {UINT64_C(0x805276e5d81fdba7), UINT64_C(0xd9b64e360bd03b2b), "edge_cases.Outer"}, - {UINT64_C(0x80e1183326542df4), UINT64_C(0xe8a57e12cc6ed276), "edge_cases.select"}, - {UINT64_C(0x923ce5e9bcd769a1), UINT64_C(0x0b973f5dc07dda8b), "edge_cases.Vault.to_string"}, - {UINT64_C(0x98ca8c01f5239404), UINT64_C(0xfe11c93eb6b303ef), "edge_cases.Widget.rename"}, - {UINT64_C(0xa13e22e98ea278d4), UINT64_C(0xf1fae215d7387800), "edge_cases.widgets_everywhere"}, - {UINT64_C(0xa84a8c9bf2a881e1), UINT64_C(0xbe1dc840bf221996), "edge_cases.async_optional"}, - {UINT64_C(0xad958c478fbf205d), UINT64_C(0xe386b0716d24f19b), "edge_cases.Retired"}, - {UINT64_C(0xb2fc3964ef56ab39), UINT64_C(0x73c325c8bc351236), "edge_cases.async_list"}, - {UINT64_C(0xb3403af521cf0006), UINT64_C(0x6abc30d48206a3ba), "edge_cases.Widget.close"}, - {UINT64_C(0xb794304a22a406b8), UINT64_C(0xf6fb98711c516105), "edge_cases.async_struct"}, - {UINT64_C(0xba3936106a49c47d), UINT64_C(0xb7025f2fadd6288e), "edge_cases.Vault.peek"}, - {UINT64_C(0xc155b0c3cfb164f4), UINT64_C(0xc777ae57d92ebc47), "edge_cases.noop"}, - {UINT64_C(0xcdc18607777019dd), UINT64_C(0xb81a31e5c20aa7a8), "edge_cases.inspect_widget"}, - {UINT64_C(0xd164a82aeb64b8cc), UINT64_C(0xded02c68005c4737), "edge_cases.nested_optional"}, - {UINT64_C(0xd978ccad2468038f), UINT64_C(0x30c39c65d6235153), "edge_cases.Mode"}, - {UINT64_C(0xdbe9d947431c9124), UINT64_C(0x39537fb848fdf980), "edge_cases.BatchListener"}, - {UINT64_C(0xeae8b7862c08bb07), UINT64_C(0xfe12eeaa830452ea), "edge_cases.Keyword"}, - {UINT64_C(0xf1e155a05ae262b8), UINT64_C(0xb575eb818a423d42), "edge_cases.async_enum"}, - {UINT64_C(0xf22e210bdbce7343), UINT64_C(0x2a5e0f801bc8c768), "edge_cases.Widget.dispose"}, - {UINT64_C(0xf3c7cf675563ee6c), UINT64_C(0x0ecffeef399facc8), "edge_cases.Vault.probe"}, - {UINT64_C(0xfd34bc67acac379e), UINT64_C(0x576caa14f3c0901a), "edge_cases.Widget.fetch"}, - {UINT64_C(0xff2e9d2bff941786), UINT64_C(0x7e96764ef1f5fbaa), "edge_cases.string_stream"}, -}; - -static jint Jni_load(JNIEnv* env) { - (void)env; - if (Jni_check_contract(env, edge_cases_edge_cases_contract, Jni_contract_edge_cases, sizeof Jni_contract_edge_cases / sizeof Jni_contract_edge_cases[0]) != JNI_OK) { - return JNI_ERR; - } - if (Jni_load_async(env) != JNI_OK) { - return JNI_ERR; - } - Jni_mid_edge_cases_BatchListener_on_batch = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_on_batch", "(Ledge_cases/BatchListener;J[B[B[BI)V"); - if (Jni_mid_edge_cases_BatchListener_on_batch == NULL) { - return JNI_ERR; - } - Jni_mid_edge_cases_BatchListener_on_widget = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_on_widget", "(Ledge_cases/BatchListener;JJJ[B)V"); - if (Jni_mid_edge_cases_BatchListener_on_widget == NULL) { - return JNI_ERR; - } - Jni_mid_edge_cases_BatchListener_on_tick = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_on_tick", "(Ledge_cases/BatchListener;J)V"); - if (Jni_mid_edge_cases_BatchListener_on_tick == NULL) { - return JNI_ERR; - } - Jni_mid_edge_cases_BatchListener_should_continue = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_should_continue", "(Ledge_cases/BatchListener;JI)Z"); - if (Jni_mid_edge_cases_BatchListener_should_continue == NULL) { - return JNI_ERR; - } - Jni_mid_edge_cases_BatchListener_pick_mode = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_pick_mode", "(Ledge_cases/BatchListener;J)I"); - if (Jni_mid_edge_cases_BatchListener_pick_mode == NULL) { - return JNI_ERR; - } - Jni_mid_edge_cases_Keyword_delete = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_Keyword_delete", "(Ledge_cases/Keyword;JI)V"); - if (Jni_mid_edge_cases_Keyword_delete == NULL) { - return JNI_ERR; - } - Jni_mid_edge_cases_Keyword_import = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_Keyword_import", "(Ledge_cases/Keyword;J)I"); - if (Jni_mid_edge_cases_Keyword_import == NULL) { - return JNI_ERR; - } - return JNI_OK; -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1clone(JNIEnv* env, jclass cls, jlong self) { - (void)env; - (void)cls; - return (jlong)(intptr_t)edge_cases_edge_cases_Widget_clone((const edge_cases_edge_cases_Widget*)(intptr_t)self); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1destroy(JNIEnv* env, jclass cls, jlong self) { - (void)env; - (void)cls; - edge_cases_edge_cases_Widget_destroy((edge_cases_edge_cases_Widget*)(intptr_t)self); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1clone(JNIEnv* env, jclass cls, jlong self) { - (void)env; - (void)cls; - return (jlong)(intptr_t)edge_cases_edge_cases_Vault_clone((const edge_cases_edge_cases_Vault*)(intptr_t)self); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1destroy(JNIEnv* env, jclass cls, jlong self) { - (void)env; - (void)cls; - edge_cases_edge_cases_Vault_destroy((edge_cases_edge_cases_Vault*)(intptr_t)self); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1noop(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_noop(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return; - } -} - -JNIEXPORT jint JNICALL Java_edge_1cases_JniBridge_edge_1cases_1select(JNIEnv* env, jclass cls, jint p_class, jbyteArray p_from, jbyteArray p_func, jbyteArray p_range, jboolean p_type_) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_from_b = Jni_borrow_bytes(env, p_from); - Jni_bytes p_func_b = Jni_borrow_bytes(env, p_func); - Jni_bytes p_range_b = Jni_borrow_bytes(env, p_range); - int32_t rv = edge_cases_edge_cases_select((int32_t)p_class, p_from_b.ptr, p_from_b.len, p_func_b.ptr, p_func_b.len, p_range_b.ptr, p_range_b.len, p_type_ == JNI_TRUE, &err); - Jni_release_bytes(env, &p_from_b); - Jni_release_bytes(env, &p_func_b); - Jni_release_bytes(env, &p_range_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jint)rv; -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1nested_1optional(JNIEnv* env, jclass cls, jbyteArray p_items) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_items_b = Jni_borrow_bytes(env, p_items); - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_nested_optional(p_items_b.ptr, p_items_b.len, &out_len, &err); - Jni_release_bytes(env, &p_items_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1optional_1params(JNIEnv* env, jclass cls, jbyteArray p_text, jbyteArray p_count, jbyteArray p_outer, jbyteArray p_tags, jbyteArray p_blob, jbyteArray p_mode) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_text_b = Jni_borrow_bytes(env, p_text); - Jni_bytes p_count_b = Jni_borrow_bytes(env, p_count); - Jni_bytes p_outer_b = Jni_borrow_bytes(env, p_outer); - Jni_bytes p_tags_b = Jni_borrow_bytes(env, p_tags); - Jni_bytes p_blob_b = Jni_borrow_bytes(env, p_blob); - Jni_bytes p_mode_b = Jni_borrow_bytes(env, p_mode); - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_optional_params(p_text_b.ptr, p_text_b.len, p_count_b.ptr, p_count_b.len, p_outer_b.ptr, p_outer_b.len, p_tags_b.ptr, p_tags_b.len, p_blob_b.ptr, p_blob_b.len, p_mode_b.ptr, p_mode_b.len, &out_len, &err); - Jni_release_bytes(env, &p_text_b); - Jni_release_bytes(env, &p_count_b); - Jni_release_bytes(env, &p_outer_b); - Jni_release_bytes(env, &p_tags_b); - Jni_release_bytes(env, &p_blob_b); - Jni_release_bytes(env, &p_mode_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1register_1batch(JNIEnv* env, jclass cls, jobject p_listener, jobject p_keyword) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_register_batch((void*)(*env)->NewGlobalRef(env, p_listener), &Jni_edge_cases_BatchListener_vtable, (void*)(*env)->NewGlobalRef(env, p_keyword), &Jni_edge_cases_Keyword_vtable, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return; - } -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1widgets_1everywhere(JNIEnv* env, jclass cls, jbyteArray p_list, jbyteArray p_by_name, jbyteArray p_sparse, jbyteArray p_holder) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_list_b = Jni_borrow_bytes(env, p_list); - Jni_bytes p_by_name_b = Jni_borrow_bytes(env, p_by_name); - Jni_bytes p_sparse_b = Jni_borrow_bytes(env, p_sparse); - Jni_bytes p_holder_b = Jni_borrow_bytes(env, p_holder); - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_widgets_everywhere(p_list_b.ptr, p_list_b.len, p_by_name_b.ptr, p_by_name_b.len, p_sparse_b.ptr, p_sparse_b.len, p_holder_b.ptr, p_holder_b.len, &out_len, &err); - Jni_release_bytes(env, &p_list_b); - Jni_release_bytes(env, &p_by_name_b); - Jni_release_bytes(env, &p_sparse_b); - Jni_release_bytes(env, &p_holder_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1widget_1stream(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_WidgetStreamIterator* rv = edge_cases_edge_cases_widget_stream(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1WidgetStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter, jlongArray out) { - edge_cases_error err = {0, NULL, NULL, 0}; - edge_cases_edge_cases_Widget* item = (edge_cases_edge_cases_Widget*)0; - (void)cls; - int32_t has = edge_cases_edge_cases_WidgetStreamIterator_next((edge_cases_edge_cases_WidgetStreamIterator*)(intptr_t)iter, &item, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return JNI_FALSE; - } - if (has == 0) { - return JNI_FALSE; - } - jlong value = (jlong)(intptr_t)item; - (*env)->SetLongArrayRegion(env, out, 0, 1, &value); - return JNI_TRUE; -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1WidgetStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { - (void)env; - (void)cls; - edge_cases_edge_cases_WidgetStreamIterator_destroy((edge_cases_edge_cases_WidgetStreamIterator*)(intptr_t)iter); -} - -static void Jni_done_edge_cases_async_widget(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_long, (jlong)(intptr_t)result); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1widget(JNIEnv* env, jclass cls, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_widget(Jni_done_edge_cases_async_widget, context); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1make_1widget(JNIEnv* env, jclass cls, jbyteArray p_label) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_label_b = Jni_borrow_bytes(env, p_label); - edge_cases_edge_cases_Widget* rv = edge_cases_edge_cases_make_widget(p_label_b.ptr, p_label_b.len, &err); - Jni_release_bytes(env, &p_label_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1find_1widget(JNIEnv* env, jclass cls, jlong p_id) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_Widget* rv = edge_cases_edge_cases_find_widget((int64_t)p_id, &err); - if (err.code != 0) { - Jni_throw(env, &err, 1); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1inspect_1widget(JNIEnv* env, jclass cls, jlong p_widget, jlong p_fallback) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_inspect_widget((const edge_cases_edge_cases_Widget*)(intptr_t)p_widget, (const edge_cases_edge_cases_Widget*)(intptr_t)p_fallback, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1string_1stream(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_StringStreamIterator* rv = edge_cases_edge_cases_string_stream(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1StringStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter) { - edge_cases_error err = {0, NULL, NULL, 0}; - const uint8_t* item = NULL; - size_t out_len = 0; - (void)cls; - int32_t has = edge_cases_edge_cases_StringStreamIterator_next((edge_cases_edge_cases_StringStreamIterator*)(intptr_t)iter, &item, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - if (has == 0) { - return NULL; - } - return Jni_take_bytes(env, item, out_len); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1StringStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { - (void)env; - (void)cls; - edge_cases_edge_cases_StringStreamIterator_destroy((edge_cases_edge_cases_StringStreamIterator*)(intptr_t)iter); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1scalar_1stream(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_ScalarStreamIterator* rv = edge_cases_edge_cases_scalar_stream(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ScalarStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter, jintArray out) { - edge_cases_error err = {0, NULL, NULL, 0}; - int32_t item = (int32_t)0; - (void)cls; - int32_t has = edge_cases_edge_cases_ScalarStreamIterator_next((edge_cases_edge_cases_ScalarStreamIterator*)(intptr_t)iter, &item, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return JNI_FALSE; - } - if (has == 0) { - return JNI_FALSE; - } - jint value = (jint)item; - (*env)->SetIntArrayRegion(env, out, 0, 1, &value); - return JNI_TRUE; -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ScalarStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { - (void)env; - (void)cls; - edge_cases_edge_cases_ScalarStreamIterator_destroy((edge_cases_edge_cases_ScalarStreamIterator*)(intptr_t)iter); -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1mode_1stream(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_ModeStreamIterator* rv = edge_cases_edge_cases_mode_stream(&err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ModeStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter, jintArray out) { - edge_cases_error err = {0, NULL, NULL, 0}; - edge_cases_edge_cases_Mode item = (edge_cases_edge_cases_Mode)0; - (void)cls; - int32_t has = edge_cases_edge_cases_ModeStreamIterator_next((edge_cases_edge_cases_ModeStreamIterator*)(intptr_t)iter, &item, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return JNI_FALSE; - } - if (has == 0) { - return JNI_FALSE; - } - jint value = (jint)item; - (*env)->SetIntArrayRegion(env, out, 0, 1, &value); - return JNI_TRUE; -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ModeStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { - (void)env; - (void)cls; - edge_cases_edge_cases_ModeStreamIterator_destroy((edge_cases_edge_cases_ModeStreamIterator*)(intptr_t)iter); -} - -static void Jni_done_edge_cases_async_void(void* context, edge_cases_error* err) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_unit); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1void(JNIEnv* env, jclass cls, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_void(Jni_done_edge_cases_async_void, context); -} - -static void Jni_done_edge_cases_async_bytes(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1bytes(JNIEnv* env, jclass cls, jint p_size, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_bytes((uint32_t)p_size, Jni_done_edge_cases_async_bytes, context); -} - -static void Jni_done_edge_cases_async_struct(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1struct(JNIEnv* env, jclass cls, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_struct(Jni_done_edge_cases_async_struct, context); -} - -static void Jni_done_edge_cases_async_list(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1list(JNIEnv* env, jclass cls, jlong cancel_token, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_list((edge_cases_cancel_token*)(intptr_t)cancel_token, Jni_done_edge_cases_async_list, context); -} - -static void Jni_done_edge_cases_async_optional(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1optional(JNIEnv* env, jclass cls, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_optional(Jni_done_edge_cases_async_optional, context); -} - -static void Jni_done_edge_cases_async_enum(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_int, (jint)result); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1enum(JNIEnv* env, jclass cls, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_async_enum(Jni_done_edge_cases_async_enum, context); -} - -static void Jni_done_edge_cases_async_keywords(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1keywords(JNIEnv* env, jclass cls, jbyteArray p_class, jint p_import, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - Jni_bytes p_class_b = Jni_borrow_bytes(env, p_class); - edge_cases_edge_cases_async_keywords(p_class_b.ptr, p_class_b.len, (int32_t)p_import, Jni_done_edge_cases_async_keywords, context); - Jni_release_bytes(env, &p_class_b); -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1keywords_1roundtrip(JNIEnv* env, jclass cls, jbyteArray p_value) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_value_b = Jni_borrow_bytes(env, p_value); - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_keywords_roundtrip(p_value_b.ptr, p_value_b.len, &out_len, &err); - Jni_release_bytes(env, &p_value_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1legacy_1value(JNIEnv* env, jclass cls, jint p_value) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_legacy_value((edge_cases_edge_cases_Legacy)p_value, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1refresh(JNIEnv* env, jclass cls, jlong self) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_Widget_refresh((const edge_cases_edge_cases_Widget*)(intptr_t)self, &err); - if (err.code != 0) { - Jni_throw(env, &err, 1); - return; - } -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1rename(JNIEnv* env, jclass cls, jlong self, jbyteArray p_class, jbyteArray p_import) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_class_b = Jni_borrow_bytes(env, p_class); - Jni_bytes p_import_b = Jni_borrow_bytes(env, p_import); - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_Widget_rename((const edge_cases_edge_cases_Widget*)(intptr_t)self, p_class_b.ptr, p_class_b.len, p_import_b.ptr, p_import_b.len, &out_len, &err); - Jni_release_bytes(env, &p_class_b); - Jni_release_bytes(env, &p_import_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -static void Jni_done_edge_cases_Widget_fetch(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - int detach = 0; - JNIEnv* env = Jni_complete_begin(context, err, &detach); - if (env == NULL) { - return; - } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1fetch(JNIEnv* env, jclass cls, jlong self, jlong p_id, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); - (void)cls; - edge_cases_edge_cases_Widget_fetch((const edge_cases_edge_cases_Widget*)(intptr_t)self, (int64_t)p_id, Jni_done_edge_cases_Widget_fetch, context); -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1old_1name(JNIEnv* env, jclass cls, jlong self) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_Widget_old_name((const edge_cases_edge_cases_Widget*)(intptr_t)self, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1close(JNIEnv* env, jclass cls, jlong self) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - bool rv = edge_cases_edge_cases_Widget_close((const edge_cases_edge_cases_Widget*)(intptr_t)self, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1dispose(JNIEnv* env, jclass cls, jlong self, jboolean p_force) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_Widget_dispose((const edge_cases_edge_cases_Widget*)(intptr_t)self, p_force == JNI_TRUE, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return; - } -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1open(JNIEnv* env, jclass cls, jbyteArray p_path) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_path_b = Jni_borrow_bytes(env, p_path); - edge_cases_edge_cases_Vault* rv = edge_cases_edge_cases_Vault_open(p_path_b.ptr, p_path_b.len, &err); - Jni_release_bytes(env, &p_path_b); - if (err.code != 0) { - Jni_throw(env, &err, 1); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1peek(JNIEnv* env, jclass cls, jlong self) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - edge_cases_edge_cases_Widget* rv = edge_cases_edge_cases_Vault_peek((const edge_cases_edge_cases_Vault*)(intptr_t)self, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1handle(JNIEnv* env, jclass cls, jlong self) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - int64_t rv = edge_cases_edge_cases_Vault_handle((const edge_cases_edge_cases_Vault*)(intptr_t)self, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)rv; -} - -JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1probe(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - bool rv = edge_cases_edge_cases_Vault_probe(&err); - if (err.code != 0) { - Jni_throw(env, &err, 1); - return 0; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1to_1string(JNIEnv* env, jclass cls) { - edge_cases_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = edge_cases_edge_cases_Vault_to_string(&out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - - -// END edge_cases_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Codecs_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Codecs_kt.snap deleted file mode 100644 index cdeb529b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Codecs_kt.snap +++ /dev/null @@ -1,104 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -@file:Suppress("DEPRECATION") - -package edge_cases - -internal fun packListOfI32(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeI32(it) } - -internal fun unpackListOfI32(_r: BufferReader): List = _r.readList { _r.readI32() } - -internal fun packListOfLeaf(_w: BufferWriter, _v: List) = _w.writeList(_v) { packLeaf(_w, it) } - -internal fun unpackListOfLeaf(_r: BufferReader): List = _r.readList { unpackLeaf(_r) } - -internal fun packListOfListOfI32(_w: BufferWriter, _v: List>) = _w.writeList(_v) { packListOfI32(_w, it) } - -internal fun unpackListOfListOfI32(_r: BufferReader): List> = _r.readList { unpackListOfI32(_r) } - -internal fun packListOfMode(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeI32(it.value) } - -internal fun unpackListOfMode(_r: BufferReader): List = _r.readList { Mode.fromValue(_r.readI32()) } - -internal fun packListOfOptionalOfI64(_w: BufferWriter, _v: List) = _w.writeList(_v) { packOptionalOfI64(_w, it) } - -internal fun unpackListOfOptionalOfI64(_r: BufferReader): List = _r.readList { unpackOptionalOfI64(_r) } - -internal fun packListOfOptionalOfOuter(_w: BufferWriter, _v: List) = _w.writeList(_v) { packOptionalOfOuter(_w, it) } - -internal fun unpackListOfOptionalOfOuter(_r: BufferReader): List = _r.readList { unpackOptionalOfOuter(_r) } - -internal fun packListOfOptionalOfWidget(_w: BufferWriter, _v: List) = _w.writeList(_v) { packOptionalOfWidget(_w, it) } - -internal fun unpackListOfOptionalOfWidget(_r: BufferReader): List = _r.readList { unpackOptionalOfWidget(_r) } - -internal fun packListOfOuter(_w: BufferWriter, _v: List) = _w.writeList(_v) { packOuter(_w, it) } - -internal fun unpackListOfOuter(_r: BufferReader): List = _r.readList { unpackOuter(_r) } - -internal fun packListOfString(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeString(it) } - -internal fun unpackListOfString(_r: BufferReader): List = _r.readList { _r.readString() } - -internal fun packListOfWidget(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeObject(it.cloneHandle(), JniBridge::edge_cases_Widget_destroy) } - -internal fun unpackListOfWidget(_r: BufferReader): List = _r.readList { Widget.fromHandle(_r.readObject()) } - -internal fun packMapOfI64ToString(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeI64(it) }, { _w.writeString(it) }) - -internal fun unpackMapOfI64ToString(_r: BufferReader): Map = _r.readMap({ _r.readI64() }, { _r.readString() }) - -internal fun packMapOfStringToInner(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeString(it) }, { packInner(_w, it) }) - -internal fun unpackMapOfStringToInner(_r: BufferReader): Map = _r.readMap({ _r.readString() }, { unpackInner(_r) }) - -internal fun packMapOfStringToWidget(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeString(it) }, { _w.writeObject(it.cloneHandle(), JniBridge::edge_cases_Widget_destroy) }) - -internal fun unpackMapOfStringToWidget(_r: BufferReader): Map = _r.readMap({ _r.readString() }, { Widget.fromHandle(_r.readObject()) }) - -internal fun packOptionalOfBytes(_w: BufferWriter, _v: ByteArray?) = _w.writeOptional(_v) { _w.writeBytes(it) } - -internal fun unpackOptionalOfBytes(_r: BufferReader): ByteArray? = _r.readOptional { _r.readBytes() } - -internal fun packOptionalOfI32(_w: BufferWriter, _v: Int?) = _w.writeOptional(_v) { _w.writeI32(it) } - -internal fun unpackOptionalOfI32(_r: BufferReader): Int? = _r.readOptional { _r.readI32() } - -internal fun packOptionalOfI64(_w: BufferWriter, _v: Long?) = _w.writeOptional(_v) { _w.writeI64(it) } - -internal fun unpackOptionalOfI64(_r: BufferReader): Long? = _r.readOptional { _r.readI64() } - -internal fun packOptionalOfInner(_w: BufferWriter, _v: Inner?) = _w.writeOptional(_v) { packInner(_w, it) } - -internal fun unpackOptionalOfInner(_r: BufferReader): Inner? = _r.readOptional { unpackInner(_r) } - -internal fun packOptionalOfListOfOptionalOfOuter(_w: BufferWriter, _v: List?) = _w.writeOptional(_v) { packListOfOptionalOfOuter(_w, it) } - -internal fun unpackOptionalOfListOfOptionalOfOuter(_r: BufferReader): List? = _r.readOptional { unpackListOfOptionalOfOuter(_r) } - -internal fun packOptionalOfListOfString(_w: BufferWriter, _v: List?) = _w.writeOptional(_v) { packListOfString(_w, it) } - -internal fun unpackOptionalOfListOfString(_r: BufferReader): List? = _r.readOptional { unpackListOfString(_r) } - -internal fun packOptionalOfMode(_w: BufferWriter, _v: Mode?) = _w.writeOptional(_v) { _w.writeI32(it.value) } - -internal fun unpackOptionalOfMode(_r: BufferReader): Mode? = _r.readOptional { Mode.fromValue(_r.readI32()) } - -internal fun packOptionalOfOuter(_w: BufferWriter, _v: Outer?) = _w.writeOptional(_v) { packOuter(_w, it) } - -internal fun unpackOptionalOfOuter(_r: BufferReader): Outer? = _r.readOptional { unpackOuter(_r) } - -internal fun packOptionalOfString(_w: BufferWriter, _v: String?) = _w.writeOptional(_v) { _w.writeString(it) } - -internal fun unpackOptionalOfString(_r: BufferReader): String? = _r.readOptional { _r.readString() } - -internal fun packOptionalOfWidget(_w: BufferWriter, _v: Widget?) = _w.writeOptional(_v) { _w.writeObject(it.cloneHandle(), JniBridge::edge_cases_Widget_destroy) } - -internal fun unpackOptionalOfWidget(_r: BufferReader): Widget? = _r.readOptional { Widget.fromHandle(_r.readObject()) } - -// END Codecs.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_EdgeCases_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_EdgeCases_kt.snap deleted file mode 100644 index e684975f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_EdgeCases_kt.snap +++ /dev/null @@ -1,476 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -@file:Suppress("DEPRECATION") - -package edge_cases - -/** Errors of the `EdgeErrors` domain. */ -sealed class EdgeErrorsException(code: Int, message: String) : FfiException(code, message) { - class Busy(message: String = "Resource is busy") : EdgeErrorsException(7, message) - - internal companion object { - /** The exception for a raw code and its serialized payload. */ - fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { - 7 -> Busy(message) - else -> FfiException(code, message) - } - } -} - -/** Deprecated enum with non-contiguous values and keyword-like variants */ -@Deprecated("Use Mode instead") -enum class Legacy(val value: Int) { - None(0), - Default(5), - Class(10); - - companion object { - /** The variant whose ABI value is [value]. */ - fun fromValue(value: Int): Legacy = entries.firstOrNull { it.value == value } - ?: throw NativeBugException(-3, "unknown Legacy value $value") - } -} - -enum class Mode(val value: Int) { - Fast(1), - Safe(2); - - companion object { - /** The variant whose ABI value is [value]. */ - fun fromValue(value: Int): Mode = entries.firstOrNull { it.value == value } - ?: throw NativeBugException(-3, "unknown Mode value $value") - } -} - -data class Leaf(val weight: Float) - -internal fun packLeaf(_w: BufferWriter, _v: Leaf) { - _w.writeF32(_v.weight) -} - -internal fun unpackLeaf(_r: BufferReader): Leaf = Leaf( - _r.readF32(), -) - -/** Middle layer of a three-deep struct nesting */ -data class Inner(val leaf: Leaf, val leaves: List) - -internal fun packInner(_w: BufferWriter, _v: Inner) { - packLeaf(_w, _v.leaf) - packListOfLeaf(_w, _v.leaves) -} - -internal fun unpackInner(_r: BufferReader): Inner = Inner( - unpackLeaf(_r), - unpackListOfLeaf(_r), -) - -data class Outer(val inner: Inner, val maybe_inner: Inner?, val by_name: Map, val modes: List, val matrix: List>, val sparse: List, val blob: ByteArray?, val note: String?, val by_id: Map, val _hidden: Boolean, val v2: UByte) - -internal fun packOuter(_w: BufferWriter, _v: Outer) { - packInner(_w, _v.inner) - packOptionalOfInner(_w, _v.maybe_inner) - packMapOfStringToInner(_w, _v.by_name) - packListOfMode(_w, _v.modes) - packListOfListOfI32(_w, _v.matrix) - packListOfOptionalOfI64(_w, _v.sparse) - packOptionalOfBytes(_w, _v.blob) - packOptionalOfString(_w, _v.note) - packMapOfI64ToString(_w, _v.by_id) - _w.writeBool(_v._hidden) - _w.writeU8(_v.v2) -} - -internal fun unpackOuter(_r: BufferReader): Outer = Outer( - unpackInner(_r), - unpackOptionalOfInner(_r), - unpackMapOfStringToInner(_r), - unpackListOfMode(_r), - unpackListOfListOfI32(_r), - unpackListOfOptionalOfI64(_r), - unpackOptionalOfBytes(_r), - unpackOptionalOfString(_r), - unpackMapOfI64ToString(_r), - _r.readBool(), - _r.readU8(), -) - -/** Every field is a reserved word somewhere */ -data class Keywords(val class_: Int, val def: Int, val import: Int, val from: Int, val func: Int, val range: Int, val register: Int, val volatile: Int, val new: Int, val default: Int, val end: Int, val in_: Int, val is_: Int, val int: Int, val var_: Int, val val_: Int, val self: Int, val this_: Int, val object_: Int, val string: Int, val case: Int, val select: Int, val switch: Int, val void: Int, val union: Int, val namespace: Int, val operator: Int, val template: Int, val package_: Int, val protocol: Int, val guard: Int, val defer: Int, val inout: Int, val internal: Int, val when_: Int, val fun_: Int, val data: Int, val late: Int, val part: Int, val out: Int, val ref: Int, val event: Int, val base: Int, val lambda: Int, val yield: Int, val nil: Int, val null_: Int, val begin: Int, val rescue: Int, val module: Int, val chan: Int, val go: Int, val let: Int, val export: Int, val delete: Int, val typedef: Int, val sizeof: Int, val static: Int, val super_: Int) - -internal fun packKeywords(_w: BufferWriter, _v: Keywords) { - _w.writeI32(_v.class_) - _w.writeI32(_v.def) - _w.writeI32(_v.import) - _w.writeI32(_v.from) - _w.writeI32(_v.func) - _w.writeI32(_v.range) - _w.writeI32(_v.register) - _w.writeI32(_v.volatile) - _w.writeI32(_v.new) - _w.writeI32(_v.default) - _w.writeI32(_v.end) - _w.writeI32(_v.in_) - _w.writeI32(_v.is_) - _w.writeI32(_v.int) - _w.writeI32(_v.var_) - _w.writeI32(_v.val_) - _w.writeI32(_v.self) - _w.writeI32(_v.this_) - _w.writeI32(_v.object_) - _w.writeI32(_v.string) - _w.writeI32(_v.case) - _w.writeI32(_v.select) - _w.writeI32(_v.switch) - _w.writeI32(_v.void) - _w.writeI32(_v.union) - _w.writeI32(_v.namespace) - _w.writeI32(_v.operator) - _w.writeI32(_v.template) - _w.writeI32(_v.package_) - _w.writeI32(_v.protocol) - _w.writeI32(_v.guard) - _w.writeI32(_v.defer) - _w.writeI32(_v.inout) - _w.writeI32(_v.internal) - _w.writeI32(_v.when_) - _w.writeI32(_v.fun_) - _w.writeI32(_v.data) - _w.writeI32(_v.late) - _w.writeI32(_v.part) - _w.writeI32(_v.out) - _w.writeI32(_v.ref) - _w.writeI32(_v.event) - _w.writeI32(_v.base) - _w.writeI32(_v.lambda) - _w.writeI32(_v.yield) - _w.writeI32(_v.nil) - _w.writeI32(_v.null_) - _w.writeI32(_v.begin) - _w.writeI32(_v.rescue) - _w.writeI32(_v.module) - _w.writeI32(_v.chan) - _w.writeI32(_v.go) - _w.writeI32(_v.let) - _w.writeI32(_v.export) - _w.writeI32(_v.delete) - _w.writeI32(_v.typedef) - _w.writeI32(_v.sizeof) - _w.writeI32(_v.static) - _w.writeI32(_v.super_) -} - -internal fun unpackKeywords(_r: BufferReader): Keywords = Keywords( - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), - _r.readI32(), -) - -/** Deprecated struct */ -@Deprecated("Use Outer instead") -data class Retired(val id: Long) - -internal fun packRetired(_w: BufferWriter, _v: Retired) { - _w.writeI64(_v.id) -} - -internal fun unpackRetired(_r: BufferReader): Retired = Retired( - _r.readI64(), -) - -/** Record carrying objects in every buffered position */ -data class WidgetHolder(val primary: Widget, val fallback: Widget?, val extras: List, val by_name: Map) - -internal fun packWidgetHolder(_w: BufferWriter, _v: WidgetHolder) { - _w.writeObject(_v.primary.cloneHandle(), JniBridge::edge_cases_Widget_destroy) - packOptionalOfWidget(_w, _v.fallback) - packListOfWidget(_w, _v.extras) - packMapOfStringToWidget(_w, _v.by_name) -} - -internal fun unpackWidgetHolder(_r: BufferReader): WidgetHolder = WidgetHolder( - Widget.fromHandle(_r.readObject()), - unpackOptionalOfWidget(_r), - unpackListOfWidget(_r), - unpackMapOfStringToWidget(_r), -) - -/** Callback interface carrying composite payloads and objects */ -interface BatchListener { - /** Composite payloads cross as value buffers */ - fun onBatch(items: List, raw: ByteArray, first: Outer?, mode: Mode) - - /** Objects cross as strong references the consumer must release */ - fun onWidget(widget: Widget, fallback: Widget?, children: List) - - fun onTick() - - /** Scalar return travels back through the vtable */ - fun shouldContinue(class_: Int): Boolean - - /** Enum return travels back as its integer discriminant */ - fun pickMode(): Mode -} - -/** Callback interface whose name and methods are keywords */ -interface Keyword { - fun delete(self: Int) - - fun import(): Int -} - -/** Interface with no constructor; instances come from make_widget */ -class Widget private constructor(address: Long) : AutoCloseable { - internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::edge_cases_Widget_destroy)) - - /** A new strong reference to this object, as written into a value buffer. */ - internal fun cloneHandle(): Long = handle.borrow { JniBridge.edge_cases_Widget_clone(it) } - - /** Void method that can fail */ - fun refresh() { - handle.borrow { _self -> - JniBridge.edge_cases_Widget_refresh(_self) - } - } - - fun rename(class_: String, import: String?): String = handle.borrow { _self -> - decodeUtf8(JniBridge.edge_cases_Widget_rename(_self, encodeUtf8(class_), encodeBuffer { _w -> packOptionalOfString(_w, import) })) - } - - /** Async method with a struct result */ - suspend fun fetch(id: Long): Outer = awaitNative(false, 1, { _raw -> decodeBuffer(_raw as ByteArray) { _r -> unpackOuter(_r) } }) { _, _done -> - handle.borrow { _self -> - JniBridge.edge_cases_Widget_fetch(_self, id, _done) - } - } - - @Deprecated("Use rename instead") - fun oldName(): String = handle.borrow { _self -> - decodeUtf8(JniBridge.edge_cases_Widget_old_name(_self)) - } - - /** Spelled like the release method of several wrappers */ - fun close_(): Boolean = handle.borrow { _self -> - JniBridge.edge_cases_Widget_close(_self) - } - - /** Spelled like the release method of the Dart and .NET wrappers */ - fun dispose(force: Boolean) { - handle.borrow { _self -> - JniBridge.edge_cases_Widget_dispose(_self, force) - } - } - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() = handle.close() - - companion object { - /** Adopts one strong reference (a return value or a buffer token). */ - internal fun fromHandle(address: Long): Widget = Widget(address) - - /** [fromHandle], with address 0 meaning no object. */ - internal fun fromHandleOrNull(address: Long): Widget? = if (address == 0L) null else Widget(address) - } -} - -/** Deprecated interface with a throwing constructor */ -@Deprecated("Use Widget instead") -class Vault private constructor(address: Long) : AutoCloseable { - internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::edge_cases_Vault_destroy)) - - /** A new strong reference to this object, as written into a value buffer. */ - internal fun cloneHandle(): Long = handle.borrow { JniBridge.edge_cases_Vault_clone(it) } - - fun peek(): Widget? = handle.borrow { _self -> - Widget.fromHandleOrNull(JniBridge.edge_cases_Vault_peek(_self)) - } - - /** Spelled like the handle accessor of several wrappers */ - fun handle_(): Long = handle.borrow { _self -> - JniBridge.edge_cases_Vault_handle(_self) - } - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() = handle.close() - - companion object { - /** Adopts one strong reference (a return value or a buffer token). */ - internal fun fromHandle(address: Long): Vault = Vault(address) - - /** [fromHandle], with address 0 meaning no object. */ - internal fun fromHandleOrNull(address: Long): Vault? = if (address == 0L) null else Vault(address) - - /** Fails when the vault is busy */ - fun open(path: String): Vault = Vault.fromHandle(JniBridge.edge_cases_Vault_open(encodeUtf8(path))) - - /** Throwing static */ - fun probe(): Boolean = JniBridge.edge_cases_Vault_probe() - - /** Static spelled like an inherited object member */ - fun toString_(): String = decodeUtf8(JniBridge.edge_cases_Vault_to_string()) - } -} - -object EdgeCases { - /** No parameters and no result */ - fun noop() { - JniBridge.edge_cases_noop() - } - - /** Function named after a keyword whose parameters are all keywords */ - fun select(class_: Int, from: String, func: List, range: Int?, type: Boolean): Int = JniBridge.edge_cases_select(class_, encodeUtf8(from), encodeBuffer { _w -> packListOfString(_w, func) }, encodeBuffer { _w -> packOptionalOfI32(_w, range) }, type) - - /** Optional list of optional structs, the deepest legal nesting */ - fun nestedOptional(items: List?): List? = decodeBuffer(JniBridge.edge_cases_nested_optional(encodeBuffer { _w -> packOptionalOfListOfOptionalOfOuter(_w, items) })) { _r -> unpackOptionalOfListOfOptionalOfOuter(_r) } - - fun optionalParams(text: String?, count: Int?, outer: Outer?, tags: List?, blob: ByteArray?, mode: Mode?): Mode? = decodeBuffer(JniBridge.edge_cases_optional_params(encodeBuffer { _w -> packOptionalOfString(_w, text) }, encodeBuffer { _w -> packOptionalOfI32(_w, count) }, encodeBuffer { _w -> packOptionalOfOuter(_w, outer) }, encodeBuffer { _w -> packOptionalOfListOfString(_w, tags) }, encodeBuffer { _w -> packOptionalOfBytes(_w, blob) }, encodeBuffer { _w -> packOptionalOfMode(_w, mode) })) { _r -> unpackOptionalOfMode(_r) } - - /** Two callback interface parameters on one function */ - fun registerBatch(listener: BatchListener, keyword: Keyword) { - JniBridge.edge_cases_register_batch(listener, keyword) - } - - /** Objects in every buffered position */ - fun widgetsEverywhere(list: List, byName: Map, sparse: List, holder: WidgetHolder): WidgetHolder = decodeBuffer(JniBridge.edge_cases_widgets_everywhere(encodeBuffer { _w -> packListOfWidget(_w, list) }, encodeBuffer { _w -> packMapOfStringToWidget(_w, byName) }, encodeBuffer { _w -> packListOfOptionalOfWidget(_w, sparse) }, encodeBuffer { _w -> packWidgetHolder(_w, holder) })) { _r -> unpackWidgetHolder(_r) } - - /** Iterator of objects */ - fun widgetStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_widget_stream(), JniBridge::edge_cases_WidgetStreamIterator_destroy) { _it -> - val _slot = LongArray(1) - if (JniBridge.edge_cases_WidgetStreamIterator_next(_it, _slot)) Widget.fromHandle(_slot[0]) else NativeIterator.DONE - } - - /** Async returning an object */ - suspend fun asyncWidget(): Widget = awaitNative(false, 1, { _raw -> Widget.fromHandle(_raw as Long) }) { _, _done -> - JniBridge.edge_cases_async_widget(_done) - } - - /** Factory for an interface with no constructor */ - fun makeWidget(label: String): Widget = Widget.fromHandle(JniBridge.edge_cases_make_widget(encodeUtf8(label))) - - /** Optional interface return */ - fun findWidget(id: Long): Widget? = Widget.fromHandleOrNull(JniBridge.edge_cases_find_widget(id)) - - /** Interface parameter, including an optional one */ - fun inspectWidget(widget: Widget, fallback: Widget?): String = widget.handle.borrow { _h0 -> - fallback?.handle.borrowOrNull { _h1 -> - decodeUtf8(JniBridge.edge_cases_inspect_widget(_h0, _h1)) - } - } - - /** Iterator of strings */ - fun stringStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_string_stream(), JniBridge::edge_cases_StringStreamIterator_destroy) { _it -> - val _item = JniBridge.edge_cases_StringStreamIterator_next(_it) - if (_item == null) NativeIterator.DONE else decodeUtf8(_item) - } - - /** Iterator of scalars */ - fun scalarStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_scalar_stream(), JniBridge::edge_cases_ScalarStreamIterator_destroy) { _it -> - val _slot = IntArray(1) - if (JniBridge.edge_cases_ScalarStreamIterator_next(_it, _slot)) _slot[0] else NativeIterator.DONE - } - - /** Iterator of enums */ - fun modeStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_mode_stream(), JniBridge::edge_cases_ModeStreamIterator_destroy) { _it -> - val _slot = IntArray(1) - if (JniBridge.edge_cases_ModeStreamIterator_next(_it, _slot)) Mode.fromValue(_slot[0]) else NativeIterator.DONE - } - - /** Async with no result */ - suspend fun asyncVoid() { - awaitNative(false, 0, { }) { _, _done -> - JniBridge.edge_cases_async_void(_done) - } - } - - suspend fun asyncBytes(size: UInt): ByteArray = awaitNative(false, 0, { _raw -> _raw as ByteArray }) { _, _done -> - JniBridge.edge_cases_async_bytes(size.toInt(), _done) - } - - suspend fun asyncStruct(): Outer = awaitNative(false, 1, { _raw -> decodeBuffer(_raw as ByteArray) { _r -> unpackOuter(_r) } }) { _, _done -> - JniBridge.edge_cases_async_struct(_done) - } - - suspend fun asyncList(): List = awaitNative(true, 1, { _raw -> decodeBuffer(_raw as ByteArray) { _r -> unpackListOfOuter(_r) } }) { _token, _done -> - JniBridge.edge_cases_async_list(_token, _done) - } - - suspend fun asyncOptional(): Long? = awaitNative(false, 0, { _raw -> decodeBuffer(_raw as ByteArray) { _r -> unpackOptionalOfI64(_r) } }) { _, _done -> - JniBridge.edge_cases_async_optional(_done) - } - - suspend fun asyncEnum(): Mode = awaitNative(false, 0, { _raw -> Mode.fromValue(_raw as Int) }) { _, _done -> - JniBridge.edge_cases_async_enum(_done) - } - - suspend fun asyncKeywords(class_: String, import: Int): String = awaitNative(false, 0, { _raw -> decodeUtf8(_raw as ByteArray) }) { _, _done -> - JniBridge.edge_cases_async_keywords(encodeUtf8(class_), import, _done) - } - - fun keywordsRoundtrip(value: Keywords): Keywords = decodeBuffer(JniBridge.edge_cases_keywords_roundtrip(encodeBuffer { _w -> packKeywords(_w, value) })) { _r -> unpackKeywords(_r) } - - @Deprecated("Use Mode-based APIs") - fun legacyValue(value: Legacy): Retired = decodeBuffer(JniBridge.edge_cases_legacy_value(value.value)) { _r -> unpackRetired(_r) } -} - -// END EdgeCases.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_JniBridge_kt.snap deleted file mode 100644 index 316d0774..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_JniBridge_kt.snap +++ /dev/null @@ -1,166 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -@file:Suppress("DEPRECATION") - -package edge_cases - -/** - * The JNI entry points of `libedge_cases_jni`: one native per C symbol, named after - * it, plus the hooks the shim calls back into. Internal plumbing for the - * public wrappers. - */ -internal object JniBridge { - init { - NativeLibrary.ensureLoaded() - } - - /** - * The exception for a native failure: through error domain [domain] for a - * throwing call, or a [NativeBugException] (domain 0) for one that can't fail. - */ - @JvmStatic - fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): Throwable { - val text = decodeUtf8(message) - return when (domain) { - 1 -> EdgeErrorsException.fromCode(code, text, payload) - else -> NativeBugException(code, text) - } - } - - /** - * Reports a failed callback through its vtable entry's `out_err` ([err]): - * [typed], a domain error the method is declared to throw, with its code and - * payload fields, and anything else as code -4 with its message. - */ - private fun fail(err: Long, error: Throwable, typed: FfiException?) { - if (typed != null) { - error_set(err, typed.code, encodeUtf8(typed.message ?: "")) - typed.encodePayload()?.let { error_set_payload(err, it) } - } else { - error_set(err, -4, encodeUtf8(error.message ?: error.toString())) - } - } - - @JvmStatic external fun error_set(err: Long, code: Int, message: ByteArray) - @JvmStatic external fun error_set_payload(err: Long, payload: ByteArray) - - @JvmStatic external fun debug_live(kind: Int): Long - @JvmStatic external fun cancel_token_create(): Long - @JvmStatic external fun cancel_token_cancel(token: Long) - @JvmStatic external fun cancel_token_destroy(token: Long) - @JvmStatic external fun edge_cases_Widget_clone(_self: Long): Long - @JvmStatic external fun edge_cases_Widget_destroy(_self: Long) - @JvmStatic external fun edge_cases_Vault_clone(_self: Long): Long - @JvmStatic external fun edge_cases_Vault_destroy(_self: Long) - @JvmStatic external fun edge_cases_noop() - @JvmStatic external fun edge_cases_select(class_: Int, from: ByteArray, func: ByteArray, range: ByteArray, type: Boolean): Int - @JvmStatic external fun edge_cases_nested_optional(items: ByteArray): ByteArray - @JvmStatic external fun edge_cases_optional_params(text: ByteArray, count: ByteArray, outer: ByteArray, tags: ByteArray, blob: ByteArray, mode: ByteArray): ByteArray - @JvmStatic external fun edge_cases_register_batch(listener: BatchListener, keyword: Keyword) - @JvmStatic external fun edge_cases_widgets_everywhere(list: ByteArray, byName: ByteArray, sparse: ByteArray, holder: ByteArray): ByteArray - @JvmStatic external fun edge_cases_widget_stream(): Long - @JvmStatic external fun edge_cases_WidgetStreamIterator_next(_iter: Long, _out: LongArray): Boolean - @JvmStatic external fun edge_cases_WidgetStreamIterator_destroy(_iter: Long) - @JvmStatic external fun edge_cases_async_widget(_done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_make_widget(label: ByteArray): Long - @JvmStatic external fun edge_cases_find_widget(id: Long): Long - @JvmStatic external fun edge_cases_inspect_widget(widget: Long, fallback: Long): ByteArray - @JvmStatic external fun edge_cases_string_stream(): Long - @JvmStatic external fun edge_cases_StringStreamIterator_next(_iter: Long): ByteArray? - @JvmStatic external fun edge_cases_StringStreamIterator_destroy(_iter: Long) - @JvmStatic external fun edge_cases_scalar_stream(): Long - @JvmStatic external fun edge_cases_ScalarStreamIterator_next(_iter: Long, _out: IntArray): Boolean - @JvmStatic external fun edge_cases_ScalarStreamIterator_destroy(_iter: Long) - @JvmStatic external fun edge_cases_mode_stream(): Long - @JvmStatic external fun edge_cases_ModeStreamIterator_next(_iter: Long, _out: IntArray): Boolean - @JvmStatic external fun edge_cases_ModeStreamIterator_destroy(_iter: Long) - @JvmStatic external fun edge_cases_async_void(_done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_async_bytes(size: Int, _done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_async_struct(_done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_async_list(_token: Long, _done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_async_optional(_done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_async_enum(_done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_async_keywords(class_: ByteArray, import: Int, _done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_keywords_roundtrip(value: ByteArray): ByteArray - @JvmStatic external fun edge_cases_legacy_value(value: Int): ByteArray - @JvmStatic external fun edge_cases_Widget_refresh(_self: Long) - @JvmStatic external fun edge_cases_Widget_rename(_self: Long, class_: ByteArray, import: ByteArray): ByteArray - @JvmStatic external fun edge_cases_Widget_fetch(_self: Long, id: Long, _done: NativeCompletion<*>) - @JvmStatic external fun edge_cases_Widget_old_name(_self: Long): ByteArray - @JvmStatic external fun edge_cases_Widget_close(_self: Long): Boolean - @JvmStatic external fun edge_cases_Widget_dispose(_self: Long, force: Boolean) - @JvmStatic external fun edge_cases_Vault_open(path: ByteArray): Long - @JvmStatic external fun edge_cases_Vault_peek(_self: Long): Long - @JvmStatic external fun edge_cases_Vault_handle(_self: Long): Long - @JvmStatic external fun edge_cases_Vault_probe(): Boolean - @JvmStatic external fun edge_cases_Vault_to_string(): ByteArray - - @JvmStatic - fun edge_cases_BatchListener_on_batch(_impl: BatchListener, _err: Long, items: ByteArray, raw: ByteArray, first: ByteArray, mode: Int) { - try { - _impl.onBatch(decodeBuffer(items) { _r -> unpackListOfOuter(_r) }, raw, decodeBuffer(first) { _r -> unpackOptionalOfOuter(_r) }, Mode.fromValue(mode)) - } catch (_e: Throwable) { - fail(_err, _e, null) - } - } - - @JvmStatic - fun edge_cases_BatchListener_on_widget(_impl: BatchListener, _err: Long, widget: Long, fallback: Long, children: ByteArray) { - val _widget = Widget.fromHandle(widget) - val _fallback = Widget.fromHandleOrNull(fallback) - try { - _impl.onWidget(_widget, _fallback, decodeBuffer(children) { _r -> unpackListOfWidget(_r) }) - } catch (_e: Throwable) { - fail(_err, _e, null) - } - } - - @JvmStatic - fun edge_cases_BatchListener_on_tick(_impl: BatchListener, _err: Long) { - try { - _impl.onTick() - } catch (_e: Throwable) { - fail(_err, _e, null) - } - } - - @JvmStatic - fun edge_cases_BatchListener_should_continue(_impl: BatchListener, _err: Long, class_: Int): Boolean = try { - _impl.shouldContinue(class_) - } catch (_e: Throwable) { - fail(_err, _e, null) - false - } - - @JvmStatic - fun edge_cases_BatchListener_pick_mode(_impl: BatchListener, _err: Long): Int = try { - _impl.pickMode().value - } catch (_e: Throwable) { - fail(_err, _e, null) - 0 - } - - @JvmStatic - fun edge_cases_Keyword_delete(_impl: Keyword, _err: Long, self: Int) { - try { - _impl.delete(self) - } catch (_e: Throwable) { - fail(_err, _e, null) - } - } - - @JvmStatic - fun edge_cases_Keyword_import(_impl: Keyword, _err: Long): Int = try { - _impl.import() - } catch (_e: Throwable) { - fail(_err, _e, null) - 0 - } -} - -// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap deleted file mode 100644 index e533c56b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap +++ /dev/null @@ -1,69 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -plugins { - id("com.android.library") version "8.7.3" - id("org.jetbrains.kotlin.android") version "2.0.21" - `maven-publish` -} - -group = "kitchen_sink" -version = "0.1.0" - -// A packaged build ships a prebuilt libkitchen_sink_jni.so next to the producer -// library in src/main/jniLibs//; without one, CMake builds it. -val prebuiltJni = fileTree("src/main/jniLibs") { include("**/libkitchen_sink_jni.so") }.files.isNotEmpty() - -android { - namespace = "kitchen_sink" - compileSdk = 35 - defaultConfig { - minSdk = 21 - consumerProguardFiles("consumer-rules.pro") - } - // Builds libkitchen_sink_jni.so per ABI, linked against the producer library in - // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). - if (!prebuiltJni) { - externalNativeBuild { - cmake { - path = file("src/main/cpp/CMakeLists.txt") - } - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - publishing { - singleVariant("release") { - withSourcesJar() - } - } -} - -kotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) - } -} - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") -} - -publishing { - publications { - register("release") { - artifactId = "kitchen_sink" - afterEvaluate { - from(components["release"]) - } - } - } -} - -// END build.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__consumer_rules_pro.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__consumer_rules_pro.snap deleted file mode 100644 index 62642aac..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__consumer_rules_pro.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -# The JNI shim looks classes and methods up by name. --keep class kitchen_sink.** { *; } - -# END consumer-rules.pro diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap deleted file mode 100644 index 176f7c5a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap +++ /dev/null @@ -1,25 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "kitchen_sink" - -// END settings.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap deleted file mode 100644 index 1f99f6c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -cmake_minimum_required(VERSION 3.18) -project(kitchen_sink_jni C) - -# The directory holding the producer library (libkitchen_sink). Android builds -# default to the per-ABI copy under src/main/jniLibs/. -set(KITCHEN_SINK_LIBRARY_DIR "" CACHE PATH "Directory holding the kitchen_sink producer library") -if(NOT KITCHEN_SINK_LIBRARY_DIR AND ANDROID) - set(KITCHEN_SINK_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") -endif() -if(KITCHEN_SINK_LIBRARY_DIR) - find_library(KITCHEN_SINK_PRODUCER NAMES kitchen_sink PATHS "${KITCHEN_SINK_LIBRARY_DIR}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) -else() - set(CMAKE_FIND_FRAMEWORK NEVER) - find_library(KITCHEN_SINK_PRODUCER NAMES kitchen_sink REQUIRED) -endif() - -add_library(kitchen_sink_jni SHARED kitchen_sink_jni.c) -target_include_directories(kitchen_sink_jni PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(kitchen_sink_jni PRIVATE "${KITCHEN_SINK_PRODUCER}") -if(NOT ANDROID) - find_package(JNI REQUIRED) - find_package(Threads REQUIRED) - target_include_directories(kitchen_sink_jni PRIVATE ${JNI_INCLUDE_DIRS}) - target_link_libraries(kitchen_sink_jni PRIVATE Threads::Threads) -endif() - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap index 8e24d0b9..eeb65b86 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap @@ -21,67 +21,166 @@ source: crates/weaveffi-cli/tests/snapshots.rs (all lowercase `kitchen_sink_...`) and no JNI export (`Java_...`) can spell. */ +/* Helpers a given shim may not use (it only uses what its API needs). */ +#if defined(__GNUC__) +#define Jni_helper static inline __attribute__((unused)) +#else +#define Jni_helper static inline +#endif + static JavaVM* Jni_vm = NULL; static jclass Jni_bridge = NULL; static jmethodID Jni_error = NULL; -/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value - buffers all cross as one. */ -typedef struct { - jbyteArray array; - jbyte* elems; - const uint8_t* ptr; - size_t len; -} Jni_bytes; - -static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { - Jni_bytes b = {array, NULL, NULL, 0}; - if (array == NULL) { - return b; - } - b.len = (size_t)(*env)->GetArrayLength(env, array); - b.elems = (*env)->GetByteArrayElements(env, array, NULL); - b.ptr = (const uint8_t*)b.elems; - if (b.elems == NULL) { - b.len = 0; +/* The JNI primitive kinds: a direct value's carrier, a typed array's + element, and a box's payload. A string, bytes, or a value buffer crosses + as a byte array (`Jni_B`). */ +typedef enum { Jni_Z, Jni_B, Jni_S, Jni_I, Jni_J, Jni_F, Jni_D } Jni_kind; + +static const size_t Jni_kind_size[] = {1, 1, 2, 4, 8, 4, 8}; + +Jni_helper void Jni_throw_oom(JNIEnv* env, const char* what) { + jclass cls = (*env)->FindClass(env, "java/lang/OutOfMemoryError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, what); } - return b; } -static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { - if (b->elems != NULL) { - (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); +/* Copies all `n` elements of a Java primitive array into `dst`. */ +Jni_helper void Jni_get_region(JNIEnv* env, jarray array, Jni_kind kind, jsize n, void* dst) { + switch (kind) { + case Jni_Z: (*env)->GetBooleanArrayRegion(env, (jbooleanArray)array, 0, n, (jboolean*)dst); break; + case Jni_B: (*env)->GetByteArrayRegion(env, (jbyteArray)array, 0, n, (jbyte*)dst); break; + case Jni_S: (*env)->GetShortArrayRegion(env, (jshortArray)array, 0, n, (jshort*)dst); break; + case Jni_I: (*env)->GetIntArrayRegion(env, (jintArray)array, 0, n, (jint*)dst); break; + case Jni_J: (*env)->GetLongArrayRegion(env, (jlongArray)array, 0, n, (jlong*)dst); break; + case Jni_F: (*env)->GetFloatArrayRegion(env, (jfloatArray)array, 0, n, (jfloat*)dst); break; + case Jni_D: (*env)->GetDoubleArrayRegion(env, (jdoubleArray)array, 0, n, (jdouble*)dst); break; } } -/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid - input, so NULL + 0 is the empty array). */ -static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = (*env)->NewByteArray(env, (jsize)len); - if (out != NULL && len > 0 && ptr != NULL) { - (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); +/* A new Java primitive array holding a copy of the `count` elements at + `ptr` (never read when `count` is 0); NULL with an exception pending when + the JVM can't make it. */ +Jni_helper jarray Jni_new_array(JNIEnv* env, Jni_kind kind, const void* ptr, size_t count) { + if (count > (size_t)INT32_MAX) { + Jni_throw_oom(env, "native array is too large for a JVM array"); + return NULL; + } + jsize n = (jsize)count; + jarray out = NULL; + switch (kind) { + case Jni_Z: out = (*env)->NewBooleanArray(env, n); break; + case Jni_B: out = (*env)->NewByteArray(env, n); break; + case Jni_S: out = (*env)->NewShortArray(env, n); break; + case Jni_I: out = (*env)->NewIntArray(env, n); break; + case Jni_J: out = (*env)->NewLongArray(env, n); break; + case Jni_F: out = (*env)->NewFloatArray(env, n); break; + case Jni_D: out = (*env)->NewDoubleArray(env, n); break; + } + if (out == NULL || n == 0 || ptr == NULL) { + return out; + } + switch (kind) { + case Jni_Z: (*env)->SetBooleanArrayRegion(env, (jbooleanArray)out, 0, n, (const jboolean*)ptr); break; + case Jni_B: (*env)->SetByteArrayRegion(env, (jbyteArray)out, 0, n, (const jbyte*)ptr); break; + case Jni_S: (*env)->SetShortArrayRegion(env, (jshortArray)out, 0, n, (const jshort*)ptr); break; + case Jni_I: (*env)->SetIntArrayRegion(env, (jintArray)out, 0, n, (const jint*)ptr); break; + case Jni_J: (*env)->SetLongArrayRegion(env, (jlongArray)out, 0, n, (const jlong*)ptr); break; + case Jni_F: (*env)->SetFloatArrayRegion(env, (jfloatArray)out, 0, n, (const jfloat*)ptr); break; + case Jni_D: (*env)->SetDoubleArrayRegion(env, (jdoubleArray)out, 0, n, (const jdouble*)ptr); break; } return out; } -/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ -static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = Jni_new_bytes(env, ptr, len); +/* Jni_new_array over a run the producer handed over (a string, bytes, a + value buffer, or a typed array of `count` elements), which it then frees + whether or not the copy succeeded. */ +Jni_helper jarray Jni_take_array(JNIEnv* env, Jni_kind kind, const void* ptr, size_t count) { + jarray out = Jni_new_array(env, kind, ptr, count); if (ptr != NULL) { - kitchen_sink_free_bytes((uint8_t*)ptr, len); + kitchen_sink_free_bytes((uint8_t*)(uintptr_t)ptr, count * Jni_kind_size[kind]); } return out; } -/* Throws the Kotlin exception for `err` (mapped through error domain - `domain`, or 0 for a call that can't fail, which raises the trap) and - clears `err`. */ -static inline void Jni_throw(JNIEnv* env, kitchen_sink_error* err, jint domain) { - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; +/* A Java primitive array's elements copied out for one call (strings, + bytes, and value buffers as `ByteArray`s, typed arrays as `IntArray`s and + the like): one copy, into inline storage when it fits (8-aligned, as the + producer requires of typed arrays) and the heap otherwise. */ +typedef struct { + const void* ptr; /* NULL when empty */ + size_t len; /* element count */ + void* heap; + uint64_t inline_buf[32]; +} Jni_run; + +/* Fills `run` from `array` (NULL is empty); 0, with OutOfMemoryError + pending, when the heap copy can't be allocated. */ +Jni_helper int Jni_borrow(JNIEnv* env, jarray array, Jni_kind kind, Jni_run* run) { + run->ptr = NULL; + run->len = 0; + run->heap = NULL; + if (array == NULL) { + return 1; + } + jsize n = (*env)->GetArrayLength(env, array); + if (n <= 0) { + return 1; + } + size_t bytes = (size_t)n * Jni_kind_size[kind]; + void* dst = run->inline_buf; + if (bytes > sizeof run->inline_buf) { + run->heap = malloc(bytes); + if (run->heap == NULL) { + Jni_throw_oom(env, "could not copy an array argument"); + return 0; + } + dst = run->heap; + } + Jni_get_region(env, array, kind, n, dst); + run->ptr = dst; + run->len = (size_t)n; + return 1; +} + +Jni_helper void Jni_unborrow(Jni_run* run) { + free(run->heap); + run->heap = NULL; +} + +/* A global reference the producer holds on to (a callback implementation + or an async completion): NULL for NULL, and NULL with OutOfMemoryError + pending when the JVM can't make one. */ +Jni_helper void* Jni_pin(JNIEnv* env, jobject obj) { + if (obj == NULL) { + return NULL; + } + jobject ref = (*env)->NewGlobalRef(env, obj); + if (ref == NULL && !(*env)->ExceptionCheck(env)) { + Jni_throw_oom(env, "could not create a JNI global reference"); + } + return (void*)ref; +} + +Jni_helper void Jni_unpin(JNIEnv* env, void* ref) { + if (ref != NULL) { + (*env)->DeleteGlobalRef(env, (jobject)ref); + } +} + +/* Sets a failure with a C string message on `err`. */ +Jni_helper void Jni_set_error(kitchen_sink_error* err, int32_t code, const char* message) { + kitchen_sink_error_set(err, code, (const uint8_t*)message, strlen(message)); +} + +/* Throws the Kotlin exception for `err` (mapped through `JniBridge.error` + domain `domain`) and clears `err`. */ +Jni_helper void Jni_throw(JNIEnv* env, kitchen_sink_error* err, jint domain) { + jarray text = Jni_new_array(env, Jni_B, err->message_ptr, err->message_len); + jarray payload = NULL; if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + payload = Jni_new_array(env, Jni_B, err->payload_ptr, err->payload_len); } jint code = (jint)err->code; kitchen_sink_error_clear(err); @@ -94,7 +193,7 @@ static inline void Jni_throw(JNIEnv* env, kitchen_sink_error* err, jint domain) } } -static jint Jni_load_error(JNIEnv* env, const char* message) { +Jni_helper jint Jni_load_error(JNIEnv* env, const char* message) { jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); if (cls != NULL) { (*env)->ThrowNew(env, cls, message); @@ -114,7 +213,7 @@ typedef struct { has every expected entry with an equal hash; entries only the library has are fine. Fails loading with an error naming the first declaration that's missing or changed. */ -static inline jint Jni_check_contract(JNIEnv* env, const kitchen_sink_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { +Jni_helper jint Jni_check_contract(JNIEnv* env, const kitchen_sink_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { size_t len = 0; const kitchen_sink_contract_entry* table = contract(&len); if (table == NULL) { @@ -146,7 +245,8 @@ static inline jint Jni_check_contract(JNIEnv* env, const kitchen_sink_contract_e return JNI_OK; } -/* Generated below: the contract checks and the method IDs the shim caches. */ +/* Generated below: the contract checks and the classes and method IDs the + shim caches. */ static jint Jni_load(JNIEnv* env); JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { @@ -156,14 +256,17 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - if (kitchen_sink_abi_version() != 4u) { - return Jni_load_error(env, "kitchen_sink: the native library implements a different C ABI revision than these bindings (expected 4)"); + if (kitchen_sink_abi_version() != 5u) { + return Jni_load_error(env, "kitchen_sink: the native library implements a different C ABI revision than these bindings (expected 5)"); } jclass bridge = (*env)->FindClass(env, "kitchen_sink/JniBridge"); if (bridge == NULL) { return JNI_ERR; } - Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_bridge = (jclass)Jni_pin(env, bridge); + if (Jni_bridge == NULL) { + return JNI_ERR; + } Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ljava/lang/Throwable;"); if (Jni_error == NULL) { return JNI_ERR; @@ -180,6 +283,78 @@ JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_debug_1live(JNIEnv* env, jc return (jlong)kitchen_sink_debug_live((int32_t)kind); } +/* Boxes: an optional scalar crossing out of native code is a nullable + `java.lang.Integer` (and the like), and an async result or iterator item + reaches Kotlin as an `Any?`, so scalars box through `valueOf` (which + caches small values) and unbox through `intValue` (and the like). */ +static jclass Jni_box_class[7]; +static jmethodID Jni_box_of[7]; +static jmethodID Jni_unbox_of[7]; + +static jint Jni_load_boxes(JNIEnv* env) { + static const char* const classes[7] = { + "java/lang/Boolean", "java/lang/Byte", "java/lang/Short", "java/lang/Integer", + "java/lang/Long", "java/lang/Float", "java/lang/Double", + }; + static const char* const value_of[7] = { + "(Z)Ljava/lang/Boolean;", "(B)Ljava/lang/Byte;", "(S)Ljava/lang/Short;", "(I)Ljava/lang/Integer;", + "(J)Ljava/lang/Long;", "(F)Ljava/lang/Float;", "(D)Ljava/lang/Double;", + }; + static const char* const unbox[7] = { + "booleanValue", "byteValue", "shortValue", "intValue", "longValue", "floatValue", "doubleValue", + }; + static const char* const unbox_sig[7] = {"()Z", "()B", "()S", "()I", "()J", "()F", "()D"}; + for (int i = 0; i < 7; i++) { + jclass cls = (*env)->FindClass(env, classes[i]); + if (cls == NULL) { + return JNI_ERR; + } + Jni_box_class[i] = (jclass)Jni_pin(env, cls); + if (Jni_box_class[i] == NULL) { + return JNI_ERR; + } + Jni_box_of[i] = (*env)->GetStaticMethodID(env, cls, "valueOf", value_of[i]); + Jni_unbox_of[i] = (*env)->GetMethodID(env, cls, unbox[i], unbox_sig[i]); + if (Jni_box_of[i] == NULL || Jni_unbox_of[i] == NULL) { + return JNI_ERR; + } + } + return JNI_OK; +} + +/* The box of `value` (NULL with an exception pending if the JVM can't make + one). */ +Jni_helper jobject Jni_box(JNIEnv* env, Jni_kind kind, jvalue value) { + return (*env)->CallStaticObjectMethodA(env, Jni_box_class[kind], Jni_box_of[kind], &value); +} + +/* The scalar in the box `obj`. */ +Jni_helper jvalue Jni_unbox(JNIEnv* env, Jni_kind kind, jobject obj) { + jvalue v; + memset(&v, 0, sizeof v); + switch (kind) { + case Jni_Z: v.z = (*env)->CallBooleanMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_B: v.b = (*env)->CallByteMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_S: v.s = (*env)->CallShortMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_I: v.i = (*env)->CallIntMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_J: v.j = (*env)->CallLongMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_F: v.f = (*env)->CallFloatMethod(env, obj, Jni_unbox_of[kind]); break; + case Jni_D: v.d = (*env)->CallDoubleMethod(env, obj, Jni_unbox_of[kind]); break; + } + return v; +} + +/* Hands one iterator item (an object, or NULL for an absent optional) to + Kotlin through the one-element array `out`; JNI_FALSE when making the item + failed (the exception stays pending). */ +Jni_helper jboolean Jni_yield(JNIEnv* env, jobjectArray out, jobject item) { + if ((*env)->ExceptionCheck(env)) { + return JNI_FALSE; + } + (*env)->SetObjectArrayElement(env, out, 0, item); + return JNI_TRUE; +} + /* Producer threads (async completions, callback calls, vtable `free`) are usually not JVM threads. The first time one calls in it attaches as a daemon, and a thread-local destructor (a pthread key, or a fiber-local @@ -266,42 +441,42 @@ static void Jni_env_done(int detach) { } } -/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ -static jmethodID Jni_on_unit = NULL; -static jmethodID Jni_on_boolean = NULL; -static jmethodID Jni_on_byte = NULL; -static jmethodID Jni_on_short = NULL; -static jmethodID Jni_on_int = NULL; -static jmethodID Jni_on_long = NULL; -static jmethodID Jni_on_float = NULL; -static jmethodID Jni_on_double = NULL; -static jmethodID Jni_on_bytes = NULL; +/* Async completions deliver to the pinned Kotlin `NativeCompletion`: the + result as one object (boxed, an array, or NULL), a native failure, or a + JVM failure making the result. */ +static jmethodID Jni_on_value = NULL; static jmethodID Jni_on_error = NULL; +static jmethodID Jni_on_failure = NULL; static jint Jni_load_async(JNIEnv* env) { jclass cls = (*env)->FindClass(env, "kitchen_sink/NativeCompletion"); if (cls == NULL) { return JNI_ERR; } - Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); - Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); - Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); - Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); - Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); - Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); - Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); - Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); - Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); + Jni_on_value = (*env)->GetMethodID(env, cls, "onValue", "(Ljava/lang/Object;)V"); Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); - if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { + Jni_on_failure = (*env)->GetMethodID(env, cls, "onFailure", "(Ljava/lang/Throwable;)V"); + if (Jni_on_value == NULL || Jni_on_error == NULL || Jni_on_failure == NULL) { return JNI_ERR; } return JNI_OK; } +/* Reports anything the Kotlin side threw (it has no caller to propagate + to), then releases the frame, `context`, and the thread. */ +static void Jni_complete_end(JNIEnv* env, void* context, int detach) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + Jni_unpin(env, context); + Jni_env_done(detach); +} + /* Starts a completion on the producer thread: returns the JNIEnv with a local frame pushed, or NULL when the call failed (the failure, or the - loss of the JVM, has then been handled and `context` released). */ + loss of the JVM, has then been delivered and `context` released). */ static JNIEnv* Jni_complete_begin(void* context, kitchen_sink_error* err, int* detach) { JNIEnv* env = Jni_env(detach); if (env == NULL) { @@ -311,41 +486,42 @@ static JNIEnv* Jni_complete_begin(void* context, kitchen_sink_error* err, int* d if ((*env)->PushLocalFrame(env, 8) != 0) { (*env)->ExceptionClear(env); kitchen_sink_error_free(err); + Jni_unpin(env, context); Jni_env_done(*detach); return NULL; } if (err == NULL) { return env; } - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; + jarray text = Jni_new_array(env, Jni_B, err->message_ptr, err->message_len); + jarray payload = NULL; if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + payload = Jni_new_array(env, Jni_B, err->payload_ptr, err->payload_len); } jint code = (jint)err->code; kitchen_sink_error_free(err); - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); + text = NULL; + payload = NULL; } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(*detach); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); + Jni_complete_end(env, context, *detach); return NULL; } -/* Finishes a successful completion: reports anything the Kotlin side threw - (it has no caller to propagate to) and releases `context`. */ -static void Jni_complete_end(JNIEnv* env, void* context, int detach) { - if ((*env)->ExceptionCheck(env)) { - (*env)->ExceptionDescribe(env); +/* Delivers a successful result (`value`, made after Jni_complete_begin) + and finishes the completion. A JVM failure making `value` is delivered + instead, so the caller always resumes. */ +static void Jni_complete(JNIEnv* env, void* context, int detach, jobject value) { + jthrowable failure = (*env)->ExceptionOccurred(env); + if (failure != NULL) { (*env)->ExceptionClear(env); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_failure, failure); + } else { + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_value, value); } - (*env)->PopLocalFrame(env, NULL); - (*env)->DeleteGlobalRef(env, (jobject)context); - Jni_env_done(detach); + Jni_complete_end(env, context, detach); } JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_cancel_1token_1create(JNIEnv* env, jclass cls) { @@ -370,19 +546,20 @@ JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_cancel_1token_1destroy(JNIEn implementation as `ctx`; trampolines call the matching static dispatch shim on JniBridge from whatever thread the producer uses. The shim never lets an exception escape: it reports a failure itself through `out_err` - (with the two exports below) and returns a zero value. */ + (with the two exports below) and returns a zero value. Every vtable's + `flags` is 0: the JVM can run a method on any thread. */ /* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after reporting the failure through `out_err`. */ static JNIEnv* Jni_callback_begin(kitchen_sink_error* out_err, int* detach) { JNIEnv* env = Jni_env(detach); if (env == NULL) { - kitchen_sink_error_set(out_err, -4, "could not attach the calling thread to the JVM"); + Jni_set_error(out_err, -4, "could not attach the calling thread to the JVM"); return NULL; } if ((*env)->PushLocalFrame(env, 16) != 0) { (*env)->ExceptionClear(env); - kitchen_sink_error_set(out_err, -4, "JNI local frame exhausted"); + Jni_set_error(out_err, -4, "JNI local frame exhausted"); Jni_env_done(*detach); return NULL; } @@ -395,7 +572,7 @@ static JNIEnv* Jni_callback_begin(kitchen_sink_error* out_err, int* detach) { static void Jni_callback_end(JNIEnv* env, kitchen_sink_error* out_err, int detach) { if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); - kitchen_sink_error_set(out_err, -4, "the JVM failed to dispatch the callback"); + Jni_set_error(out_err, -4, "the JVM failed to dispatch the callback"); } (*env)->PopLocalFrame(env, NULL); Jni_env_done(detach); @@ -407,57 +584,69 @@ static void Jni_release_callback(void* ctx) { int detach = 0; JNIEnv* env = Jni_env(&detach); if (env != NULL) { - if (ctx != NULL) { - (*env)->DeleteGlobalRef(env, (jobject)ctx); - } + Jni_unpin(env, ctx); Jni_env_done(detach); } } +/* Hands a run-shaped callback return (a string, bytes, a value buffer, or a + typed array, as a Java array of `kind`) to the producer: a copy in a + kitchen_sink_alloc run, which the producer adopts, with its element count in + `*out_len`. A NULL `value` (the shim reported a failure) is no run. */ +Jni_helper void* Jni_callback_run(JNIEnv* env, Jni_kind kind, jarray value, size_t* out_len, kitchen_sink_error* out_err) { + *out_len = 0; + if (value == NULL || (*env)->ExceptionCheck(env)) { + return NULL; + } + jsize len = (*env)->GetArrayLength(env, value); + if (len <= 0) { + return NULL; + } + uint8_t* run = kitchen_sink_alloc((size_t)len * Jni_kind_size[kind]); + if (run == NULL) { + Jni_set_error(out_err, -4, "out of memory"); + return NULL; + } + Jni_get_region(env, value, kind, len, run); + *out_len = (size_t)len; + return run; +} + +/* Reads an optional scalar callback return (a box, or NULL for none) into + `*out`; whether it was present. */ +Jni_helper bool Jni_callback_opt(JNIEnv* env, Jni_kind kind, jobject value, jvalue* out) { + if (value == NULL || (*env)->ExceptionCheck(env)) { + return false; + } + *out = Jni_unbox(env, kind, value); + return !(*env)->ExceptionCheck(env); +} + /* `JniBridge.error_set`: reports a failed callback through the trampoline's - `out_err` (the message is cut at its first NUL). */ + `out_err`, with the message as UTF-8. */ JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_error_1set(JNIEnv* env, jclass cls, jlong err, jint code, jbyteArray message) { + Jni_run m; (void)cls; - Jni_bytes m = Jni_borrow_bytes(env, message); - char* text = (char*)malloc(m.len + 1u); - if (text != NULL) { - if (m.len > 0) { - memcpy(text, m.ptr, m.len); - } - text[m.len] = '\0'; + if (!Jni_borrow(env, message, Jni_B, &m)) { + (*env)->ExceptionClear(env); + Jni_set_error((kitchen_sink_error*)(intptr_t)err, (int32_t)code, "callback failed"); + return; } - Jni_release_bytes(env, &m); - kitchen_sink_error_set((kitchen_sink_error*)(intptr_t)err, (int32_t)code, text != NULL ? text : "callback failed"); - free(text); + kitchen_sink_error_set((kitchen_sink_error*)(intptr_t)err, (int32_t)code, (const uint8_t*)m.ptr, m.len); + Jni_unborrow(&m); } /* `JniBridge.error_set_payload`: attaches a typed error's fields (a value buffer) to the failure `error_set` reported. */ JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_error_1set_1payload(JNIEnv* env, jclass cls, jlong err, jbyteArray payload) { + Jni_run p; (void)cls; - Jni_bytes p = Jni_borrow_bytes(env, payload); - kitchen_sink_error_set_payload((kitchen_sink_error*)(intptr_t)err, p.ptr, p.len); - Jni_release_bytes(env, &p); -} - -/* Hands a string, bytes, or buffer return to the producer: a run allocated - with kitchen_sink_alloc in the out slots, which the producer adopts. A NULL - `value` (the shim reported a failure) leaves the slots empty. */ -static void Jni_callback_return_bytes(JNIEnv* env, jbyteArray value, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { - if (value == NULL || (*env)->ExceptionCheck(env)) { - return; - } - jsize len = (*env)->GetArrayLength(env, value); - uint8_t* run = kitchen_sink_alloc((size_t)len); - if (len > 0 && run == NULL) { - kitchen_sink_error_set(out_err, -4, "out of memory"); + if (!Jni_borrow(env, payload, Jni_B, &p)) { + (*env)->ExceptionClear(env); return; } - if (len > 0) { - (*env)->GetByteArrayRegion(env, value, 0, len, (jbyte*)run); - } - *out_ptr = run; - *out_len = (size_t)len; + kitchen_sink_error_set_payload((kitchen_sink_error*)(intptr_t)err, (const uint8_t*)p.ptr, p.len); + Jni_unborrow(&p); } static jmethodID Jni_mid_kitchen_ReadyListener_on_ready = NULL; @@ -472,7 +661,12 @@ static void Jni_kitchen_ReadyListener_on_ready(void* ctx, int32_t p_code, const if (env == NULL) { return; } - (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_on_ready, (jobject)ctx, (jlong)(intptr_t)out_err, (jint)p_code, Jni_new_bytes(env, p_msg_ptr, p_msg_len)); + jarray a_msg = Jni_new_array(env, Jni_B, p_msg_ptr, p_msg_len); + if ((*env)->ExceptionCheck(env)) { + Jni_callback_end(env, out_err, detach); + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_on_ready, (jobject)ctx, (jlong)(intptr_t)out_err, (jint)p_code, a_msg); Jni_callback_end(env, out_err, detach); } @@ -480,10 +674,16 @@ static bool Jni_kitchen_ReadyListener_on_item(void* ctx, const uint8_t* p_item_p int detach = 0; JNIEnv* env = Jni_callback_begin(out_err, &detach); if (env == NULL) { - kitchen_sink_kitchen_Gadget_destroy(p_gadget); + if (p_gadget != NULL) { kitchen_sink_kitchen_Gadget_destroy(p_gadget); } + return (bool)0; + } + jarray a_item = Jni_new_array(env, Jni_B, p_item_ptr, p_item_len); + if ((*env)->ExceptionCheck(env)) { + if (p_gadget != NULL) { kitchen_sink_kitchen_Gadget_destroy(p_gadget); } + Jni_callback_end(env, out_err, detach); return (bool)0; } - jboolean rv = (*env)->CallStaticBooleanMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_on_item, (jobject)ctx, (jlong)(intptr_t)out_err, Jni_new_bytes(env, p_item_ptr, p_item_len), (jlong)(intptr_t)p_gadget); + jboolean rv = (*env)->CallStaticBooleanMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_on_item, (jobject)ctx, (jlong)(intptr_t)out_err, a_item, (jlong)(intptr_t)p_gadget); Jni_callback_end(env, out_err, detach); return rv == JNI_TRUE; } @@ -494,8 +694,8 @@ static void Jni_kitchen_ReadyListener_label(void* ctx, uint8_t** out_ptr, size_t if (env == NULL) { return; } - jbyteArray rv = (jbyteArray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_label, (jobject)ctx, (jlong)(intptr_t)out_err); - Jni_callback_return_bytes(env, rv, out_ptr, out_len, out_err); + jarray rv = (jarray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_label, (jobject)ctx, (jlong)(intptr_t)out_err); + *out_ptr = (uint8_t*)Jni_callback_run(env, Jni_B, rv, out_len, out_err); Jni_callback_end(env, out_err, detach); } @@ -505,8 +705,8 @@ static void Jni_kitchen_ReadyListener_latest(void* ctx, uint8_t** out_ptr, size_ if (env == NULL) { return; } - jbyteArray rv = (jbyteArray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_latest, (jobject)ctx, (jlong)(intptr_t)out_err); - Jni_callback_return_bytes(env, rv, out_ptr, out_len, out_err); + jarray rv = (jarray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_latest, (jobject)ctx, (jlong)(intptr_t)out_err); + *out_ptr = (uint8_t*)Jni_callback_run(env, Jni_B, rv, out_len, out_err); Jni_callback_end(env, out_err, detach); } @@ -523,53 +723,133 @@ static kitchen_sink_kitchen_Gadget* Jni_kitchen_ReadyListener_favorite(void* ctx static const kitchen_sink_kitchen_ReadyListener_vtable Jni_kitchen_ReadyListener_vtable = {sizeof(kitchen_sink_kitchen_ReadyListener_vtable), 0, Jni_release_callback, Jni_kitchen_ReadyListener_on_ready, Jni_kitchen_ReadyListener_on_item, Jni_kitchen_ReadyListener_label, Jni_kitchen_ReadyListener_latest, Jni_kitchen_ReadyListener_favorite}; +static jmethodID Jni_mid_kitchen_Sampler_threshold = NULL; +static jmethodID Jni_mid_kitchen_Sampler_samples = NULL; +static jmethodID Jni_mid_kitchen_Sampler_describe = NULL; + +static bool Jni_kitchen_Sampler_threshold(void* ctx, bool p_has_level, uint32_t p_level, double* out_value, kitchen_sink_error* out_err) { + int detach = 0; + JNIEnv* env = Jni_callback_begin(out_err, &detach); + if (env == NULL) { + return (bool)0; + } + jvalue value = {0}; + bool present = Jni_callback_opt(env, Jni_D, (*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_Sampler_threshold, (jobject)ctx, (jlong)(intptr_t)out_err, p_has_level ? JNI_TRUE : JNI_FALSE, (jint)p_level), &value); + if (present) { + *out_value = (double)value.d; + } + Jni_callback_end(env, out_err, detach); + return present; +} + +static void Jni_kitchen_Sampler_samples(void* ctx, bool p_has_limit, uint32_t p_limit, const double* p_weights_ptr, size_t p_weights_len, int16_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { + int detach = 0; + JNIEnv* env = Jni_callback_begin(out_err, &detach); + if (env == NULL) { + return; + } + jarray a_weights = Jni_new_array(env, Jni_D, p_weights_ptr, p_weights_len); + if ((*env)->ExceptionCheck(env)) { + Jni_callback_end(env, out_err, detach); + return; + } + jarray rv = (jarray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_Sampler_samples, (jobject)ctx, (jlong)(intptr_t)out_err, p_has_limit ? JNI_TRUE : JNI_FALSE, (jint)p_limit, a_weights); + *out_ptr = (int16_t*)Jni_callback_run(env, Jni_S, rv, out_len, out_err); + Jni_callback_end(env, out_err, detach); +} + +static void Jni_kitchen_Sampler_describe(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { + int detach = 0; + JNIEnv* env = Jni_callback_begin(out_err, &detach); + if (env == NULL) { + return; + } + jarray rv = (jarray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_mid_kitchen_Sampler_describe, (jobject)ctx, (jlong)(intptr_t)out_err); + *out_ptr = (uint8_t*)Jni_callback_run(env, Jni_B, rv, out_len, out_err); + Jni_callback_end(env, out_err, detach); +} + +static const kitchen_sink_kitchen_Sampler_vtable Jni_kitchen_Sampler_vtable = {sizeof(kitchen_sink_kitchen_Sampler_vtable), 0, Jni_release_callback, Jni_kitchen_Sampler_threshold, Jni_kitchen_Sampler_samples, Jni_kitchen_Sampler_describe}; + static const Jni_contract_entry Jni_contract_shared[] = { - {UINT64_C(0x709e234caa962446), UINT64_C(0x3894f2ca452e9f98), "shared.ping"}, - {UINT64_C(0xcfb306fa5801ee5b), UINT64_C(0x1cbd9e10983cac2c), "shared.Token"}, + {UINT64_C(0x709e234caa962446), UINT64_C(0x3894f2ca452e9f98), "shared.ping"}, /* function ping() -> string */ + {UINT64_C(0xcfb306fa5801ee5b), UINT64_C(0x18e9fc204a13df6f), "shared.Token"}, /* record Token {i64, string} */ }; static const Jni_contract_entry Jni_contract_kitchen[] = { - {UINT64_C(0x01d4f09bbddc986f), UINT64_C(0x29eb88fecc66544d), "kitchen.Gadget.new"}, - {UINT64_C(0x10472d8fe748c2ee), UINT64_C(0xb265832b6a759fc4), "kitchen.maybe_gadget"}, - {UINT64_C(0x11368eec07a74078), UINT64_C(0xfa049f78b77e146a), "kitchen.gadgets"}, - {UINT64_C(0x137f08a29cde6021), UINT64_C(0xda8de7aeb2dbb8a3), "kitchen.stream_gadgets"}, - {UINT64_C(0x2111b54980826dd7), UINT64_C(0x452783f3e6ca6e1d), "kitchen.do_cancellable"}, - {UINT64_C(0x29dddbc60bf8d7db), UINT64_C(0x452a3e573d57f6db), "kitchen.Priority"}, - {UINT64_C(0x2b227003b74a0d16), UINT64_C(0x12332957470e9782), "kitchen.maybe_subscribe"}, - {UINT64_C(0x2dd66cb0064f0494), UINT64_C(0x60197363fefd92dd), "kitchen.Item"}, - {UINT64_C(0x33c265bbf079b0e9), UINT64_C(0xc3dcd0927e6c1deb), "kitchen.u32_id"}, - {UINT64_C(0x33e6381d01660dfe), UINT64_C(0x077c63c31fd7f90a), "kitchen.KitchenErrors"}, - {UINT64_C(0x3f5fc99d73504b76), UINT64_C(0x2df88d710f1ea10e), "kitchen.Gadget.poke"}, - {UINT64_C(0x4c5764cf109528d1), UINT64_C(0x2a65529af4338a58), "kitchen.maybe_item"}, - {UINT64_C(0x4de17dfb93fd494d), UINT64_C(0xba0ad0dd52ed74af), "kitchen.i32_id"}, - {UINT64_C(0x504e95b7f7e65570), UINT64_C(0x01b3e8e0c9a10e23), "kitchen.stream_items"}, - {UINT64_C(0x68cdb9a4bd3e3dd5), UINT64_C(0xc678b9164348d497), "kitchen.bool_id"}, - {UINT64_C(0x69e7cd703d16eb85), UINT64_C(0xf3b28dd22ae9e689), "kitchen.do_async"}, - {UINT64_C(0x70865079ce41be82), UINT64_C(0xbe3b69464672af46), "kitchen.Gadget.describe"}, - {UINT64_C(0x71c5a1116cb7c49a), UINT64_C(0xcba1837108a89b84), "kitchen.echo_bytes"}, - {UINT64_C(0x735ee1e0049ee91a), UINT64_C(0xca74320bc53eddab), "kitchen.echo_string"}, - {UINT64_C(0x76d7fc58f4e41d84), UINT64_C(0x7373a0e47d30e690), "kitchen.i64_id"}, - {UINT64_C(0x79abfe1aa960fd6a), UINT64_C(0xbca7a9e5eb26f3a6), "kitchen.nested.hello"}, - {UINT64_C(0x7d3dd7d5cf6749e3), UINT64_C(0xa192f5a7298adb39), "kitchen.new_op"}, - {UINT64_C(0x89a5e664bc25c67b), UINT64_C(0x5764a95e81ecfd31), "kitchen.f64_id"}, - {UINT64_C(0x8cfdbdcecac5f7f1), UINT64_C(0x07e272f09c41fffc), "kitchen.Gadget.version"}, - {UINT64_C(0x974b9f3e823c1738), UINT64_C(0xb81b91e975695f27), "kitchen.cross_module_token"}, - {UINT64_C(0xaca0655fac15d230), UINT64_C(0x11c4d2b28abe07bb), "kitchen.list_items"}, - {UINT64_C(0xc883b4afae20543e), UINT64_C(0xc40560e5411ff392), "kitchen.legacy_op"}, - {UINT64_C(0xf2f7753e6e1dffc2), UINT64_C(0xc9047a95bdf49e93), "kitchen.get_attrs"}, - {UINT64_C(0xfae25d3391922ead), UINT64_C(0x1c7f95739f3210b8), "kitchen.subscribe"}, - {UINT64_C(0xfd0059b05cb2fcfb), UINT64_C(0x248bf8404a5823b4), "kitchen.Gadget"}, - {UINT64_C(0xfed9c551fb9fd2d6), UINT64_C(0x59aec980ef3f3291), "kitchen.ReadyListener"}, + {UINT64_C(0x01d4f09bbddc986f), UINT64_C(0xffcf2bd3b04aa002), "kitchen.Gadget.new"}, /* constructor new(i64) -> Gadget */ + {UINT64_C(0x02bbd5f31c953704), UINT64_C(0x2cfa679ea1373e09), "kitchen.Sampler.samples"}, /* callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError */ + {UINT64_C(0x10472d8fe748c2ee), UINT64_C(0xcc570a2649bbf7dc), "kitchen.maybe_gadget"}, /* function maybe_gadget(Gadget?) -> Gadget? */ + {UINT64_C(0x11368eec07a74078), UINT64_C(0xfa049f78b77e146a), "kitchen.gadgets"}, /* function gadgets() -> [Gadget] */ + {UINT64_C(0x137f08a29cde6021), UINT64_C(0xda8de7aeb2dbb8a3), "kitchen.stream_gadgets"}, /* function stream_gadgets() -> iter */ + {UINT64_C(0x184bfc4ab0e3fe45), UINT64_C(0xb64587477022696b), "kitchen.ids_u64"}, /* function ids_u64([u64]) -> [u64] */ + {UINT64_C(0x2111b54980826dd7), UINT64_C(0x627bf01ab365b209), "kitchen.do_cancellable"}, /* function do_cancellable(string) -> string async cancellable */ + {UINT64_C(0x21c70f9371374d74), UINT64_C(0x25d5d88a30fe0a79), "kitchen.ReadyListener.favorite"}, /* callback_method #4 favorite() -> Gadget */ + {UINT64_C(0x23de5bd597372f46), UINT64_C(0xcf1bc6bb37872798), "kitchen.stream_chunks"}, /* function stream_chunks() -> iter<[i32]> */ + {UINT64_C(0x249f1ddaec6a6cbe), UINT64_C(0x0710d6c9a4de93da), "kitchen.take_from_pantry"}, /* function take_from_pantry(string) -> i32 throws PantryError */ + {UINT64_C(0x29dddbc60bf8d7db), UINT64_C(0x452a3e573d57f6db), "kitchen.Priority"}, /* enum Priority {Low = 0, Normal = 1, High = 2} */ + {UINT64_C(0x2b227003b74a0d16), UINT64_C(0x5dae036b8a6aed30), "kitchen.maybe_subscribe"}, /* function maybe_subscribe(ReadyListener?) -> bool */ + {UINT64_C(0x2dd66cb0064f0494), UINT64_C(0x74b131724dd0d867), "kitchen.Item"}, /* record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} */ + {UINT64_C(0x33c265bbf079b0e9), UINT64_C(0x97c32b764693f2cb), "kitchen.u32_id"}, /* function u32_id(u32) -> u32 */ + {UINT64_C(0x33e6381d01660dfe), UINT64_C(0x05e35a563089bf7d), "kitchen.KitchenErrors"}, /* errors KitchenErrors */ + {UINT64_C(0x3d969ae76db6cb23), UINT64_C(0x65a325cfc772296b), "kitchen.ReadyListener.latest"}, /* callback_method #3 latest() -> Item? */ + {UINT64_C(0x3f5fc99d73504b76), UINT64_C(0x417e4a2acd3210b3), "kitchen.Gadget.poke"}, /* method poke(i32) -> i32 throws KitchenErrors */ + {UINT64_C(0x4082f0085db28781), UINT64_C(0x18848e701962912e), "kitchen.Sampler"}, /* callback Sampler */ + {UINT64_C(0x4340a6b1e7ec8c50), UINT64_C(0xa59d0f32a66dc7a2), "kitchen.Sampler.describe"}, /* callback_method #2 describe() -> string throws any */ + {UINT64_C(0x4c5764cf109528d1), UINT64_C(0xb374accf14676d90), "kitchen.maybe_item"}, /* function maybe_item(i64) -> Item? throws KitchenErrors */ + {UINT64_C(0x4de17dfb93fd494d), UINT64_C(0x78be3203bcd10727), "kitchen.i32_id"}, /* function i32_id(i32) -> i32 */ + {UINT64_C(0x504e95b7f7e65570), UINT64_C(0x01b3e8e0c9a10e23), "kitchen.stream_items"}, /* function stream_items() -> iter */ + {UINT64_C(0x5dafabb7775fcb2d), UINT64_C(0x6c4b78c7d3f858c1), "kitchen.PantryError.Spoiled"}, /* code Spoiled = 2 */ + {UINT64_C(0x5e0a8f6be4b00600), UINT64_C(0x18ea1a42036c96a0), "kitchen.Sampler.threshold"}, /* callback_method #0 threshold(u32?) -> f64? */ + {UINT64_C(0x5ec0af1d4ecc6fed), UINT64_C(0x6540bdb74246420c), "kitchen.async_samples"}, /* function async_samples(u32) -> [f32] async cancellable */ + {UINT64_C(0x61a358b0571e0a4e), UINT64_C(0x11a24c4b50d11996), "kitchen.async_maybe_count"}, /* function async_maybe_count() -> i64? async */ + {UINT64_C(0x68cdb9a4bd3e3dd5), UINT64_C(0xcde022fe6bc3b1bf), "kitchen.bool_id"}, /* function bool_id(bool) -> bool */ + {UINT64_C(0x69e7cd703d16eb85), UINT64_C(0xb13cb8d39e702c79), "kitchen.do_async"}, /* function do_async(string) -> string async */ + {UINT64_C(0x70865079ce41be82), UINT64_C(0xbe3b69464672af46), "kitchen.Gadget.describe"}, /* method describe() -> string */ + {UINT64_C(0x71c5a1116cb7c49a), UINT64_C(0x1506cfe6f548fac4), "kitchen.echo_bytes"}, /* function echo_bytes(bytes) -> bytes */ + {UINT64_C(0x735ee1e0049ee91a), UINT64_C(0xa713dbd1be707752), "kitchen.echo_string"}, /* function echo_string(string) -> string */ + {UINT64_C(0x76d7fc58f4e41d84), UINT64_C(0x6bdb4ccea2841d5e), "kitchen.i64_id"}, /* function i64_id(i64) -> i64 */ + {UINT64_C(0x78d91fd617e162b8), UINT64_C(0xd88e2753503b36df), "kitchen.stream_maybe"}, /* function stream_maybe() -> iter */ + {UINT64_C(0x79abfe1aa960fd6a), UINT64_C(0xbca7a9e5eb26f3a6), "kitchen.nested.hello"}, /* function hello() -> string */ + {UINT64_C(0x7b3865b536da998c), UINT64_C(0x1c4e79d60a20e74f), "kitchen.mixed_slices"}, /* function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] */ + {UINT64_C(0x7ba266dc26a58989), UINT64_C(0x0f86001cac0c21e8), "kitchen.KitchenErrors.InvalidInput"}, /* code InvalidInput = 2 */ + {UINT64_C(0x7d3dd7d5cf6749e3), UINT64_C(0xa192f5a7298adb39), "kitchen.new_op"}, /* function new_op() -> i32 */ + {UINT64_C(0x81ded69adee0abf2), UINT64_C(0x8c71141a931906f6), "kitchen.scale_i32"}, /* function scale_i32([i32], i32) -> [i32] */ + {UINT64_C(0x81fa72769330fed6), UINT64_C(0x89681e305b55a4f4), "kitchen.ReadyListener.label"}, /* callback_method #2 label() -> string throws KitchenErrors */ + {UINT64_C(0x89a5e664bc25c67b), UINT64_C(0x357fd9b65ad879a9), "kitchen.f64_id"}, /* function f64_id(f64) -> f64 */ + {UINT64_C(0x8cfdbdcecac5f7f1), UINT64_C(0x07e272f09c41fffc), "kitchen.Gadget.version"}, /* static version() -> string */ + {UINT64_C(0x8dc42397e47614b9), UINT64_C(0xcddf6fede09b2e7f), "kitchen.opt_priority"}, /* function opt_priority(Priority?) -> Priority? */ + {UINT64_C(0x92f060ef68961fc1), UINT64_C(0x4af6ce5b4f9fa508), "kitchen.ReadyListener.on_item"}, /* callback_method #1 on_item(Item, Gadget) -> bool */ + {UINT64_C(0x974b9f3e823c1738), UINT64_C(0xb81b91e975695f27), "kitchen.cross_module_token"}, /* function cross_module_token() -> Token */ + {UINT64_C(0x9f0f001aa85f97d5), UINT64_C(0x55815247f895bd45), "kitchen.sum_f64"}, /* function sum_f64([f64]) -> f64 */ + {UINT64_C(0xab2a29fd1ef5458b), UINT64_C(0x471a34aa4c09ae2b), "kitchen.KitchenErrors.NotFound"}, /* code NotFound = 1 */ + {UINT64_C(0xaca0655fac15d230), UINT64_C(0x11c4d2b28abe07bb), "kitchen.list_items"}, /* function list_items() -> [Item] */ + {UINT64_C(0xb57c8e6ae3fb4383), UINT64_C(0x3f857851ab75366c), "kitchen.PantryError"}, /* errors PantryError */ + {UINT64_C(0xbdc795b195edd428), UINT64_C(0x36127462f8b0969f), "kitchen.opt_mixed"}, /* function opt_mixed(f64, f32?, i64?, string?) -> u16? */ + {UINT64_C(0xbe4821ab3c340e71), UINT64_C(0xe1d32e38b766e623), "kitchen.ReadyListener.on_ready"}, /* callback_method #0 on_ready(i32, string) -> void */ + {UINT64_C(0xc2ad53f2a6a36323), UINT64_C(0x071e29abee823f17), "kitchen.opt_i32_id"}, /* function opt_i32_id(i32?) -> i32? */ + {UINT64_C(0xc7ab409554fd3d7b), UINT64_C(0xfea12b6bc219fc46), "kitchen.parse_any"}, /* function parse_any(string) -> i64 throws any */ + {UINT64_C(0xc82a7f2d85f99f4e), UINT64_C(0xa9bcb8bacedb036c), "kitchen.sample_with"}, /* function sample_with(Sampler, u32?) -> [i16] throws PantryError */ + {UINT64_C(0xc883b4afae20543e), UINT64_C(0xc40560e5411ff392), "kitchen.legacy_op"}, /* function legacy_op() -> i32 */ + {UINT64_C(0xd180bf646bff59e2), UINT64_C(0xa4515a6d00d7c600), "kitchen.PantryError.OutOfStock"}, /* code OutOfStock = 1 {string} */ + {UINT64_C(0xe6778df0f5c1b82b), UINT64_C(0x38eaafdb350559e1), "kitchen.opt_bool_id"}, /* function opt_bool_id(bool?) -> bool? */ + {UINT64_C(0xf2f7753e6e1dffc2), UINT64_C(0xc9047a95bdf49e93), "kitchen.get_attrs"}, /* function get_attrs() -> {string:i32} */ + {UINT64_C(0xfae25d3391922ead), UINT64_C(0xcc034cf506cae182), "kitchen.subscribe"}, /* function subscribe(ReadyListener) -> i32 */ + {UINT64_C(0xfd0059b05cb2fcfb), UINT64_C(0x248bf8404a5823b4), "kitchen.Gadget"}, /* interface Gadget */ + {UINT64_C(0xfed9c551fb9fd2d6), UINT64_C(0xe1020573f084dc1d), "kitchen.ReadyListener"}, /* callback ReadyListener */ + {UINT64_C(0xff3a99b52597dee2), UINT64_C(0x14c4ecac97bdfc9b), "kitchen.Gadget.readings"}, /* method readings(u32?) -> [f64] throws any */ }; static jint Jni_load(JNIEnv* env) { - (void)env; if (Jni_check_contract(env, kitchen_sink_shared_contract, Jni_contract_shared, sizeof Jni_contract_shared / sizeof Jni_contract_shared[0]) != JNI_OK) { return JNI_ERR; } if (Jni_check_contract(env, kitchen_sink_kitchen_contract, Jni_contract_kitchen, sizeof Jni_contract_kitchen / sizeof Jni_contract_kitchen[0]) != JNI_OK) { return JNI_ERR; } + if (Jni_load_boxes(env) != JNI_OK) { + return JNI_ERR; + } if (Jni_load_async(env) != JNI_OK) { return JNI_ERR; } @@ -593,19 +873,31 @@ static jint Jni_load(JNIEnv* env) { if (Jni_mid_kitchen_ReadyListener_favorite == NULL) { return JNI_ERR; } + Jni_mid_kitchen_Sampler_threshold = (*env)->GetStaticMethodID(env, Jni_bridge, "kitchen_Sampler_threshold", "(Lkitchen_sink/Sampler;JZI)Ljava/lang/Double;"); + if (Jni_mid_kitchen_Sampler_threshold == NULL) { + return JNI_ERR; + } + Jni_mid_kitchen_Sampler_samples = (*env)->GetStaticMethodID(env, Jni_bridge, "kitchen_Sampler_samples", "(Lkitchen_sink/Sampler;JZI[D)[S"); + if (Jni_mid_kitchen_Sampler_samples == NULL) { + return JNI_ERR; + } + Jni_mid_kitchen_Sampler_describe = (*env)->GetStaticMethodID(env, Jni_bridge, "kitchen_Sampler_describe", "(Lkitchen_sink/Sampler;J)[B"); + if (Jni_mid_kitchen_Sampler_describe == NULL) { + return JNI_ERR; + } return JNI_OK; } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_shared_1ping(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_shared_ping(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1clone(JNIEnv* env, jclass cls, jlong self) { @@ -621,7 +913,7 @@ JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1destroy(JNI } JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1bool_1id(JNIEnv* env, jclass cls, jboolean p_x) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; bool rv = kitchen_sink_kitchen_bool_id(p_x == JNI_TRUE, &err); if (err.code != 0) { @@ -632,7 +924,7 @@ JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1bool_1id(JNIEnv } JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1i32_1id(JNIEnv* env, jclass cls, jint p_x) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; int32_t rv = kitchen_sink_kitchen_i32_id((int32_t)p_x, &err); if (err.code != 0) { @@ -643,7 +935,7 @@ JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1i32_1id(JNIEnv* env } JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1u32_1id(JNIEnv* env, jclass cls, jint p_x) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; uint32_t rv = kitchen_sink_kitchen_u32_id((uint32_t)p_x, &err); if (err.code != 0) { @@ -654,7 +946,7 @@ JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1u32_1id(JNIEnv* env } JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1i64_1id(JNIEnv* env, jclass cls, jlong p_x) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; int64_t rv = kitchen_sink_kitchen_i64_id((int64_t)p_x, &err); if (err.code != 0) { @@ -665,7 +957,7 @@ JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1i64_1id(JNIEnv* en } JNIEXPORT jdouble JNICALL Java_kitchen_1sink_JniBridge_kitchen_1f64_1id(JNIEnv* env, jclass cls, jdouble p_x) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; double rv = kitchen_sink_kitchen_f64_id((double)p_x, &err); if (err.code != 0) { @@ -676,37 +968,47 @@ JNIEXPORT jdouble JNICALL Java_kitchen_1sink_JniBridge_kitchen_1f64_1id(JNIEnv* } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1echo_1string(JNIEnv* env, jclass cls, jbyteArray p_s) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_s_b = Jni_borrow_bytes(env, p_s); + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; - const uint8_t* rv = kitchen_sink_kitchen_echo_string(p_s_b.ptr, p_s_b.len, &out_len, &err); - Jni_release_bytes(env, &p_s_b); + (void)cls; + Jni_run r_s; + if (!Jni_borrow(env, p_s, Jni_B, &r_s)) { + return NULL; + } + const uint8_t* rv = kitchen_sink_kitchen_echo_string((const uint8_t*)r_s.ptr, r_s.len, &out_len, &err); + Jni_unborrow(&r_s); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1echo_1bytes(JNIEnv* env, jclass cls, jbyteArray p_b) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_b_b = Jni_borrow_bytes(env, p_b); + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; - const uint8_t* rv = kitchen_sink_kitchen_echo_bytes(p_b_b.ptr, p_b_b.len, &out_len, &err); - Jni_release_bytes(env, &p_b_b); + (void)cls; + Jni_run r_b; + if (!Jni_borrow(env, p_b, Jni_B, &r_b)) { + return NULL; + } + const uint8_t* rv = kitchen_sink_kitchen_echo_bytes((const uint8_t*)r_b.ptr, r_b.len, &out_len, &err); + Jni_unborrow(&r_b); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1subscribe(JNIEnv* env, jclass cls, jobject p_listener) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; - int32_t rv = kitchen_sink_kitchen_subscribe((void*)(*env)->NewGlobalRef(env, p_listener), &Jni_kitchen_ReadyListener_vtable, &err); + void* c_listener = Jni_pin(env, p_listener); + if (p_listener != NULL && c_listener == NULL) { + return 0; + } + int32_t rv = kitchen_sink_kitchen_subscribe(c_listener, &Jni_kitchen_ReadyListener_vtable, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return 0; @@ -715,9 +1017,13 @@ JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1subscribe(JNIEnv* e } JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1subscribe(JNIEnv* env, jclass cls, jobject p_listener) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; - bool rv = kitchen_sink_kitchen_maybe_subscribe(p_listener != NULL ? (void*)(*env)->NewGlobalRef(env, p_listener) : NULL, p_listener != NULL ? &Jni_kitchen_ReadyListener_vtable : NULL, &err); + void* c_listener = Jni_pin(env, p_listener); + if (p_listener != NULL && c_listener == NULL) { + return 0; + } + bool rv = kitchen_sink_kitchen_maybe_subscribe(c_listener, p_listener != NULL ? &Jni_kitchen_ReadyListener_vtable : NULL, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return 0; @@ -726,7 +1032,7 @@ JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1subscrib } JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1gadget(JNIEnv* env, jclass cls, jlong p_fallback) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; kitchen_sink_kitchen_Gadget* rv = kitchen_sink_kitchen_maybe_gadget((const kitchen_sink_kitchen_Gadget*)(intptr_t)p_fallback, &err); if (err.code != 0) { @@ -737,19 +1043,19 @@ JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1gadget(JNIE } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1gadgets(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_gadgets(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1gadgets(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; kitchen_sink_kitchen_StreamGadgetsIterator* rv = kitchen_sink_kitchen_stream_gadgets(&err); if (err.code != 0) { @@ -759,11 +1065,11 @@ JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1gadgets(JN return (jlong)(intptr_t)rv; } -JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIterator_1next(JNIEnv* env, jclass cls, jlong iter, jlongArray out) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - kitchen_sink_kitchen_Gadget* item = (kitchen_sink_kitchen_Gadget*)0; +JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIterator_1next(JNIEnv* env, jclass cls, jlong iter, jobjectArray out) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + kitchen_sink_kitchen_Gadget* out_item = NULL; (void)cls; - int32_t has = kitchen_sink_kitchen_StreamGadgetsIterator_next((kitchen_sink_kitchen_StreamGadgetsIterator*)(intptr_t)iter, &item, &err); + int32_t has = kitchen_sink_kitchen_StreamGadgetsIterator_next((kitchen_sink_kitchen_StreamGadgetsIterator*)(intptr_t)iter, &out_item, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return JNI_FALSE; @@ -771,9 +1077,7 @@ JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIt if (has == 0) { return JNI_FALSE; } - jlong value = (jlong)(intptr_t)item; - (*env)->SetLongArrayRegion(env, out, 0, 1, &value); - return JNI_TRUE; + return Jni_yield(env, out, Jni_box(env, Jni_J, (jvalue){.j = (jlong)(intptr_t)out_item})); } JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { @@ -783,43 +1087,43 @@ JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIterat } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1item(JNIEnv* env, jclass cls, jlong p_id) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_maybe_item((int64_t)p_id, &out_len, &err); if (err.code != 0) { - Jni_throw(env, &err, 1); + Jni_throw(env, &err, 2); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1list_1items(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_list_items(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1get_1attrs(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_get_attrs(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1items(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; kitchen_sink_kitchen_StreamItemsIterator* rv = kitchen_sink_kitchen_stream_items(&err); if (err.code != 0) { @@ -829,20 +1133,20 @@ JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1items(JNIE return (jlong)(intptr_t)rv; } -JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamItemsIterator_1next(JNIEnv* env, jclass cls, jlong iter) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - const uint8_t* item = NULL; +JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamItemsIterator_1next(JNIEnv* env, jclass cls, jlong iter, jobjectArray out) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + const uint8_t* out_item = NULL; size_t out_len = 0; (void)cls; - int32_t has = kitchen_sink_kitchen_StreamItemsIterator_next((kitchen_sink_kitchen_StreamItemsIterator*)(intptr_t)iter, &item, &out_len, &err); + int32_t has = kitchen_sink_kitchen_StreamItemsIterator_next((kitchen_sink_kitchen_StreamItemsIterator*)(intptr_t)iter, &out_item, &out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); - return NULL; + return JNI_FALSE; } if (has == 0) { - return NULL; + return JNI_FALSE; } - return Jni_take_bytes(env, item, out_len); + return Jni_yield(env, out, Jni_take_array(env, Jni_B, out_item, out_len)); } JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamItemsIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { @@ -852,15 +1156,15 @@ JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamItemsIterator } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1cross_1module_1token(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_cross_module_token(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } static void Jni_done_kitchen_do_async(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { @@ -869,16 +1173,22 @@ static void Jni_done_kitchen_do_async(void* context, kitchen_sink_error* err, co if (env == NULL) { return; } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); + Jni_complete(env, context, detach, Jni_take_array(env, Jni_B, result_ptr, result_len)); } JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1do_1async(JNIEnv* env, jclass cls, jbyteArray p_input, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); (void)cls; - Jni_bytes p_input_b = Jni_borrow_bytes(env, p_input); - kitchen_sink_kitchen_do_async(p_input_b.ptr, p_input_b.len, Jni_done_kitchen_do_async, context); - Jni_release_bytes(env, &p_input_b); + Jni_run r_input; + if (!Jni_borrow(env, p_input, Jni_B, &r_input)) { + return; + } + void* context = Jni_pin(env, completion); + if (context == NULL) { + Jni_unborrow(&r_input); + return; + } + kitchen_sink_kitchen_do_async((const uint8_t*)r_input.ptr, r_input.len, Jni_done_kitchen_do_async, context); + Jni_unborrow(&r_input); } static void Jni_done_kitchen_do_cancellable(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { @@ -887,20 +1197,26 @@ static void Jni_done_kitchen_do_cancellable(void* context, kitchen_sink_error* e if (env == NULL) { return; } - (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); - Jni_complete_end(env, context, detach); + Jni_complete(env, context, detach, Jni_take_array(env, Jni_B, result_ptr, result_len)); } JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1do_1cancellable(JNIEnv* env, jclass cls, jbyteArray p_input, jlong cancel_token, jobject completion) { - void* context = (void*)(*env)->NewGlobalRef(env, completion); (void)cls; - Jni_bytes p_input_b = Jni_borrow_bytes(env, p_input); - kitchen_sink_kitchen_do_cancellable(p_input_b.ptr, p_input_b.len, (kitchen_sink_cancel_token*)(intptr_t)cancel_token, Jni_done_kitchen_do_cancellable, context); - Jni_release_bytes(env, &p_input_b); + Jni_run r_input; + if (!Jni_borrow(env, p_input, Jni_B, &r_input)) { + return; + } + void* context = Jni_pin(env, completion); + if (context == NULL) { + Jni_unborrow(&r_input); + return; + } + kitchen_sink_kitchen_do_cancellable((const uint8_t*)r_input.ptr, r_input.len, (kitchen_sink_cancel_token*)(intptr_t)cancel_token, Jni_done_kitchen_do_cancellable, context); + Jni_unborrow(&r_input); } JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1legacy_1op(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; int32_t rv = kitchen_sink_kitchen_legacy_op(&err); if (err.code != 0) { @@ -911,7 +1227,7 @@ JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1legacy_1op(JNIEnv* } JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1new_1op(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; int32_t rv = kitchen_sink_kitchen_new_op(&err); if (err.code != 0) { @@ -921,8 +1237,318 @@ JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1new_1op(JNIEnv* env return (jint)rv; } +JNIEXPORT jobject JNICALL Java_kitchen_1sink_JniBridge_kitchen_1opt_1i32_1id(JNIEnv* env, jclass cls, jboolean h_x, jint p_x) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + int32_t out_value = 0; + (void)cls; + bool rv = kitchen_sink_kitchen_opt_i32_id(h_x == JNI_TRUE, (int32_t)p_x, &out_value, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return rv ? Jni_box(env, Jni_I, (jvalue){.i = (jint)out_value}) : NULL; +} + +JNIEXPORT jobject JNICALL Java_kitchen_1sink_JniBridge_kitchen_1opt_1bool_1id(JNIEnv* env, jclass cls, jboolean h_x, jboolean p_x) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + bool out_value = 0; + (void)cls; + bool rv = kitchen_sink_kitchen_opt_bool_id(h_x == JNI_TRUE, p_x == JNI_TRUE, &out_value, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return rv ? Jni_box(env, Jni_Z, (jvalue){.z = out_value ? JNI_TRUE : JNI_FALSE}) : NULL; +} + +JNIEXPORT jobject JNICALL Java_kitchen_1sink_JniBridge_kitchen_1opt_1priority(JNIEnv* env, jclass cls, jboolean h_p, jint p_p) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + kitchen_sink_kitchen_Priority out_value = 0; + (void)cls; + bool rv = kitchen_sink_kitchen_opt_priority(h_p == JNI_TRUE, (kitchen_sink_kitchen_Priority)p_p, &out_value, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return rv ? Jni_box(env, Jni_I, (jvalue){.i = (jint)out_value}) : NULL; +} + +JNIEXPORT jobject JNICALL Java_kitchen_1sink_JniBridge_kitchen_1opt_1mixed(JNIEnv* env, jclass cls, jdouble p_base, jboolean h_scale, jfloat p_scale, jboolean h_offset, jlong p_offset, jbyteArray p_label) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + uint16_t out_value = 0; + (void)cls; + Jni_run r_label; + if (!Jni_borrow(env, p_label, Jni_B, &r_label)) { + return NULL; + } + bool rv = kitchen_sink_kitchen_opt_mixed((double)p_base, h_scale == JNI_TRUE, (float)p_scale, h_offset == JNI_TRUE, (int64_t)p_offset, (const uint8_t*)r_label.ptr, r_label.len, &out_value, &err); + Jni_unborrow(&r_label); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return rv ? Jni_box(env, Jni_S, (jvalue){.s = (jshort)out_value}) : NULL; +} + +JNIEXPORT jdouble JNICALL Java_kitchen_1sink_JniBridge_kitchen_1sum_1f64(JNIEnv* env, jclass cls, jdoubleArray p_xs) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + (void)cls; + Jni_run r_xs; + if (!Jni_borrow(env, p_xs, Jni_D, &r_xs)) { + return 0; + } + double rv = kitchen_sink_kitchen_sum_f64((const double*)r_xs.ptr, r_xs.len, &err); + Jni_unborrow(&r_xs); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jdouble)rv; +} + +JNIEXPORT jintArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1scale_1i32(JNIEnv* env, jclass cls, jintArray p_xs, jint p_factor) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + size_t out_len = 0; + (void)cls; + Jni_run r_xs; + if (!Jni_borrow(env, p_xs, Jni_I, &r_xs)) { + return NULL; + } + int32_t* rv = kitchen_sink_kitchen_scale_i32((const int32_t*)r_xs.ptr, r_xs.len, (int32_t)p_factor, &out_len, &err); + Jni_unborrow(&r_xs); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return (jintArray)Jni_take_array(env, Jni_I, rv, out_len); +} + +JNIEXPORT jlongArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1ids_1u64(JNIEnv* env, jclass cls, jlongArray p_ids) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + size_t out_len = 0; + (void)cls; + Jni_run r_ids; + if (!Jni_borrow(env, p_ids, Jni_J, &r_ids)) { + return NULL; + } + uint64_t* rv = kitchen_sink_kitchen_ids_u64((const uint64_t*)r_ids.ptr, r_ids.len, &out_len, &err); + Jni_unborrow(&r_ids); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return (jlongArray)Jni_take_array(env, Jni_J, rv, out_len); +} + +JNIEXPORT jintArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1mixed_1slices(JNIEnv* env, jclass cls, jbyteArray p_a, jshortArray p_b, jshortArray p_c, jintArray p_d, jlongArray p_e, jfloatArray p_f) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + size_t out_len = 0; + (void)cls; + Jni_run r_a; + if (!Jni_borrow(env, p_a, Jni_B, &r_a)) { + return NULL; + } + Jni_run r_b; + if (!Jni_borrow(env, p_b, Jni_S, &r_b)) { + Jni_unborrow(&r_a); + return NULL; + } + Jni_run r_c; + if (!Jni_borrow(env, p_c, Jni_S, &r_c)) { + Jni_unborrow(&r_b); + Jni_unborrow(&r_a); + return NULL; + } + Jni_run r_d; + if (!Jni_borrow(env, p_d, Jni_I, &r_d)) { + Jni_unborrow(&r_c); + Jni_unborrow(&r_b); + Jni_unborrow(&r_a); + return NULL; + } + Jni_run r_e; + if (!Jni_borrow(env, p_e, Jni_J, &r_e)) { + Jni_unborrow(&r_d); + Jni_unborrow(&r_c); + Jni_unborrow(&r_b); + Jni_unborrow(&r_a); + return NULL; + } + Jni_run r_f; + if (!Jni_borrow(env, p_f, Jni_F, &r_f)) { + Jni_unborrow(&r_e); + Jni_unborrow(&r_d); + Jni_unborrow(&r_c); + Jni_unborrow(&r_b); + Jni_unborrow(&r_a); + return NULL; + } + uint32_t* rv = kitchen_sink_kitchen_mixed_slices((const int8_t*)r_a.ptr, r_a.len, (const int16_t*)r_b.ptr, r_b.len, (const uint16_t*)r_c.ptr, r_c.len, (const uint32_t*)r_d.ptr, r_d.len, (const int64_t*)r_e.ptr, r_e.len, (const float*)r_f.ptr, r_f.len, &out_len, &err); + Jni_unborrow(&r_a); + Jni_unborrow(&r_b); + Jni_unborrow(&r_c); + Jni_unborrow(&r_d); + Jni_unborrow(&r_e); + Jni_unborrow(&r_f); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return (jintArray)Jni_take_array(env, Jni_I, rv, out_len); +} + +static void Jni_done_kitchen_async_maybe_count(void* context, kitchen_sink_error* err, bool has_result, int64_t result) { + int detach = 0; + JNIEnv* env = Jni_complete_begin(context, err, &detach); + if (env == NULL) { + return; + } + Jni_complete(env, context, detach, has_result ? Jni_box(env, Jni_J, (jvalue){.j = (jlong)result}) : NULL); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1async_1maybe_1count(JNIEnv* env, jclass cls, jobject completion) { + (void)cls; + void* context = Jni_pin(env, completion); + if (context == NULL) { + return; + } + kitchen_sink_kitchen_async_maybe_count(Jni_done_kitchen_async_maybe_count, context); +} + +static void Jni_done_kitchen_async_samples(void* context, kitchen_sink_error* err, const float* result_ptr, size_t result_len) { + int detach = 0; + JNIEnv* env = Jni_complete_begin(context, err, &detach); + if (env == NULL) { + return; + } + Jni_complete(env, context, detach, Jni_take_array(env, Jni_F, result_ptr, result_len)); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1async_1samples(JNIEnv* env, jclass cls, jint p_n, jlong cancel_token, jobject completion) { + (void)cls; + void* context = Jni_pin(env, completion); + if (context == NULL) { + return; + } + kitchen_sink_kitchen_async_samples((uint32_t)p_n, (kitchen_sink_cancel_token*)(intptr_t)cancel_token, Jni_done_kitchen_async_samples, context); +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1maybe(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + (void)cls; + kitchen_sink_kitchen_StreamMaybeIterator* rv = kitchen_sink_kitchen_stream_maybe(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamMaybeIterator_1next(JNIEnv* env, jclass cls, jlong iter, jobjectArray out) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + bool out_has_item = false; + int32_t out_item = 0; + (void)cls; + int32_t has = kitchen_sink_kitchen_StreamMaybeIterator_next((kitchen_sink_kitchen_StreamMaybeIterator*)(intptr_t)iter, &out_has_item, &out_item, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return JNI_FALSE; + } + if (has == 0) { + return JNI_FALSE; + } + return Jni_yield(env, out, out_has_item ? Jni_box(env, Jni_I, (jvalue){.i = (jint)out_item}) : NULL); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamMaybeIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + kitchen_sink_kitchen_StreamMaybeIterator_destroy((kitchen_sink_kitchen_StreamMaybeIterator*)(intptr_t)iter); +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1chunks(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + (void)cls; + kitchen_sink_kitchen_StreamChunksIterator* rv = kitchen_sink_kitchen_stream_chunks(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamChunksIterator_1next(JNIEnv* env, jclass cls, jlong iter, jobjectArray out) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + int32_t* out_item = NULL; + size_t out_len = 0; + (void)cls; + int32_t has = kitchen_sink_kitchen_StreamChunksIterator_next((kitchen_sink_kitchen_StreamChunksIterator*)(intptr_t)iter, &out_item, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return JNI_FALSE; + } + if (has == 0) { + return JNI_FALSE; + } + return Jni_yield(env, out, Jni_take_array(env, Jni_I, out_item, out_len)); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamChunksIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + kitchen_sink_kitchen_StreamChunksIterator_destroy((kitchen_sink_kitchen_StreamChunksIterator*)(intptr_t)iter); +} + +JNIEXPORT jshortArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1sample_1with(JNIEnv* env, jclass cls, jobject p_sampler, jboolean h_level, jint p_level) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + size_t out_len = 0; + (void)cls; + void* c_sampler = Jni_pin(env, p_sampler); + if (p_sampler != NULL && c_sampler == NULL) { + return NULL; + } + int16_t* rv = kitchen_sink_kitchen_sample_with(c_sampler, &Jni_kitchen_Sampler_vtable, h_level == JNI_TRUE, (uint32_t)p_level, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 3); + return NULL; + } + return (jshortArray)Jni_take_array(env, Jni_S, rv, out_len); +} + +JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1take_1from_1pantry(JNIEnv* env, jclass cls, jbyteArray p_item) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + (void)cls; + Jni_run r_item; + if (!Jni_borrow(env, p_item, Jni_B, &r_item)) { + return 0; + } + int32_t rv = kitchen_sink_kitchen_take_from_pantry((const uint8_t*)r_item.ptr, r_item.len, &err); + Jni_unborrow(&r_item); + if (err.code != 0) { + Jni_throw(env, &err, 3); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1parse_1any(JNIEnv* env, jclass cls, jbyteArray p_text) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + (void)cls; + Jni_run r_text; + if (!Jni_borrow(env, p_text, Jni_B, &r_text)) { + return 0; + } + int64_t rv = kitchen_sink_kitchen_parse_any((const uint8_t*)r_text.ptr, r_text.len, &err); + Jni_unborrow(&r_text); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return 0; + } + return (jlong)rv; +} + JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1new(JNIEnv* env, jclass cls, jlong p_id) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; kitchen_sink_kitchen_Gadget* rv = kitchen_sink_kitchen_Gadget_new((int64_t)p_id, &err); if (err.code != 0) { @@ -933,50 +1559,62 @@ JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1new(JNIEnv } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1describe(JNIEnv* env, jclass cls, jlong self) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_Gadget_describe((const kitchen_sink_kitchen_Gadget*)(intptr_t)self, &out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1poke(JNIEnv* env, jclass cls, jlong self, jint p_times) { - kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; (void)cls; int32_t rv = kitchen_sink_kitchen_Gadget_poke((const kitchen_sink_kitchen_Gadget*)(intptr_t)self, (int32_t)p_times, &err); if (err.code != 0) { - Jni_throw(env, &err, 1); + Jni_throw(env, &err, 2); return 0; } return (jint)rv; } -JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1version(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; +JNIEXPORT jdoubleArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1readings(JNIEnv* env, jclass cls, jlong self, jboolean h_limit, jint p_limit) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; + size_t out_len = 0; (void)cls; + double* rv = kitchen_sink_kitchen_Gadget_readings((const kitchen_sink_kitchen_Gadget*)(intptr_t)self, h_limit == JNI_TRUE, (uint32_t)p_limit, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return NULL; + } + return (jdoubleArray)Jni_take_array(env, Jni_D, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1version(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_Gadget_version(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1nested_1hello(JNIEnv* env, jclass cls) { - kitchen_sink_error err = {0, NULL, NULL, 0}; - (void)cls; + kitchen_sink_error err = {0, NULL, 0, NULL, 0}; size_t out_len = 0; + (void)cls; const uint8_t* rv = kitchen_sink_kitchen_nested_hello(&out_len, &err); if (err.code != 0) { Jni_throw(env, &err, 0); return NULL; } - return Jni_take_bytes(env, rv, out_len); + return (jbyteArray)Jni_take_array(env, Jni_B, rv, out_len); } diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Async_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Async_kt.snap deleted file mode 100644 index 9ef1e9d2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Async_kt.snap +++ /dev/null @@ -1,124 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package kitchen_sink - -import kotlin.coroutines.cancellation.CancellationException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.suspendCancellableCoroutine - -/** - * Bridges one native async call to a suspended coroutine. The JNI shim pins - * this object with a global reference and calls exactly one `on*` method, - * from whichever producer thread completes the call. - */ -internal class NativeCompletion( - private val cont: CancellableContinuation, - private val domain: Int, - private val convert: (Any?) -> T, - cancellable: Boolean, -) { - // The native cancel token, or 0 when the call isn't cancellable or has - // completed. Guarded by `this` so a late cancel never touches a freed - // token; a completion that lands inside `cancel` (on the same thread) - // defers the release until the cancel returns. - private var token: Long = if (cancellable) JniBridge.cancel_token_create() else 0L - private var cancelling = false - private var released = false - - val cancelToken: Long - get() = synchronized(this) { token } - - fun cancel() { - synchronized(this) { - if (token == 0L) return - cancelling = true - try { - JniBridge.cancel_token_cancel(token) - } finally { - cancelling = false - } - if (released) destroyToken() - } - } - - fun releaseToken() { - synchronized(this) { - if (cancelling) released = true else destroyToken() - } - } - - private fun destroyToken() { - if (token != 0L) JniBridge.cancel_token_destroy(token) - token = 0L - } - - fun onUnit() = succeed(Unit) - - fun onBoolean(value: Boolean) = succeed(value) - - fun onByte(value: Byte) = succeed(value) - - fun onShort(value: Short) = succeed(value) - - fun onInt(value: Int) = succeed(value) - - fun onLong(value: Long) = succeed(value) - - fun onFloat(value: Float) = succeed(value) - - fun onDouble(value: Double) = succeed(value) - - fun onBytes(value: ByteArray) = succeed(value) - - fun onError(code: Int, message: ByteArray, payload: ByteArray?) { - releaseToken() - val error = if (code == -5) { - CancellationException(decodeUtf8(message)) - } else { - JniBridge.error(domain, code, message, payload) - } - cont.resumeWithException(error) - } - - private fun succeed(raw: Any?) { - releaseToken() - val result = try { - convert(raw) - } catch (t: Throwable) { - cont.resumeWithException(t) - return - } - cont.resume(result) - } -} - -/** - * Starts a native async call and suspends until it completes. [launch] gets - * the cancel token (0 unless [cancellable]) and the completion to hand to the - * shim. Cancelling the coroutine cancels the native call; a cancelled call - * (code -5) resumes with [CancellationException]. - */ -internal suspend fun awaitNative( - cancellable: Boolean, - domain: Int, - convert: (Any?) -> T, - launch: (Long, NativeCompletion) -> Unit, -): T = suspendCancellableCoroutine { cont -> - val completion = NativeCompletion(cont, domain, convert, cancellable) - cont.invokeOnCancellation { completion.cancel() } - try { - launch(completion.cancelToken, completion) - } catch (t: Throwable) { - completion.releaseToken() - throw t - } -} - -// END Async.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Buffers_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Buffers_kt.snap deleted file mode 100644 index 722dd74b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Buffers_kt.snap +++ /dev/null @@ -1,255 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package kitchen_sink - -/** - * Growable little-endian writer for the value-buffer wire format that - * records, rich enums, optionals, lists, and maps cross the ABI in. - */ -internal class BufferWriter { - private var buf = ByteArray(64) - private var len = 0 - private var minted: ArrayList Unit>>? = null - - private fun reserve(extra: Int) { - if (len + extra <= buf.size) return - var cap = buf.size * 2 - while (cap < len + extra) cap *= 2 - buf = buf.copyOf(cap) - } - - fun toByteArray(): ByteArray = buf.copyOf(len) - - fun writeBool(v: Boolean) { - reserve(1) - buf[len++] = if (v) 1 else 0 - } - - fun writeI8(v: Byte) { - reserve(1) - buf[len++] = v - } - - fun writeU8(v: UByte) = writeI8(v.toByte()) - - fun writeI16(v: Short) { - reserve(2) - val b = v.toInt() - buf[len++] = b.toByte() - buf[len++] = (b shr 8).toByte() - } - - fun writeU16(v: UShort) = writeI16(v.toShort()) - - fun writeI32(v: Int) { - reserve(4) - buf[len++] = v.toByte() - buf[len++] = (v shr 8).toByte() - buf[len++] = (v shr 16).toByte() - buf[len++] = (v shr 24).toByte() - } - - fun writeU32(v: UInt) = writeI32(v.toInt()) - - fun writeI64(v: Long) { - writeI32(v.toInt()) - writeI32((v ushr 32).toInt()) - } - - fun writeU64(v: ULong) = writeI64(v.toLong()) - - fun writeF32(v: Float) = writeI32(v.toRawBits()) - - fun writeF64(v: Double) = writeI64(v.toRawBits()) - - fun writeString(v: String) = writeBytes(encodeUtf8(v)) - - fun writeBytes(v: ByteArray) { - writeI32(v.size) - reserve(v.size) - v.copyInto(buf, len) - len += v.size - } - - /** - * An object token: one new strong reference the reader adopts, which - * [abandon] releases with [release] if the encoding fails. - */ - fun writeObject(token: Long, release: (Long) -> Unit) { - val list = minted ?: ArrayList Unit>>().also { minted = it } - list.add(token to release) - writeI64(token) - } - - /** Releases every object token written so far, for an encoding that failed midway. */ - fun abandon() { - minted?.forEach { (token, release) -> release(token) } - minted = null - } - - inline fun writeOptional(v: T?, write: (T) -> Unit) { - if (v == null) { - writeBool(false) - } else { - writeBool(true) - write(v) - } - } - - inline fun writeList(v: List, write: (T) -> Unit) { - writeI32(v.size) - for (e in v) write(e) - } - - inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { - writeI32(v.size) - for ((k, e) in v) { - writeKey(k) - writeValue(e) - } - } -} - -/** - * Validating little-endian reader for the value-buffer wire format. A - * truncated buffer, an invalid flag byte, a zero object token, an oversized - * length, or trailing bytes raise [NativeBugException] code -3. - */ -internal class BufferReader(private val buf: ByteArray) { - private var pos = 0 - - private fun malformed(detail: String): Nothing = - throw NativeBugException(-3, "malformed value buffer: $detail") - - private fun take(n: Int): Int { - if (n > buf.size - pos) malformed("truncated buffer") - val at = pos - pos += n - return at - } - - fun readBool(): Boolean = when (buf[take(1)].toInt()) { - 0 -> false - 1 -> true - else -> malformed("invalid bool byte") - } - - fun readI8(): Byte = buf[take(1)] - - fun readU8(): UByte = readI8().toUByte() - - fun readI16(): Short { - val at = take(2) - return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() - } - - fun readU16(): UShort = readI16().toUShort() - - fun readI32(): Int { - val at = take(4) - return (buf[at].toInt() and 0xFF) or - ((buf[at + 1].toInt() and 0xFF) shl 8) or - ((buf[at + 2].toInt() and 0xFF) shl 16) or - ((buf[at + 3].toInt() and 0xFF) shl 24) - } - - fun readU32(): UInt = readI32().toUInt() - - fun readI64(): Long { - val lo = readI32().toLong() and 0xFFFFFFFFL - val hi = readI32().toLong() - return lo or (hi shl 32) - } - - fun readU64(): ULong = readI64().toULong() - - fun readF32(): Float = Float.fromBits(readI32()) - - fun readF64(): Double = Double.fromBits(readI64()) - - private fun readLen(): Int { - val n = readI32() - if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") - return n - } - - fun readString(): String { - val n = readLen() - val at = take(n) - return try { - Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() - } catch (e: java.nio.charset.CharacterCodingException) { - malformed("string is not valid UTF-8") - } - } - - fun readBytes(): ByteArray { - val n = readLen() - val at = take(n) - return buf.copyOfRange(at, at + n) - } - - /** An object token: one strong reference the caller adopts. */ - fun readObject(): Long { - val token = readI64() - if (token == 0L) malformed("null object token") - return token - } - - inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null - - inline fun readList(read: () -> T): List { - val n = readCount() - val out = ArrayList(minOf(n, 4096)) - repeat(n) { out.add(read()) } - return out - } - - inline fun readMap(readKey: () -> K, readValue: () -> V): Map { - val n = readCount() - val out = LinkedHashMap(minOf(n, 4096)) - repeat(n) { - val k = readKey() - out[k] = readValue() - } - return out - } - - /** An element count; elements may encode to zero bytes, so only the sign is checked. */ - fun readCount(): Int { - val n = readI32() - if (n < 0) malformed("negative element count") - return n - } - - fun expectEnd() { - if (pos != buf.size) malformed("trailing bytes after value") - } -} - -/** Encodes one value into a fresh value buffer. */ -internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { - val w = BufferWriter() - try { - write(w) - } catch (t: Throwable) { - w.abandon() - throw t - } - return w.toByteArray() -} - -/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ -internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { - val r = BufferReader(bytes) - val v = read(r) - r.expectEnd() - return v -} - -// END Buffers.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Codecs_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Codecs_kt.snap index b9aec5a9..577bbbcd 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Codecs_kt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Codecs_kt.snap @@ -7,36 +7,40 @@ source: crates/weaveffi-cli/tests/snapshots.rs package kitchen_sink -internal fun packListOfGadget(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeObject(it.cloneHandle(), JniBridge::kitchen_Gadget_destroy) } +internal fun pack_list_string(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeString(it) } -internal fun unpackListOfGadget(_r: BufferReader): List = _r.readList { Gadget.fromHandle(_r.readObject()) } +internal fun unpack_list_string(_r: BufferReader): List = _r.readList { _r.readString() } -internal fun packListOfItem(_w: BufferWriter, _v: List) = _w.writeList(_v) { packItem(_w, it) } +internal fun pack_map_string_string(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeString(it) }, { _w.writeString(it) }) -internal fun unpackListOfItem(_r: BufferReader): List = _r.readList { unpackItem(_r) } +internal fun unpack_map_string_string(_r: BufferReader): Map = _r.readMap({ _r.readString() }, { _r.readString() }) -internal fun packListOfString(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeString(it) } +internal fun pack_opt_i64(_w: BufferWriter, _v: Long?) = _w.writeOptional(_v) { _w.writeI64(it) } -internal fun unpackListOfString(_r: BufferReader): List = _r.readList { _r.readString() } +internal fun unpack_opt_i64(_r: BufferReader): Long? = _r.readOptional { _r.readI64() } -internal fun packMapOfStringToI32(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeString(it) }, { _w.writeI32(it) }) +internal fun pack_opt_Gadget(_w: BufferWriter, _v: Gadget?) = _w.writeOptional(_v) { _w.writeObject(it.cloneHandle(), JniBridge::kitchen_Gadget_destroy) } -internal fun unpackMapOfStringToI32(_r: BufferReader): Map = _r.readMap({ _r.readString() }, { _r.readI32() }) +internal fun unpack_opt_Gadget(_r: BufferReader): Gadget? = _r.readOptional { Gadget.fromHandle(_r.readObject()) } -internal fun packMapOfStringToString(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeString(it) }, { _w.writeString(it) }) +internal fun pack_list_Gadget(_w: BufferWriter, _v: List) = _w.writeList(_v) { _w.writeObject(it.cloneHandle(), JniBridge::kitchen_Gadget_destroy) } -internal fun unpackMapOfStringToString(_r: BufferReader): Map = _r.readMap({ _r.readString() }, { _r.readString() }) +internal fun unpack_list_Gadget(_r: BufferReader): List = _r.readList { Gadget.fromHandle(_r.readObject()) } -internal fun packOptionalOfGadget(_w: BufferWriter, _v: Gadget?) = _w.writeOptional(_v) { _w.writeObject(it.cloneHandle(), JniBridge::kitchen_Gadget_destroy) } +internal fun pack_opt_Item(_w: BufferWriter, _v: Item?) = _w.writeOptional(_v) { pack_Item(_w, it) } -internal fun unpackOptionalOfGadget(_r: BufferReader): Gadget? = _r.readOptional { Gadget.fromHandle(_r.readObject()) } +internal fun unpack_opt_Item(_r: BufferReader): Item? = _r.readOptional { unpack_Item(_r) } -internal fun packOptionalOfI64(_w: BufferWriter, _v: Long?) = _w.writeOptional(_v) { _w.writeI64(it) } +internal fun pack_list_Item(_w: BufferWriter, _v: List) = _w.writeList(_v) { pack_Item(_w, it) } -internal fun unpackOptionalOfI64(_r: BufferReader): Long? = _r.readOptional { _r.readI64() } +internal fun unpack_list_Item(_r: BufferReader): List = _r.readList { unpack_Item(_r) } -internal fun packOptionalOfItem(_w: BufferWriter, _v: Item?) = _w.writeOptional(_v) { packItem(_w, it) } +internal fun pack_map_string_i32(_w: BufferWriter, _v: Map) = _w.writeMap(_v, { _w.writeString(it) }, { _w.writeI32(it) }) -internal fun unpackOptionalOfItem(_r: BufferReader): Item? = _r.readOptional { unpackItem(_r) } +internal fun unpack_map_string_i32(_r: BufferReader): Map = _r.readMap({ _r.readString() }, { _r.readI32() }) + +internal fun pack_opt_string(_w: BufferWriter, _v: String?) = _w.writeOptional(_v) { _w.writeString(it) } + +internal fun unpack_opt_string(_r: BufferReader): String? = _r.readOptional { _r.readString() } // END Codecs.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap index 904daec0..e81bd098 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap @@ -14,33 +14,39 @@ package kitchen_sink */ internal object JniBridge { init { - NativeLibrary.ensureLoaded() + NativeLibrary.load() } /** - * The exception for a native failure: through error domain [domain] for a - * throwing call, or a [NativeBugException] (domain 0) for one that can't fail. + * The exception for a native failure of a call whose errors map through + * [domain]: 0 for a call that can't fail ([NativeBugException]), 1 for + * `throws: any` ([FfiException]), or an error domain, whose unknown codes + * map to the domain's base exception. Runtime (negative) codes are + * [FfiException]s. */ @JvmStatic fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): Throwable { val text = decodeUtf8(message) - return when (domain) { - 1 -> KitchenErrorsException.fromCode(code, text, payload) - else -> NativeBugException(code, text) + return when { + domain == 0 -> NativeBugException(code, text) + code < 0 -> FfiException(code, text) + domain == 2 -> KitchenException.fromCode(code, text, payload) + domain == 3 -> PantryException.fromCode(code, text, payload) + else -> FfiException(code, text) } } /** * Reports a failed callback through its vtable entry's `out_err` ([err]): - * [typed], a domain error the method is declared to throw, with its code and - * payload fields, and anything else as code -4 with its message. + * [typed], an exception of the domain the method throws, with its code and + * payload fields, and anything else with code [fallback] and its message. */ - private fun fail(err: Long, error: Throwable, typed: FfiException?) { + private fun fail(err: Long, error: Throwable, typed: FfiException?, fallback: Int) { if (typed != null) { error_set(err, typed.code, encodeUtf8(typed.message ?: "")) typed.encodePayload()?.let { error_set_payload(err, it) } } else { - error_set(err, -4, encodeUtf8(error.message ?: error.toString())) + error_set(err, fallback, encodeUtf8(error.message ?: error.toString())) } } @@ -66,22 +72,42 @@ internal object JniBridge { @JvmStatic external fun kitchen_maybe_gadget(fallback: Long): Long @JvmStatic external fun kitchen_gadgets(): ByteArray @JvmStatic external fun kitchen_stream_gadgets(): Long - @JvmStatic external fun kitchen_StreamGadgetsIterator_next(_iter: Long, _out: LongArray): Boolean + @JvmStatic external fun kitchen_StreamGadgetsIterator_next(_iter: Long, _out: Array): Boolean @JvmStatic external fun kitchen_StreamGadgetsIterator_destroy(_iter: Long) @JvmStatic external fun kitchen_maybe_item(id: Long): ByteArray @JvmStatic external fun kitchen_list_items(): ByteArray @JvmStatic external fun kitchen_get_attrs(): ByteArray @JvmStatic external fun kitchen_stream_items(): Long - @JvmStatic external fun kitchen_StreamItemsIterator_next(_iter: Long): ByteArray? + @JvmStatic external fun kitchen_StreamItemsIterator_next(_iter: Long, _out: Array): Boolean @JvmStatic external fun kitchen_StreamItemsIterator_destroy(_iter: Long) @JvmStatic external fun kitchen_cross_module_token(): ByteArray @JvmStatic external fun kitchen_do_async(input: ByteArray, _done: NativeCompletion<*>) @JvmStatic external fun kitchen_do_cancellable(input: ByteArray, _token: Long, _done: NativeCompletion<*>) @JvmStatic external fun kitchen_legacy_op(): Int @JvmStatic external fun kitchen_new_op(): Int + @JvmStatic external fun kitchen_opt_i32_id(has_x: Boolean, x: Int): Int? + @JvmStatic external fun kitchen_opt_bool_id(has_x: Boolean, x: Boolean): Boolean? + @JvmStatic external fun kitchen_opt_priority(has_p: Boolean, p: Int): Int? + @JvmStatic external fun kitchen_opt_mixed(base: Double, has_scale: Boolean, scale: Float, has_offset: Boolean, offset: Long, label: ByteArray): Short? + @JvmStatic external fun kitchen_sum_f64(xs: DoubleArray): Double + @JvmStatic external fun kitchen_scale_i32(xs: IntArray, factor: Int): IntArray + @JvmStatic external fun kitchen_ids_u64(ids: LongArray): LongArray + @JvmStatic external fun kitchen_mixed_slices(a: ByteArray, b: ShortArray, c: ShortArray, d: IntArray, e: LongArray, f: FloatArray): IntArray + @JvmStatic external fun kitchen_async_maybe_count(_done: NativeCompletion<*>) + @JvmStatic external fun kitchen_async_samples(n: Int, _token: Long, _done: NativeCompletion<*>) + @JvmStatic external fun kitchen_stream_maybe(): Long + @JvmStatic external fun kitchen_StreamMaybeIterator_next(_iter: Long, _out: Array): Boolean + @JvmStatic external fun kitchen_StreamMaybeIterator_destroy(_iter: Long) + @JvmStatic external fun kitchen_stream_chunks(): Long + @JvmStatic external fun kitchen_StreamChunksIterator_next(_iter: Long, _out: Array): Boolean + @JvmStatic external fun kitchen_StreamChunksIterator_destroy(_iter: Long) + @JvmStatic external fun kitchen_sample_with(sampler: Sampler, has_level: Boolean, level: Int): ShortArray + @JvmStatic external fun kitchen_take_from_pantry(item: ByteArray): Int + @JvmStatic external fun kitchen_parse_any(text: ByteArray): Long @JvmStatic external fun kitchen_Gadget_new(id: Long): Long @JvmStatic external fun kitchen_Gadget_describe(_self: Long): ByteArray @JvmStatic external fun kitchen_Gadget_poke(_self: Long, times: Int): Int + @JvmStatic external fun kitchen_Gadget_readings(_self: Long, has_limit: Boolean, limit: Int): DoubleArray @JvmStatic external fun kitchen_Gadget_version(): ByteArray @JvmStatic external fun kitchen_nested_hello(): ByteArray @@ -90,7 +116,7 @@ internal object JniBridge { try { _impl.onReady(code, decodeUtf8(msg)) } catch (_e: Throwable) { - fail(_err, _e, null) + fail(_err, _e, null, -4) } } @@ -98,9 +124,9 @@ internal object JniBridge { fun kitchen_ReadyListener_on_item(_impl: ReadyListener, _err: Long, item: ByteArray, gadget: Long): Boolean { val _gadget = Gadget.fromHandle(gadget) return try { - _impl.onItem(decodeBuffer(item) { _r -> unpackItem(_r) }, _gadget) + _impl.onItem(decodeBuffer(item) { _r -> unpack_Item(_r) }, _gadget) } catch (_e: Throwable) { - fail(_err, _e, null) + fail(_err, _e, null, -4) false } } @@ -109,15 +135,15 @@ internal object JniBridge { fun kitchen_ReadyListener_label(_impl: ReadyListener, _err: Long): ByteArray? = try { encodeUtf8(_impl.label()) } catch (_e: Throwable) { - fail(_err, _e, _e as? KitchenErrorsException) + fail(_err, _e, _e as? KitchenException, -1) null } @JvmStatic fun kitchen_ReadyListener_latest(_impl: ReadyListener, _err: Long): ByteArray? = try { - encodeBuffer { _w -> packOptionalOfItem(_w, _impl.latest()) } + encodeBuffer { _w -> pack_opt_Item(_w, _impl.latest()) } } catch (_e: Throwable) { - fail(_err, _e, null) + fail(_err, _e, null, -4) null } @@ -125,9 +151,33 @@ internal object JniBridge { fun kitchen_ReadyListener_favorite(_impl: ReadyListener, _err: Long): Long = try { _impl.favorite().cloneHandle() } catch (_e: Throwable) { - fail(_err, _e, null) + fail(_err, _e, null, -4) 0L } + + @JvmStatic + fun kitchen_Sampler_threshold(_impl: Sampler, _err: Long, has_level: Boolean, level: Int): Double? = try { + _impl.threshold(if (has_level) level.toUInt() else null) + } catch (_e: Throwable) { + fail(_err, _e, null, -4) + null + } + + @JvmStatic + fun kitchen_Sampler_samples(_impl: Sampler, _err: Long, has_limit: Boolean, limit: Int, weights: DoubleArray): ShortArray? = try { + _impl.samples(if (has_limit) limit.toUInt() else null, weights.asList()).toShortArray() + } catch (_e: Throwable) { + fail(_err, _e, _e as? PantryException, -1) + null + } + + @JvmStatic + fun kitchen_Sampler_describe(_impl: Sampler, _err: Long): ByteArray? = try { + encodeUtf8(_impl.describe()) + } catch (_e: Throwable) { + fail(_err, _e, null, -1) + null + } } // END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap index ea1d387c..8c70ac6b 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap @@ -7,17 +7,46 @@ source: crates/weaveffi-cli/tests/snapshots.rs package kitchen_sink -/** Errors of the `KitchenErrors` domain. */ -sealed class KitchenErrorsException(code: Int, message: String) : FfiException(code, message) { - class NotFound(message: String = "Item not found") : KitchenErrorsException(1, message) - class InvalidInput(message: String = "Invalid input") : KitchenErrorsException(2, message) +/** + * Errors of the `KitchenErrors` domain, one subclass per code. + * + * A code these bindings don't declare (from a newer library) is a plain `KitchenException` + * carrying that [code] and the library's message. + */ +open class KitchenException internal constructor(code: Int, message: String) : FfiException(code, message) { + class NotFound(message: String = "Item not found") : KitchenException(1, message) + class InvalidInput(message: String = "Invalid input") : KitchenException(2, message) internal companion object { - /** The exception for a raw code and its serialized payload. */ - fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { + /** The exception for a positive code and its serialized payload. */ + fun fromCode(code: Int, message: String, payload: ByteArray?): KitchenException = when (code) { 1 -> NotFound(message) 2 -> InvalidInput(message) - else -> FfiException(code, message) + else -> KitchenException(code, message) + } + } +} + +/** + * Errors of the `PantryError` domain, one subclass per code. + * + * A code these bindings don't declare (from a newer library) is a plain `PantryException` + * carrying that [code] and the library's message. + */ +open class PantryException internal constructor(code: Int, message: String) : FfiException(code, message) { + class OutOfStock(val item: String, message: String = "Out of stock") : PantryException(1, message) { + override fun encodePayload(): ByteArray = encodeBuffer { _w -> + _w.writeString(item) + } + } + class Spoiled(message: String = "Spoiled") : PantryException(2, message) + + internal companion object { + /** The exception for a positive code and its serialized payload. */ + fun fromCode(code: Int, message: String, payload: ByteArray?): PantryException = when (code) { + 1 -> if (payload == null) PantryException(code, message) else decodeBuffer(payload) { _r -> OutOfStock(_r.readString(), message) } + 2 -> Spoiled(message) + else -> PantryException(code, message) } } } @@ -33,6 +62,7 @@ enum class Priority(val value: Int) { companion object { /** The variant whose ABI value is [value]. */ + @JvmStatic fun fromValue(value: Int): Priority = entries.firstOrNull { it.value == value } ?: throw NativeBugException(-3, "unknown Priority value $value") } @@ -60,9 +90,46 @@ data class Item( val priority: Priority, /** An optional object inside a record */ val gadget: Gadget?, -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Item) return false + return id == other.id && + name == other.name && + count == other.count && + enabled == other.enabled && + ratio == other.ratio && + nick == other.nick && + payload.contentEquals(other.payload) && + tags == other.tags && + attrs == other.attrs && + parent == other.parent && + token == other.token && + priority == other.priority && + gadget == other.gadget + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + count.hashCode() + result = 31 * result + enabled.hashCode() + result = 31 * result + ratio.hashCode() + result = 31 * result + nick.hashCode() + result = 31 * result + payload.contentHashCode() + result = 31 * result + tags.hashCode() + result = 31 * result + attrs.hashCode() + result = 31 * result + parent.hashCode() + result = 31 * result + token.hashCode() + result = 31 * result + priority.hashCode() + result = 31 * result + gadget.hashCode() + return result + } -internal fun packItem(_w: BufferWriter, _v: Item) { + override fun toString(): String = "Item(id=${id}, name=${name}, count=${count}, enabled=${enabled}, ratio=${ratio}, nick=${nick}, payload=${payload.contentToString()}, tags=${tags}, attrs=${attrs}, parent=${parent}, token=${token}, priority=${priority}, gadget=${gadget})" +} + +internal fun pack_Item(_w: BufferWriter, _v: Item) { _w.writeI64(_v.id) _w.writeString(_v.name) _w.writeI32(_v.count) @@ -70,15 +137,15 @@ internal fun packItem(_w: BufferWriter, _v: Item) { _w.writeF64(_v.ratio) _w.writeU32(_v.nick) _w.writeBytes(_v.payload) - packListOfString(_w, _v.tags) - packMapOfStringToString(_w, _v.attrs) - packOptionalOfI64(_w, _v.parent) - packToken(_w, _v.token) + pack_list_string(_w, _v.tags) + pack_map_string_string(_w, _v.attrs) + pack_opt_i64(_w, _v.parent) + pack_Token(_w, _v.token) _w.writeI32(_v.priority.value) - packOptionalOfGadget(_w, _v.gadget) + pack_opt_Gadget(_w, _v.gadget) } -internal fun unpackItem(_r: BufferReader): Item = Item( +internal fun unpack_Item(_r: BufferReader): Item = Item( _r.readI64(), _r.readString(), _r.readI32(), @@ -86,12 +153,12 @@ internal fun unpackItem(_r: BufferReader): Item = Item( _r.readF64(), _r.readU32(), _r.readBytes(), - unpackListOfString(_r), - unpackMapOfStringToString(_r), - unpackOptionalOfI64(_r), - unpackToken(_r), + unpack_list_string(_r), + unpack_map_string_string(_r), + unpack_opt_i64(_r), + unpack_Token(_r), Priority.fromValue(_r.readI32()), - unpackOptionalOfGadget(_r), + unpack_opt_Gadget(_r), ) /** Consumer-implemented observer of ready items */ @@ -105,8 +172,10 @@ interface ReadyListener { /** * A display label, failing with a kitchen error * - * Throw a [KitchenErrorsException] to report a typed error to the library. + * Throw a [KitchenException] to report a typed error to the library; anything else + * thrown reaches it as an untyped failure with its message. */ + @Throws(KitchenException::class) fun label(): String /** The most recent item the listener kept, if any */ @@ -116,6 +185,29 @@ interface ReadyListener { fun favorite(): Gadget } +/** Consumer-implemented source of scalar samples */ +interface Sampler { + /** The cutoff for a level, if the sampler has one */ + fun threshold(level: UInt?): Double? + + /** + * Up to `limit` samples, failing when the pantry is empty + * + * Throw a [PantryException] to report a typed error to the library; anything else + * thrown reaches it as an untyped failure with its message. + */ + @Throws(PantryException::class) + fun samples(limit: UInt?, weights: List): List + + /** + * A free-form description, failing with an untyped error + * + * Anything thrown reaches the library as a failure with its message. + */ + @Throws(Exception::class) + fun describe(): String +} + /** A pocket-sized interface exercising the object surface */ class Gadget private constructor(address: Long) : AutoCloseable { internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::kitchen_Gadget_destroy)) @@ -129,10 +221,17 @@ class Gadget private constructor(address: Long) : AutoCloseable { } /** Poke the gadget a number of times, failing on a negative count */ + @Throws(KitchenException::class) fun poke(times: Int): Int = handle.borrow { _self -> JniBridge.kitchen_Gadget_poke(_self, times) } + /** The gadget's sensor readings, optionally limited */ + @Throws(FfiException::class) + fun readings(limit: UInt?): List = handle.borrow { _self -> + JniBridge.kitchen_Gadget_readings(_self, limit != null, limit?.toInt() ?: 0).asList() + } + /** Releases this wrapper's native reference; safe to call more than once. */ override fun close() = handle.close() @@ -144,86 +243,170 @@ class Gadget private constructor(address: Long) : AutoCloseable { internal fun fromHandleOrNull(address: Long): Gadget? = if (address == 0L) null else Gadget(address) /** Create a gadget with the given id */ + @JvmStatic operator fun invoke(id: Long): Gadget = Gadget.fromHandle(JniBridge.kitchen_Gadget_new(id)) /** The gadget subsystem version string */ + @JvmStatic fun version(): String = decodeUtf8(JniBridge.kitchen_Gadget_version()) } } object Kitchen { /** Identity for bool */ + @JvmStatic fun boolId(x: Boolean): Boolean = JniBridge.kitchen_bool_id(x) + @JvmStatic fun i32Id(x: Int): Int = JniBridge.kitchen_i32_id(x) + @JvmStatic fun u32Id(x: UInt): UInt = JniBridge.kitchen_u32_id(x.toInt()).toUInt() + @JvmStatic fun i64Id(x: Long): Long = JniBridge.kitchen_i64_id(x) + @JvmStatic fun f64Id(x: Double): Double = JniBridge.kitchen_f64_id(x) + @JvmStatic fun echoString(s: String): String = decodeUtf8(JniBridge.kitchen_echo_string(encodeUtf8(s))) + @JvmStatic fun echoBytes(b: ByteArray): ByteArray = JniBridge.kitchen_echo_bytes(b) /** Register a listener, returning how many are attached */ + @JvmStatic fun subscribe(listener: ReadyListener): Int = JniBridge.kitchen_subscribe(listener) /** Register a listener if one is given */ + @JvmStatic fun maybeSubscribe(listener: ReadyListener?): Boolean = JniBridge.kitchen_maybe_subscribe(listener) /** Optional object in both directions */ + @JvmStatic fun maybeGadget(fallback: Gadget?): Gadget? = fallback?.handle.borrowOrNull { _h0 -> Gadget.fromHandleOrNull(JniBridge.kitchen_maybe_gadget(_h0)) } /** A list of objects */ - fun gadgets(): List = decodeBuffer(JniBridge.kitchen_gadgets()) { _r -> unpackListOfGadget(_r) } + @JvmStatic + fun gadgets(): List = decodeBuffer(JniBridge.kitchen_gadgets()) { _r -> unpack_list_Gadget(_r) } /** An iterator over objects */ - fun streamGadgets(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_gadgets(), JniBridge::kitchen_StreamGadgetsIterator_destroy) { _it -> - val _slot = LongArray(1) - if (JniBridge.kitchen_StreamGadgetsIterator_next(_it, _slot)) Gadget.fromHandle(_slot[0]) else NativeIterator.DONE - } + @JvmStatic + fun streamGadgets(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_gadgets(), JniBridge::kitchen_StreamGadgetsIterator_destroy, JniBridge::kitchen_StreamGadgetsIterator_next) { Gadget.fromHandle(it as Long) } /** Optional struct return */ - fun maybeItem(id: Long): Item? = decodeBuffer(JniBridge.kitchen_maybe_item(id)) { _r -> unpackOptionalOfItem(_r) } + @Throws(KitchenException::class) + @JvmStatic + fun maybeItem(id: Long): Item? = decodeBuffer(JniBridge.kitchen_maybe_item(id)) { _r -> unpack_opt_Item(_r) } /** List of structs */ - fun listItems(): List = decodeBuffer(JniBridge.kitchen_list_items()) { _r -> unpackListOfItem(_r) } + @JvmStatic + fun listItems(): List = decodeBuffer(JniBridge.kitchen_list_items()) { _r -> unpack_list_Item(_r) } /** Map return type */ - fun getAttrs(): Map = decodeBuffer(JniBridge.kitchen_get_attrs()) { _r -> unpackMapOfStringToI32(_r) } + @JvmStatic + fun getAttrs(): Map = decodeBuffer(JniBridge.kitchen_get_attrs()) { _r -> unpack_map_string_i32(_r) } /** Iterator return type */ - fun streamItems(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_items(), JniBridge::kitchen_StreamItemsIterator_destroy) { _it -> - val _item = JniBridge.kitchen_StreamItemsIterator_next(_it) - if (_item == null) NativeIterator.DONE else decodeBuffer(_item) { _r -> unpackItem(_r) } - } + @JvmStatic + fun streamItems(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_items(), JniBridge::kitchen_StreamItemsIterator_destroy, JniBridge::kitchen_StreamItemsIterator_next) { decodeBuffer(it as ByteArray) { _r -> unpack_Item(_r) } } /** Returns the shared Token type from another module */ - fun crossModuleToken(): Token = decodeBuffer(JniBridge.kitchen_cross_module_token()) { _r -> unpackToken(_r) } + @JvmStatic + fun crossModuleToken(): Token = decodeBuffer(JniBridge.kitchen_cross_module_token()) { _r -> unpack_Token(_r) } /** Async operation */ - suspend fun doAsync(input: String): String = awaitNative(false, 0, { _raw -> decodeUtf8(_raw as ByteArray) }) { _, _done -> + @JvmStatic + suspend fun doAsync(input: String): String = awaitNative(false, 0, { decodeUtf8(it as ByteArray) }) { _, _done -> JniBridge.kitchen_do_async(encodeUtf8(input), _done) } /** Cancellable async operation */ - suspend fun doCancellable(input: String): String = awaitNative(true, 0, { _raw -> decodeUtf8(_raw as ByteArray) }) { _token, _done -> + @JvmStatic + suspend fun doCancellable(input: String): String = awaitNative(true, 0, { decodeUtf8(it as ByteArray) }) { _token, _done -> JniBridge.kitchen_do_cancellable(encodeUtf8(input), _token, _done) } /** Legacy operation kept for compatibility */ - @Deprecated("Use new_op instead") + @Deprecated("Use `newOp` instead") + @JvmStatic fun legacyOp(): Int = JniBridge.kitchen_legacy_op() - /** Replacement for legacy_op */ + /** Replacement for `legacyOp` */ + @JvmStatic fun newOp(): Int = JniBridge.kitchen_new_op() + /** Optional scalar in both directions */ + @JvmStatic + fun optI32Id(x: Int?): Int? = JniBridge.kitchen_opt_i32_id(x != null, x ?: 0) + + @JvmStatic + fun optBoolId(x: Boolean?): Boolean? = JniBridge.kitchen_opt_bool_id(x != null, x ?: false) + + /** Optional C-style enum in both directions */ + @JvmStatic + fun optPriority(p: Priority?): Priority? = JniBridge.kitchen_opt_priority(p != null, p?.value ?: 0)?.let { Priority.fromValue(it) } + + /** Optional scalars next to ordinary parameters */ + @JvmStatic + fun optMixed(base: Double, scale: Float?, offset: Long?, label: String?): UShort? = JniBridge.kitchen_opt_mixed(base, scale != null, scale ?: 0f, offset != null, offset ?: 0L, encodeBuffer { _w -> pack_opt_string(_w, label) })?.toUShort() + + /** A typed-array parameter */ + @JvmStatic + fun sumF64(xs: List): Double = JniBridge.kitchen_sum_f64(xs.toDoubleArray()) + + /** Typed arrays in both directions */ + @JvmStatic + fun scaleI32(xs: List, factor: Int): List = JniBridge.kitchen_scale_i32(xs.toIntArray(), factor).asList() + + @JvmStatic + fun idsU64(ids: List): List = JniBridge.kitchen_ids_u64(ids.toLongArrayBits()).toULongList() + + /** Every slice element type */ + @JvmStatic + fun mixedSlices(a: List, b: List, c: List, d: List, e: List, f: List): List = JniBridge.kitchen_mixed_slices(a.toByteArray(), b.toShortArray(), c.toShortArrayBits(), d.toIntArrayBits(), e.toLongArray(), f.toFloatArray()).toUIntList() + + /** Async returning an optional scalar */ + @JvmStatic + suspend fun asyncMaybeCount(): Long? = awaitNative(false, 0, { it as Long? }) { _, _done -> + JniBridge.kitchen_async_maybe_count(_done) + } + + /** Async returning a typed array */ + @JvmStatic + suspend fun asyncSamples(n: UInt): List = awaitNative(true, 0, { (it as FloatArray).asList() }) { _token, _done -> + JniBridge.kitchen_async_samples(n.toInt(), _token, _done) + } + + /** Iterator of optional scalars */ + @JvmStatic + fun streamMaybe(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_maybe(), JniBridge::kitchen_StreamMaybeIterator_destroy, JniBridge::kitchen_StreamMaybeIterator_next) { it as Int? } + + /** Iterator of typed arrays */ + @JvmStatic + fun streamChunks(): NativeIterator> = NativeIterator(JniBridge.kitchen_stream_chunks(), JniBridge::kitchen_StreamChunksIterator_destroy, JniBridge::kitchen_StreamChunksIterator_next) { (it as IntArray).asList() } + + /** Drive a consumer-implemented sampler */ + @Throws(PantryException::class) + @JvmStatic + fun sampleWith(sampler: Sampler, level: UInt?): List = JniBridge.kitchen_sample_with(sampler, level != null, level?.toInt() ?: 0).asList() + + /** Fails with the second domain */ + @Throws(PantryException::class) + @JvmStatic + fun takeFromPantry(item: String): Int = JniBridge.kitchen_take_from_pantry(encodeUtf8(item)) + + /** Fails with an untyped error */ + @Throws(FfiException::class) + @JvmStatic + fun parseAny(text: String): Long = JniBridge.kitchen_parse_any(encodeUtf8(text)) + object Nested { /** Trivial nested-module function */ + @JvmStatic fun hello(): String = decodeUtf8(JniBridge.kitchen_nested_hello()) } } diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Runtime_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Runtime_kt.snap deleted file mode 100644 index 61ac3018..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Runtime_kt.snap +++ /dev/null @@ -1,229 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package kitchen_sink - -import java.lang.ref.PhantomReference -import java.lang.ref.ReferenceQueue -import java.util.Collections -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicLong - -/** - * A failure a throwing call reports. Each error domain subclasses it with - * one class per declared (positive) code; a runtime failure keeps this class - * with its negative [code]: -1 a generic producer error, -2 a producer - * panic, -3 a marshalling failure, or -4 a callback implementation that - * failed. - */ -open class FfiException(val code: Int, message: String) : Exception(message) { - /** - * The fields of a typed error as the value buffer a callback reports - * with its code, or `null` when the code declares none. - */ - internal open fun encodePayload(): ByteArray? = null -} - -/** - * A call that can't fail failed anyway: a bug in the native library rather - * than an error to handle. [code] is the runtime code (-2 a producer panic, - * -3 a marshalling failure, -4 a callback failure the call couldn't report, - * and so on); the message names it and carries the producer's message. - */ -class NativeBugException(val code: Int, message: String) : - IllegalStateException("native call failed with code $code: $message") - -/** - * A lazy sequence streamed from the native library, one element per step. - * The native iterator is released when the sequence is exhausted, when - * [close] is called, or once this object becomes unreachable, whichever - * comes first. - */ -class NativeIterator internal constructor( - address: Long, - release: (Long) -> Unit, - private val step: (Long) -> Any?, -) : Iterator, AutoCloseable { - private val handle = NativeCleaner.register(this, NativeHandle(address, release)) - private var lookahead: Any? = EMPTY - - override fun hasNext(): Boolean { - if (lookahead !== EMPTY) return true - if (handle.isClosed) return false - val item = handle.borrow(step) - if (item === DONE) { - close() - return false - } - lookahead = item - return true - } - - override fun next(): T { - if (!hasNext()) throw NoSuchElementException() - val item = lookahead - lookahead = EMPTY - @Suppress("UNCHECKED_CAST") - return item as T - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() = handle.close() - - internal companion object { - /** What a step function returns once the native sequence is exhausted. */ - val DONE = Any() - private val EMPTY = Any() - } -} - -/** Loads the producer library and then the JNI shim, exactly once. */ -internal object NativeLibrary { - private const val LIBRARY = "kitchen_sink" - private const val JNI_LIBRARY = "kitchen_sink_jni" - - init { - val explicit = System.getenv("KITCHEN_SINK_LIBRARY") - if (!explicit.isNullOrEmpty()) { - System.load(explicit) - } else if (!loadBundled(LIBRARY)) { - // The shim's own link dependency usually finds the producer when - // it isn't on java.library.path, so a miss here is only fatal if - // loading the shim fails too. - try { - System.loadLibrary(LIBRARY) - } catch (ignored: UnsatisfiedLinkError) { - } - } - if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) - } - - fun ensureLoaded() {} - - /** - * Loads `base` from the `natives/-/` classpath resources a - * packaged desktop build bundles; false when it isn't bundled. - */ - private fun loadBundled(base: String): Boolean { - val os = System.getProperty("os.name").lowercase() - val arch = System.getProperty("os.arch").lowercase() - val osId = when { - os.contains("mac") || os.contains("darwin") -> "darwin" - os.contains("win") -> "windows" - else -> "linux" - } - val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" - val file = when (osId) { - "darwin" -> "lib$base.dylib" - "windows" -> "$base.dll" - else -> "lib$base.so" - } - val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false - val dir = java.nio.file.Files.createTempDirectory("$base-natives") - val target = dir.resolve(file) - stream.use { java.nio.file.Files.copy(it, target) } - target.toFile().deleteOnExit() - dir.toFile().deleteOnExit() - System.load(target.toAbsolutePath().toString()) - return true - } -} - -/** - * One strong reference to a native object, released exactly once: by - * [close], or by [NativeCleaner] once the owning wrapper is unreachable. - * Every call borrows the address for its duration, and a close that races an - * in-flight call defers the release until the last borrow ends, so the object - * is never freed while native code is using it. - */ -internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { - // Bit 0 is the closed flag; the remaining bits count in-flight borrows. - private val state = AtomicLong(0L) - - val isClosed: Boolean get() = state.get() and 1L != 0L - - fun acquire(): Long { - while (true) { - val s = state.get() - check(s and 1L == 0L) { "native object used after close()" } - if (state.compareAndSet(s, s + 2L)) return address - } - } - - fun unacquire() { - if (state.addAndGet(-2L) == 1L) release(address) - } - - fun close() { - while (true) { - val s = state.get() - if (s and 1L != 0L) return - if (state.compareAndSet(s, s or 1L)) { - if (s == 0L) release(address) - return - } - } - } -} - -/** Runs [block] with the native address borrowed for its duration. */ -internal inline fun NativeHandle.borrow(block: (Long) -> R): R { - val address = acquire() - try { - return block(address) - } finally { - unacquire() - } -} - -/** [borrow] for an optional object: a missing object lends address 0. */ -internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = - if (this == null) block(0L) else borrow(block) - -/** - * Releases the native reference of every wrapper that becomes unreachable - * without being closed. A daemon thread drains a phantom-reference queue, so - * this works on every Android API level and every JVM. - */ -internal object NativeCleaner { - private val queue = ReferenceQueue() - private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) - - private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : - PhantomReference(owner, queue) - - init { - val thread = Thread({ drain() }, "kitchen_sink-cleaner") - thread.isDaemon = true - thread.start() - } - - /** Releases [handle] once [owner] is unreachable; returns [handle]. */ - fun register(owner: Any, handle: NativeHandle): NativeHandle { - pending.add(Entry(owner, handle, queue)) - return handle - } - - private fun drain() { - while (true) { - val entry = queue.remove() as Entry - pending.remove(entry) - try { - entry.handle.close() - } catch (_: Throwable) { - } - } - } -} - -/** UTF-8 bytes of [value], for a string crossing into native code. */ -internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) - -/** The string a native UTF-8 byte sequence spells. */ -internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) - -// END Runtime.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap index 9a474ddf..c58762ab 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap @@ -10,18 +10,19 @@ package kitchen_sink /** A reusable token referenced across modules */ data class Token(val id: Long, val label: String) -internal fun packToken(_w: BufferWriter, _v: Token) { +internal fun pack_Token(_w: BufferWriter, _v: Token) { _w.writeI64(_v.id) _w.writeString(_v.label) } -internal fun unpackToken(_r: BufferReader): Token = Token( +internal fun unpack_Token(_r: BufferReader): Token = Token( _r.readI64(), _r.readString(), ) object Shared { /** Trivial cross-module helper */ + @JvmStatic fun ping(): String = decodeUtf8(JniBridge.shared_ping()) } diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_jni_c.snap deleted file mode 100644 index 93dc9bd9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_jni_c.snap +++ /dev/null @@ -1,254 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#include -#include -#include -#include -#include -#include -#include - -/* The shim forwards deprecated functions too, so it doesn't warn about them. */ -#define NESTED_MODULES_DEPRECATED(msg) -#include "nested_modules.h" - -/* Every static name in this file starts with `Jni_`, which no C ABI symbol - (all lowercase `nested_modules_...`) and no JNI export (`Java_...`) can - spell. */ - -static JavaVM* Jni_vm = NULL; -static jclass Jni_bridge = NULL; -static jmethodID Jni_error = NULL; - -/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value - buffers all cross as one. */ -typedef struct { - jbyteArray array; - jbyte* elems; - const uint8_t* ptr; - size_t len; -} Jni_bytes; - -static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { - Jni_bytes b = {array, NULL, NULL, 0}; - if (array == NULL) { - return b; - } - b.len = (size_t)(*env)->GetArrayLength(env, array); - b.elems = (*env)->GetByteArrayElements(env, array, NULL); - b.ptr = (const uint8_t*)b.elems; - if (b.elems == NULL) { - b.len = 0; - } - return b; -} - -static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { - if (b->elems != NULL) { - (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); - } -} - -/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid - input, so NULL + 0 is the empty array). */ -static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = (*env)->NewByteArray(env, (jsize)len); - if (out != NULL && len > 0 && ptr != NULL) { - (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); - } - return out; -} - -/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ -static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = Jni_new_bytes(env, ptr, len); - if (ptr != NULL) { - nested_modules_free_bytes((uint8_t*)ptr, len); - } - return out; -} - -/* Throws the Kotlin exception for `err` (mapped through error domain - `domain`, or 0 for a call that can't fail, which raises the trap) and - clears `err`. */ -static inline void Jni_throw(JNIEnv* env, nested_modules_error* err, jint domain) { - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; - if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); - } - jint code = (jint)err->code; - nested_modules_error_clear(err); - if ((*env)->ExceptionCheck(env)) { - return; - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { - (*env)->Throw(env, ex); - } -} - -static jint Jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { - (*env)->ThrowNew(env, cls, message); - } - return JNI_ERR; -} - -/* One contract entry these bindings were generated with: a declaration's - id and signature hash, plus its dotted path for the load error. */ -typedef struct { - uint64_t id; - uint64_t hash; - const char* path; -} Jni_contract_entry; - -/* Checks that the library's contract table (from `contract`, sorted by id) - has every expected entry with an equal hash; entries only the library - has are fine. Fails loading with an error naming the first declaration - that's missing or changed. */ -static inline jint Jni_check_contract(JNIEnv* env, const nested_modules_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { - size_t len = 0; - const nested_modules_contract_entry* table = contract(&len); - if (table == NULL) { - len = 0; - } - for (size_t i = 0; i < count; i++) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) { - lo = mid + 1; - } else { - hi = mid; - } - } - const char* problem = NULL; - if (lo == len || table[lo].id != expected[i].id) { - problem = "is missing from the library"; - } else if (table[lo].hash != expected[i].hash) { - problem = "changed since these bindings were generated"; - } - if (problem != NULL) { - char message[512]; - snprintf(message, sizeof message, "%s: %s %s", "nested_modules", expected[i].path, problem); - return Jni_load_error(env, message); - } - } - return JNI_OK; -} - -/* Generated below: the contract checks and the method IDs the shim caches. */ -static jint Jni_load(JNIEnv* env); - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env = NULL; - (void)reserved; - Jni_vm = vm; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - if (nested_modules_abi_version() != 4u) { - return Jni_load_error(env, "nested_modules: the native library implements a different C ABI revision than these bindings (expected 4)"); - } - jclass bridge = (*env)->FindClass(env, "nested_modules/JniBridge"); - if (bridge == NULL) { - return JNI_ERR; - } - Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); - Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ljava/lang/Throwable;"); - if (Jni_error == NULL) { - return JNI_ERR; - } - if (Jni_load(env) != JNI_OK) { - return JNI_ERR; - } - return JNI_VERSION_1_6; -} - -JNIEXPORT jlong JNICALL Java_nested_1modules_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { - (void)env; - (void)cls; - return (jlong)nested_modules_debug_live((int32_t)kind); -} - -static const Jni_contract_entry Jni_contract_graphics[] = { - {UINT64_C(0x2c29eef9327a6bd4), UINT64_C(0x12983d5271651389), "graphics.Unit"}, - {UINT64_C(0x304088bbc28f0015), UINT64_C(0x97e6e809913be6b6), "graphics.new_canvas"}, - {UINT64_C(0x49f8837532df9ac6), UINT64_C(0x32e5b27e81ee0957), "graphics.Canvas"}, - {UINT64_C(0x55717fa027336b47), UINT64_C(0xb2bd3a6bdcce48c8), "graphics.shapes.describe"}, - {UINT64_C(0x7626b91b8f1f53cd), UINT64_C(0x867bbf7cf0f41c74), "graphics.shapes.geometry.compute"}, - {UINT64_C(0xb3efcb50d9a61ab7), UINT64_C(0x3cb0207dfad2a149), "graphics.GraphicsError"}, - {UINT64_C(0xc62b16a9591b744b), UINT64_C(0x7895610c8487eb1b), "graphics.shapes.Shape"}, - {UINT64_C(0xe13001f4983ccd35), UINT64_C(0x16e7de070b4a4cfc), "graphics.shapes.geometry.Detail"}, - {UINT64_C(0xf3f658bcfc5dba62), UINT64_C(0x2665ccb47356e746), "graphics.shapes.make_shape"}, -}; - -static jint Jni_load(JNIEnv* env) { - (void)env; - if (Jni_check_contract(env, nested_modules_graphics_contract, Jni_contract_graphics, sizeof Jni_contract_graphics / sizeof Jni_contract_graphics[0]) != JNI_OK) { - return JNI_ERR; - } - return JNI_OK; -} - -JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1new_1canvas(JNIEnv* env, jclass cls, jint p_width) { - nested_modules_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = nested_modules_graphics_new_canvas((int32_t)p_width, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1shapes_1make_1shape(JNIEnv* env, jclass cls, jint p_sides) { - nested_modules_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = nested_modules_graphics_shapes_make_shape((int32_t)p_sides, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1shapes_1describe(JNIEnv* env, jclass cls) { - nested_modules_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = nested_modules_graphics_shapes_describe(&out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1shapes_1geometry_1compute(JNIEnv* env, jclass cls, jbyteArray p_shape) { - nested_modules_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_shape_b = Jni_borrow_bytes(env, p_shape); - size_t out_len = 0; - const uint8_t* rv = nested_modules_graphics_shapes_geometry_compute(p_shape_b.ptr, p_shape_b.len, &out_len, &err); - Jni_release_bytes(env, &p_shape_b); - if (err.code != 0) { - Jni_throw(env, &err, 1); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - - -// END nested_modules_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Graphics_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Graphics_kt.snap deleted file mode 100644 index 61593497..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Graphics_kt.snap +++ /dev/null @@ -1,102 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package nested_modules - -/** Errors of the `GraphicsError` domain. */ -sealed class GraphicsException(code: Int, message: String) : FfiException(code, message) { - class OutOfBounds(message: String = "coordinate out of bounds") : GraphicsException(1, message) - - internal companion object { - /** The exception for a raw code and its serialized payload. */ - fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { - 1 -> OutOfBounds(message) - else -> FfiException(code, message) - } - } -} - -/** A measurement unit shared with nested submodules. */ -enum class Unit_(val value: Int) { - pixels(0), - points(1); - - companion object { - /** The variant whose ABI value is [value]. */ - fun fromValue(value: Int): Unit_ = entries.firstOrNull { it.value == value } - ?: throw NativeBugException(-3, "unknown Unit_ value $value") - } -} - -/** Top-level struct that references a struct defined one level deeper. */ -data class Canvas( - val width: Int, - /** Reference down into a nested module */ - val root: Shape, -) - -internal fun packCanvas(_w: BufferWriter, _v: Canvas) { - _w.writeI32(_v.width) - packShape(_w, _v.root) -} - -internal fun unpackCanvas(_r: BufferReader): Canvas = Canvas( - _r.readI32(), - unpackShape(_r), -) - -/** A struct one level deep that references both an ancestor enum and a deeper struct. */ -data class Shape( - val sides: Int, - /** Reference up to the parent module's enum */ - val unit: Unit_, - /** Reference down into a deeper module */ - val detail: Detail, -) - -internal fun packShape(_w: BufferWriter, _v: Shape) { - _w.writeI32(_v.sides) - _w.writeI32(_v.unit.value) - packDetail(_w, _v.detail) -} - -internal fun unpackShape(_r: BufferReader): Shape = Shape( - _r.readI32(), - Unit_.fromValue(_r.readI32()), - unpackDetail(_r), -) - -/** A struct two levels deep. */ -data class Detail(val area: Double) - -internal fun packDetail(_w: BufferWriter, _v: Detail) { - _w.writeF64(_v.area) -} - -internal fun unpackDetail(_r: BufferReader): Detail = Detail( - _r.readF64(), -) - -object Graphics { - /** Construct a Canvas. */ - fun newCanvas(width: Int): Canvas = decodeBuffer(JniBridge.graphics_new_canvas(width)) { _r -> unpackCanvas(_r) } - - object Shapes { - /** Construct a Shape. */ - fun makeShape(sides: Int): Shape = decodeBuffer(JniBridge.graphics_shapes_make_shape(sides)) { _r -> unpackShape(_r) } - - /** Nested function returning a struct owned by an ancestor module. */ - fun describe(): Canvas = decodeBuffer(JniBridge.graphics_shapes_describe()) { _r -> unpackCanvas(_r) } - - object Geometry { - /** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ - fun compute(shape: Shape): Detail = decodeBuffer(JniBridge.graphics_shapes_geometry_compute(encodeBuffer { _w -> packShape(_w, shape) })) { _r -> unpackDetail(_r) } - } - } -} - -// END Graphics.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_JniBridge_kt.snap deleted file mode 100644 index 28f93ae8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_JniBridge_kt.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package nested_modules - -/** - * The JNI entry points of `libnested_modules_jni`: one native per C symbol, named after - * it, plus the hooks the shim calls back into. Internal plumbing for the - * public wrappers. - */ -internal object JniBridge { - init { - NativeLibrary.ensureLoaded() - } - - /** - * The exception for a native failure: through error domain [domain] for a - * throwing call, or a [NativeBugException] (domain 0) for one that can't fail. - */ - @JvmStatic - fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): Throwable { - val text = decodeUtf8(message) - return when (domain) { - 1 -> GraphicsException.fromCode(code, text, payload) - else -> NativeBugException(code, text) - } - } - - @JvmStatic external fun debug_live(kind: Int): Long - @JvmStatic external fun graphics_new_canvas(width: Int): ByteArray - @JvmStatic external fun graphics_shapes_make_shape(sides: Int): ByteArray - @JvmStatic external fun graphics_shapes_describe(): ByteArray - @JvmStatic external fun graphics_shapes_geometry_compute(shape: ByteArray): ByteArray -} - -// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_jni_c.snap deleted file mode 100644 index 2e5e44ac..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_jni_c.snap +++ /dev/null @@ -1,255 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -#include -#include -#include -#include -#include -#include -#include - -/* The shim forwards deprecated functions too, so it doesn't warn about them. */ -#define SHAPES_DEPRECATED(msg) -#include "shapes.h" - -/* Every static name in this file starts with `Jni_`, which no C ABI symbol - (all lowercase `shapes_...`) and no JNI export (`Java_...`) can - spell. */ - -static JavaVM* Jni_vm = NULL; -static jclass Jni_bridge = NULL; -static jmethodID Jni_error = NULL; - -/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value - buffers all cross as one. */ -typedef struct { - jbyteArray array; - jbyte* elems; - const uint8_t* ptr; - size_t len; -} Jni_bytes; - -static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { - Jni_bytes b = {array, NULL, NULL, 0}; - if (array == NULL) { - return b; - } - b.len = (size_t)(*env)->GetArrayLength(env, array); - b.elems = (*env)->GetByteArrayElements(env, array, NULL); - b.ptr = (const uint8_t*)b.elems; - if (b.elems == NULL) { - b.len = 0; - } - return b; -} - -static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { - if (b->elems != NULL) { - (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); - } -} - -/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid - input, so NULL + 0 is the empty array). */ -static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = (*env)->NewByteArray(env, (jsize)len); - if (out != NULL && len > 0 && ptr != NULL) { - (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); - } - return out; -} - -/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ -static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { - jbyteArray out = Jni_new_bytes(env, ptr, len); - if (ptr != NULL) { - shapes_free_bytes((uint8_t*)ptr, len); - } - return out; -} - -/* Throws the Kotlin exception for `err` (mapped through error domain - `domain`, or 0 for a call that can't fail, which raises the trap) and - clears `err`. */ -static inline void Jni_throw(JNIEnv* env, shapes_error* err, jint domain) { - const char* message = err->message != NULL ? err->message : ""; - jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); - jbyteArray payload = NULL; - if (err->payload_ptr != NULL) { - payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); - } - jint code = (jint)err->code; - shapes_error_clear(err); - if ((*env)->ExceptionCheck(env)) { - return; - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { - (*env)->Throw(env, ex); - } -} - -static jint Jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { - (*env)->ThrowNew(env, cls, message); - } - return JNI_ERR; -} - -/* One contract entry these bindings were generated with: a declaration's - id and signature hash, plus its dotted path for the load error. */ -typedef struct { - uint64_t id; - uint64_t hash; - const char* path; -} Jni_contract_entry; - -/* Checks that the library's contract table (from `contract`, sorted by id) - has every expected entry with an equal hash; entries only the library - has are fine. Fails loading with an error naming the first declaration - that's missing or changed. */ -static inline jint Jni_check_contract(JNIEnv* env, const shapes_contract_entry* (*contract)(size_t*), const Jni_contract_entry* expected, size_t count) { - size_t len = 0; - const shapes_contract_entry* table = contract(&len); - if (table == NULL) { - len = 0; - } - for (size_t i = 0; i < count; i++) { - size_t lo = 0; - size_t hi = len; - while (lo < hi) { - size_t mid = lo + (hi - lo) / 2; - if (table[mid].id < expected[i].id) { - lo = mid + 1; - } else { - hi = mid; - } - } - const char* problem = NULL; - if (lo == len || table[lo].id != expected[i].id) { - problem = "is missing from the library"; - } else if (table[lo].hash != expected[i].hash) { - problem = "changed since these bindings were generated"; - } - if (problem != NULL) { - char message[512]; - snprintf(message, sizeof message, "%s: %s %s", "shapes", expected[i].path, problem); - return Jni_load_error(env, message); - } - } - return JNI_OK; -} - -/* Generated below: the contract checks and the method IDs the shim caches. */ -static jint Jni_load(JNIEnv* env); - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env = NULL; - (void)reserved; - Jni_vm = vm; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - if (shapes_abi_version() != 4u) { - return Jni_load_error(env, "shapes: the native library implements a different C ABI revision than these bindings (expected 4)"); - } - jclass bridge = (*env)->FindClass(env, "shapes/JniBridge"); - if (bridge == NULL) { - return JNI_ERR; - } - Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); - Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ljava/lang/Throwable;"); - if (Jni_error == NULL) { - return JNI_ERR; - } - if (Jni_load(env) != JNI_OK) { - return JNI_ERR; - } - return JNI_VERSION_1_6; -} - -JNIEXPORT jlong JNICALL Java_shapes_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { - (void)env; - (void)cls; - return (jlong)shapes_debug_live((int32_t)kind); -} - -static const Jni_contract_entry Jni_contract_shapes[] = { - {UINT64_C(0x0d7b3eec49243e7a), UINT64_C(0x5f232311808e1b1f), "shapes.Channel"}, - {UINT64_C(0x183fa8198100860e), UINT64_C(0xe40c6993cb31d359), "shapes.describe"}, - {UINT64_C(0x2d1395eb837b848e), UINT64_C(0x42202a0c12f3a16e), "shapes.sum_bytes"}, - {UINT64_C(0x7f9a5b87ba09b86b), UINT64_C(0xc5b32fb0efbbaa17), "shapes.scale"}, - {UINT64_C(0xc8edd779240fda17), UINT64_C(0x68e1be78fddb20e4), "shapes.Numerics"}, - {UINT64_C(0xd3d3881b2fd18a62), UINT64_C(0xf24d0eb76a99d918), "shapes.make_numerics"}, - {UINT64_C(0xf010844afff5ac3c), UINT64_C(0x50de28ae9b41ff15), "shapes.Shape"}, -}; - -static jint Jni_load(JNIEnv* env) { - (void)env; - if (Jni_check_contract(env, shapes_shapes_contract, Jni_contract_shapes, sizeof Jni_contract_shapes / sizeof Jni_contract_shapes[0]) != JNI_OK) { - return JNI_ERR; - } - return JNI_OK; -} - -JNIEXPORT jbyteArray JNICALL Java_shapes_JniBridge_shapes_1describe(JNIEnv* env, jclass cls, jbyteArray p_shape) { - shapes_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_shape_b = Jni_borrow_bytes(env, p_shape); - size_t out_len = 0; - const uint8_t* rv = shapes_shapes_describe(p_shape_b.ptr, p_shape_b.len, &out_len, &err); - Jni_release_bytes(env, &p_shape_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jbyteArray JNICALL Java_shapes_JniBridge_shapes_1scale(JNIEnv* env, jclass cls, jbyteArray p_shape, jdouble p_factor) { - shapes_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_shape_b = Jni_borrow_bytes(env, p_shape); - size_t out_len = 0; - const uint8_t* rv = shapes_shapes_scale(p_shape_b.ptr, p_shape_b.len, (double)p_factor, &out_len, &err); - Jni_release_bytes(env, &p_shape_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - -JNIEXPORT jlong JNICALL Java_shapes_JniBridge_shapes_1sum_1bytes(JNIEnv* env, jclass cls, jbyteArray p_values) { - shapes_error err = {0, NULL, NULL, 0}; - (void)cls; - Jni_bytes p_values_b = Jni_borrow_bytes(env, p_values); - uint64_t rv = shapes_shapes_sum_bytes(p_values_b.ptr, p_values_b.len, &err); - Jni_release_bytes(env, &p_values_b); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return 0; - } - return (jlong)rv; -} - -JNIEXPORT jbyteArray JNICALL Java_shapes_JniBridge_shapes_1make_1numerics(JNIEnv* env, jclass cls, jbyte p_a, jshort p_b, jint p_c, jlong p_d, jbyte p_e, jshort p_f, jint p_g, jlong p_h, jfloat p_i, jdouble p_j) { - shapes_error err = {0, NULL, NULL, 0}; - (void)cls; - size_t out_len = 0; - const uint8_t* rv = shapes_shapes_make_numerics((int8_t)p_a, (int16_t)p_b, (int32_t)p_c, (int64_t)p_d, (uint8_t)p_e, (uint16_t)p_f, (uint32_t)p_g, (uint64_t)p_h, (float)p_i, (double)p_j, &out_len, &err); - if (err.code != 0) { - Jni_throw(env, &err, 0); - return NULL; - } - return Jni_take_bytes(env, rv, out_len); -} - - -// END shapes_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_JniBridge_kt.snap deleted file mode 100644 index 29806791..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_JniBridge_kt.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package shapes - -/** - * The JNI entry points of `libshapes_jni`: one native per C symbol, named after - * it, plus the hooks the shim calls back into. Internal plumbing for the - * public wrappers. - */ -internal object JniBridge { - init { - NativeLibrary.ensureLoaded() - } - - /** - * The exception for a native failure: through error domain [domain] for a - * throwing call, or a [NativeBugException] (domain 0) for one that can't fail. - */ - @JvmStatic - fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): Throwable { - val text = decodeUtf8(message) - return when (domain) { - else -> NativeBugException(code, text) - } - } - - @JvmStatic external fun debug_live(kind: Int): Long - @JvmStatic external fun shapes_describe(shape: ByteArray): ByteArray - @JvmStatic external fun shapes_scale(shape: ByteArray, factor: Double): ByteArray - @JvmStatic external fun shapes_sum_bytes(values: ByteArray): Long - @JvmStatic external fun shapes_make_numerics(a: Byte, b: Short, c: Int, d: Long, e: Byte, f: Short, g: Int, h: Long, i: Float, j: Double): ByteArray -} - -// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Shapes_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Shapes_kt.snap deleted file mode 100644 index 5ab27c24..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Shapes_kt.snap +++ /dev/null @@ -1,119 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -package shapes - -/** An algebraic shape (sum type with associated data) */ -sealed class Shape { - /** The empty shape (a unit variant) */ - object Empty : Shape() - /** A circle with a radius */ - data class Circle( - /** Radius in points */ - val radius: Double, - ) : Shape() - /** An axis-aligned rectangle */ - data class Rectangle(val width: Float, val height: Float) : Shape() - /** An integer-bounded shape (exercises narrow + wide ints) */ - data class Bounded(val min: Short, val max: Short, val weight: ULong) : Shape() - /** A labeled shape with a small count */ - data class Labeled(val label: String, val count: UByte) : Shape() -} - -internal fun packShape(_w: BufferWriter, _v: Shape) { - when (_v) { - is Shape.Empty -> _w.writeI32(0) - is Shape.Circle -> { - _w.writeI32(1) - _w.writeF64(_v.radius) - } - is Shape.Rectangle -> { - _w.writeI32(2) - _w.writeF32(_v.width) - _w.writeF32(_v.height) - } - is Shape.Bounded -> { - _w.writeI32(3) - _w.writeI16(_v.min) - _w.writeI16(_v.max) - _w.writeU64(_v.weight) - } - is Shape.Labeled -> { - _w.writeI32(4) - _w.writeString(_v.label) - _w.writeU8(_v.count) - } - } -} - -internal fun unpackShape(_r: BufferReader): Shape = when (val _tag = _r.readI32()) { - 0 -> Shape.Empty - 1 -> Shape.Circle(_r.readF64()) - 2 -> Shape.Rectangle(_r.readF32(), _r.readF32()) - 3 -> Shape.Bounded(_r.readI16(), _r.readI16(), _r.readU64()) - 4 -> Shape.Labeled(_r.readString(), _r.readU8()) - else -> throw NativeBugException(-3, "malformed value buffer: unknown Shape tag $_tag") -} - -/** A plain C-style enum (no payloads) */ -enum class Channel(val value: Int) { - Red(0), - Green(1), - Blue(2); - - companion object { - /** The variant whose ABI value is [value]. */ - fun fromValue(value: Int): Channel = entries.firstOrNull { it.value == value } - ?: throw NativeBugException(-3, "unknown Channel value $value") - } -} - -/** One field per supported numeric primitive */ -data class Numerics(val a: Byte, val b: Short, val c: Int, val d: Long, val e: UByte, val f: UShort, val g: UInt, val h: ULong, val i: Float, val j: Double) - -internal fun packNumerics(_w: BufferWriter, _v: Numerics) { - _w.writeI8(_v.a) - _w.writeI16(_v.b) - _w.writeI32(_v.c) - _w.writeI64(_v.d) - _w.writeU8(_v.e) - _w.writeU16(_v.f) - _w.writeU32(_v.g) - _w.writeU64(_v.h) - _w.writeF32(_v.i) - _w.writeF64(_v.j) -} - -internal fun unpackNumerics(_r: BufferReader): Numerics = Numerics( - _r.readI8(), - _r.readI16(), - _r.readI32(), - _r.readI64(), - _r.readU8(), - _r.readU16(), - _r.readU32(), - _r.readU64(), - _r.readF32(), - _r.readF64(), -) - -/** Rich (algebraic) enums and the expanded numeric primitives. */ -object Shapes { - /** Render a shape to a string */ - fun describe(shape: Shape): String = decodeUtf8(JniBridge.shapes_describe(encodeBuffer { _w -> packShape(_w, shape) })) - - /** Scale a shape by a factor, returning a new shape */ - fun scale(shape: Shape, factor: Double): Shape = decodeBuffer(JniBridge.shapes_scale(encodeBuffer { _w -> packShape(_w, shape) }, factor)) { _r -> unpackShape(_r) } - - /** Sum a list of bytes into a wide integer (numerics smoke) */ - fun sumBytes(values: ByteArray): ULong = JniBridge.shapes_sum_bytes(values).toULong() - - /** Build a Numerics struct from every numeric primitive */ - fun makeNumerics(a: Byte, b: Short, c: Int, d: Long, e: UByte, f: UShort, g: UInt, h: ULong, i: Float, j: Double): Numerics = decodeBuffer(JniBridge.shapes_make_numerics(a, b, c, d, e.toByte(), f.toShort(), g.toInt(), h.toLong(), i, j)) { _r -> unpackNumerics(_r) } -} - -// END Shapes.kt diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_node_c.snap deleted file mode 100644 index 0c9ef81e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_node_c.snap +++ /dev/null @@ -1,1174 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/* libuv's Unix headers use POSIX types (pthread_rwlock_t, struct addrinfo) - * that glibc hides under a strict -std=c11; ask for them explicitly. */ -#if defined(__linux__) && !defined(_GNU_SOURCE) -#define _GNU_SOURCE -#endif -#ifndef NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include -#include -#include -#include -#include -#include - -/* The addon wraps deprecated functions too; the JavaScript API carries the - * deprecation instead. */ -#define DOCS_EVERYWHERE_DEPRECATED(msg) -#include "docs_everywhere.h" - -/* - * The N-API transport of the JavaScript bindings. Every C symbol of the - * library is exported to `index.js` under its own name and called with the - * raw convention the shared JavaScript layer documents: direct values as - * numbers, bigints, and booleans; strings as strings; bytes and value - * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint - * handles (null when absent); callback interfaces as adapter objects (null - * for an absent optional one). A failure throws an instance of the - * runtime's `$Fault` class. - */ - -typedef docs_everywhere_error js_error; - -/* Helpers an API may not need. */ -#if defined(__GNUC__) || defined(__clang__) -#define JS_HELPER static inline __attribute__((unused)) -#define JS_TLS __thread -#else -#define JS_HELPER static inline -#define JS_TLS __declspec(thread) -#endif - -/* --------------------------------------------------------------------------- - * JavaScript threads. One `js_thread` per thread that runs JavaScript and - * loaded the addon (the main thread and each worker). It counts the - * synchronous calls into the library in progress on that thread, which a - * callback made on another thread consults while it waits for the - * JavaScript thread (see js_cb_hop). Each environment and each callback - * registration holds a reference. - * ------------------------------------------------------------------------- */ - -typedef struct { - uv_mutex_t mu; - uv_cond_t cv; /* broadcast when a call begins or ends, or a hop finishes */ - unsigned depth; /* synchronous calls into the library in progress */ - uint64_t calls; /* outermost synchronous calls begun so far */ - unsigned waiters; /* producer threads waiting in js_cb_hop */ - unsigned refs; -} js_thread; - -/* The state of the JavaScript thread this is, or NULL on any other thread. */ -static JS_TLS js_thread* js_current = NULL; - -static js_thread* js_thread_new(void) { - js_thread* t = (js_thread*)calloc(1, sizeof *t); - uv_mutex_init(&t->mu); - uv_cond_init(&t->cv); - t->refs = 1; - return t; -} - -static void js_thread_retain(js_thread* t) { - uv_mutex_lock(&t->mu); - t->refs++; - uv_mutex_unlock(&t->mu); -} - -static void js_thread_release(js_thread* t) { - uv_mutex_lock(&t->mu); - bool last = --t->refs == 0; - uv_mutex_unlock(&t->mu); - if (!last) return; - uv_cond_destroy(&t->cv); - uv_mutex_destroy(&t->mu); - free(t); -} - -/* Bracket every synchronous call into the library. */ -JS_HELPER void js_sync_begin(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - if (t->depth++ == 0) t->calls++; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -JS_HELPER void js_sync_end(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - t->depth--; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* Per-environment state (one per main thread or worker): the `$Fault` - * constructor `index.js` registers through `$setup`, and the thread. */ -typedef struct { - napi_ref fault; - js_thread* thread; -} js_env; - -static void js_env_free(napi_env env, void* data, void* hint) { - (void)hint; - js_env* e = (js_env*)data; - if (e->fault != NULL) napi_delete_reference(env, e->fault); - if (js_current == e->thread) js_current = NULL; - js_thread_release(e->thread); - free(e); -} - -/* Set up the environment of the thread loading the addon. */ -static void js_env_init(napi_env env) { - js_env* e = (js_env*)calloc(1, sizeof *e); - e->thread = js_thread_new(); - js_current = e->thread; - napi_set_instance_data(env, e, js_env_free, NULL); -} - -static js_env* js_env_of(napi_env env) { - js_env* e = NULL; - napi_get_instance_data(env, (void**)&e); - return e; -} - -JS_HELPER napi_value js_undefined(napi_env env) { - napi_value v; - napi_get_undefined(env, &v); - return v; -} - -static napi_value js_setup(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value argv[1]; - napi_get_cb_info(env, info, &argc, argv, NULL, NULL); - js_env* e = js_env_of(env); - if (e->fault != NULL) napi_delete_reference(env, e->fault); - napi_create_reference(env, argv[0], 1, &e->fault); - return js_undefined(env); -} - -/* The `$Fault` constructor, or NULL before `$setup`. */ -static napi_value js_fault_class(napi_env env) { - js_env* e = js_env_of(env); - napi_value ctor = NULL; - if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok) { - return NULL; - } - return ctor; -} - -/* --------------------------------------------------------------------------- - * Value conversions - * ------------------------------------------------------------------------- */ - -JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len, &data, &ab); - if (len > 0) memcpy(data, ptr, len); - napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out; - napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); - return out; -} - -JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { - napi_value out; - napi_create_int32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { - napi_value out; - napi_create_uint32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { - napi_value out; - napi_create_bigint_int64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { - napi_value out; - napi_create_bigint_uint64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_f64(napi_env env, double v) { - napi_value out; - napi_create_double(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_bool(napi_env env, bool v) { - napi_value out; - napi_get_boolean(env, v, &out); - return out; -} - -/* An object, iterator, or cancel token handle: the pointer as a bigint, or - * null for NULL. */ -JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { - napi_value out; - if (p == NULL) { - napi_get_null(env, &out); - } else { - napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); - } - return out; -} - -/* Returned strings and buffers: convert, then release the producer's copy. */ -JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_str(env, ptr, len); - if (ptr != NULL) docs_everywhere_free_bytes((uint8_t*)ptr, len); - return out; -} - -JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_bytes(env, ptr, len); - if (ptr != NULL) docs_everywhere_free_bytes((uint8_t*)ptr, len); - return out; -} - -/* A module's contract table as a BigUint64Array of `id, hash` pairs. */ -JS_HELPER napi_value js_new_contract(napi_env env, const docs_everywhere_contract_entry* table, - size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len * 2 * sizeof(uint64_t), &data, &ab); - uint64_t* words = (uint64_t*)data; - for (size_t i = 0; i < len; i++) { - words[2 * i] = table[i].id; - words[2 * i + 1] = table[i].hash; - } - napi_create_typedarray(env, napi_biguint64_array, len * 2, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, - const uint8_t* payload, size_t payload_len) { - napi_value argv[3], ctor, out = NULL; - napi_create_int32(env, code, &argv[0]); - napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); - if (payload != NULL) { - argv[2] = js_new_bytes(env, payload, payload_len); - } else { - napi_get_null(env, &argv[2]); - } - ctor = js_fault_class(env); - if (ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { - napi_create_error(env, NULL, argv[1], &out); - napi_set_named_property(env, out, "code", argv[0]); - } - return out; -} - -/* Throw the error a call reported, then release it. */ -JS_HELPER napi_value js_throw(napi_env env, js_error* err) { - napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); - docs_everywhere_error_clear(err); - return NULL; -} - -JS_HELPER bool js_type_error(napi_env env, const char* expected) { - bool pending = false; - napi_is_exception_pending(env, &pending); - if (!pending) napi_throw_type_error(env, NULL, expected); - return false; -} - -JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { - return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { - return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int8_t)t; - return true; -} - -JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int16_t)t; - return true; -} - -JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint8_t)t; - return true; -} - -JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint16_t)t; - return true; -} - -/* 64-bit integers: a bigint that fits, or an integral number. */ -JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_int64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { - *out = (int64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_uint64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && - d == (double)(uint64_t)d) { - *out = (uint64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { - return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { - double d = 0; - if (!js_arg_f64(env, v, &d)) return false; - *out = (float)d; - return true; -} - -JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { - return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); -} - -/* A handle argument; null or undefined is NULL when `nullable`. */ -JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return nullable || js_type_error(env, "expected an object handle"); - } - uint64_t raw = 0; - bool lossless = false; - if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || - raw == 0) { - return js_type_error(env, "expected an object handle"); - } - *out = (void*)(uintptr_t)raw; - return true; -} - -/* A borrowed byte run: a Uint8Array (or Buffer). */ -JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { - bool is_typed = false; - napi_is_typedarray(env, v, &is_typed); - if (is_typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); - if (type == napi_uint8_array || type == napi_uint8_clamped_array) { - *ptr = (const uint8_t*)data; - return true; - } - } - return js_type_error(env, "expected a Uint8Array"); -} - -/* A string argument encoded as UTF-8: short strings live in the inline - * buffer, longer ones on the heap until js_str_free. */ -typedef struct { - char* ptr; - size_t len; - char inline_buf[256]; -} js_str; - -#define JS_STR_INIT {NULL, 0, {0}} - -JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { - size_t len = 0; - if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { - return js_type_error(env, "expected a string"); - } - s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); - napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); - return true; -} - -JS_HELPER void js_str_free(js_str* s) { - if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); - s->ptr = NULL; -} - -#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) - -/* Read up to `n` arguments; missing ones are undefined. */ -#define JS_ARGS(n) \ - size_t argc = (n); \ - napi_value argv[(n) + 1]; \ - napi_get_cb_info(env, info, &argc, argv, NULL, NULL) - -/* --------------------------------------------------------------------------- - * Async calls: the completion may run on any thread, so it only records the - * result and queues the settlement on the JavaScript thread through a - * thread-safe function (which also keeps the event loop alive meanwhile). - * ------------------------------------------------------------------------- */ - -typedef enum { - JS_R_VOID, - JS_R_I32, - JS_R_U32, - JS_R_I64, - JS_R_U64, - JS_R_F64, - JS_R_BOOL, - JS_R_STR, - JS_R_BYTES, - JS_R_HANDLE -} js_kind; - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - js_kind kind; - js_error* err; - union { - int64_t i; - uint64_t u; - double f; - bool b; - const void* p; - } v; - size_t len; -} js_async; - -JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { - (void)cb; - (void)context; - js_async* a = (js_async*)data; - bool failed = a->err != NULL && a->err->code != 0; - if (env != NULL && failed) { - napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, - a->err->payload_ptr, a->err->payload_len)); - } else if (env != NULL) { - napi_value v = NULL; - switch (a->kind) { - case JS_R_VOID: v = js_undefined(env); break; - case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; - case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; - case JS_R_I64: v = js_new_i64(env, a->v.i); break; - case JS_R_U64: v = js_new_u64(env, a->v.u); break; - case JS_R_F64: v = js_new_f64(env, a->v.f); break; - case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; - case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; - } - napi_resolve_deferred(env, a->deferred, v); - } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { - /* The environment is shutting down: just release the result. */ - docs_everywhere_free_bytes((uint8_t*)a->v.p, a->len); - } - docs_everywhere_error_free(a->err); - napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); - free(a); -} - -/* Create the promise and its settlement queue for one async call. */ -JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { - js_async* a = (js_async*)calloc(1, sizeof *a); - a->kind = kind; - napi_create_promise(env, &a->deferred, promise); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, - js_async_settle, &a->tsfn); - return a; -} - -/* Called by each completion once it has recorded its result. */ -JS_HELPER void js_async_done(js_async* a, js_error* err) { - a->err = err; - napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); -} - -/* --------------------------------------------------------------------------- - * Callback interfaces. One registration per implementation passed to the - * producer: a reference to the adapter object, the JavaScript thread it - * belongs to, and a thread-safe function that runs calls made from any other - * thread on that thread while the caller waits. - * - * Waiting is what makes a call from another thread safe, and also what could - * deadlock it: if the JavaScript thread is itself inside a synchronous call - * into the library that waits for the calling thread, neither can proceed. - * A waiting call therefore watches the JavaScript thread. While that thread - * stays inside one synchronous call for JS_CB_DEADLOCK_MS without having - * started the callback, the wait is abandoned and the callback fails with - * code -4 instead of hanging (a shorter synchronous call, or one that - * returns to the event loop, just delays the callback). - * ------------------------------------------------------------------------- */ - -#define JS_CB_DEADLOCK_MS 1000 - -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; - uv_thread_t js_thread; - js_thread* thread; -} js_cb; - -typedef enum { JS_REQ_QUEUED, JS_REQ_RUNNING, JS_REQ_DONE, JS_REQ_ABANDONED } js_req_state; - -/* A call queued from another thread: `method` is the vtable index, or -1 to - * release the registration; `frame` holds the method's slots. The request is - * heap-allocated and holds its own reference to the thread state, so a - * caller that abandons it can return (and the producer can then release the - * registration) while it is still queued: the dispatcher frees it without - * touching the frame or the registration. */ -typedef struct { - js_cb* cb; - js_thread* thread; - int method; - void* frame; - js_error* out_err; - js_req_state state; -} js_cb_req; - -JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &cb->js_thread) != 0; -} - -JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, - napi_threadsafe_function_call_js dispatch) { - js_cb* cb = (js_cb*)calloc(1, sizeof *cb); - cb->env = env; - cb->js_thread = uv_thread_self(); - cb->thread = js_env_of(env)->thread; - js_thread_retain(cb->thread); - napi_create_reference(env, adapter, 1, &cb->ref); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, - &cb->tsfn); - /* A live implementation must not keep the process alive by itself. */ - napi_unref_threadsafe_function(env, cb->tsfn); - return cb; -} - -JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { - if (env != NULL) napi_delete_reference(env, cb->ref); - napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); - js_thread_release(cb->thread); - free(cb); -} - -/* A callback-interface argument: the adapter object `index.js` built, or - * null for an absent optional one. */ -JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, - napi_threadsafe_function_call_js dispatch, bool nullable, js_cb** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (nullable && (t == napi_null || t == napi_undefined)) { - *out = NULL; - return true; - } - if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); - *out = js_cb_register(env, v, name, dispatch); - return true; -} - -/* The vtable's `free`: the producer is done with the implementation. The - * reference can only be deleted on its own thread. */ -JS_HELPER void js_cb_free(void* ctx) { - js_cb* cb = (js_cb*)ctx; - if (cb == NULL) return; - if (js_cb_on_js_thread(cb)) { - js_cb_release(cb->env, cb); - return; - } - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->method = -1; - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); -} - -/* Run method `method` of `cb` on its JavaScript thread and wait for it, - * failing it with code -4 rather than deadlocking (see above). `what` names - * the method in that failure. */ -JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, - const char* what) { - js_thread* t = cb->thread; - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->thread = t; - js_thread_retain(t); - req->method = method; - req->frame = frame; - req->out_err = out_err; - req->state = JS_REQ_QUEUED; - uv_mutex_lock(&t->mu); - t->waiters++; - uv_mutex_unlock(&t->mu); - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - uv_mutex_lock(&t->mu); - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); - docs_everywhere_error_set(out_err, -4, "the callback implementation is no longer reachable"); - return; - } - const uint64_t limit = (uint64_t)JS_CB_DEADLOCK_MS * 1000000u; - bool blocked = false; - uint64_t call = 0, since = 0; - uv_mutex_lock(&t->mu); - while (req->state != JS_REQ_DONE) { - if (req->state == JS_REQ_QUEUED && t->depth > 0) { - uint64_t now = uv_hrtime(); - if (!blocked || call != t->calls) { - blocked = true; - call = t->calls; - since = now; - } - if (now - since >= limit) { - req->state = JS_REQ_ABANDONED; /* the dispatcher frees it */ - t->waiters--; - uv_mutex_unlock(&t->mu); - char msg[512]; - snprintf(msg, sizeof msg, - "%s was called on another thread while the JavaScript thread was blocked in a " - "synchronous call into the library for %d ms; the call likely waits for this " - "callback, so waiting longer would deadlock (call it asynchronously, or call " - "back on the calling thread)", - what, JS_CB_DEADLOCK_MS); - docs_everywhere_error_set(out_err, -4, msg); - return; - } - uv_cond_timedwait(&t->cv, &t->mu, limit - (now - since)); - } else { - blocked = false; - uv_cond_wait(&t->cv, &t->mu); - } - } - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); -} - -/* The dispatcher's side of a hop. js_cb_take claims a queued request (false - * when its caller gave up, in which case the request is freed); - * js_cb_finish reports it done. */ -JS_HELPER bool js_cb_take(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - bool abandoned = req->state == JS_REQ_ABANDONED; - if (!abandoned) req->state = JS_REQ_RUNNING; - uv_mutex_unlock(&t->mu); - if (abandoned) { - free(req); - js_thread_release(t); - } - return !abandoned; -} - -JS_HELPER void js_cb_finish(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - req->state = JS_REQ_DONE; - uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* A JavaScript value's text as a heap-allocated C string (NULL on failure). */ -JS_HELPER char* js_cstring(napi_env env, napi_value v) { - napi_value str; - size_t len = 0; - if (napi_coerce_to_string(env, v, &str) != napi_ok || - napi_get_value_string_utf8(env, str, NULL, 0, &len) != napi_ok) { - return NULL; - } - char* out = (char*)malloc(len + 1); - napi_get_value_string_utf8(env, str, out, len + 1, NULL); - return out; -} - -/* Report the pending JavaScript exception (or `fallback`) through - * `out_err`; nothing unwinds through the C frame. A `$Fault` (a domain error - * a throwing method raised) reports its code, message, and payload; any - * other exception is a foreign failure, code -4, with its message. */ -JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { - int32_t code = -4; - char* msg = NULL; - const uint8_t* payload = NULL; - size_t payload_len = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc, text; - napi_get_and_clear_last_exception(env, &exc); - napi_value fault = js_fault_class(env); - bool is_fault = false; - if (fault != NULL) napi_instanceof(env, exc, fault, &is_fault); - napi_valuetype t; - napi_typeof(env, exc, &t); - text = exc; - if (t == napi_object) napi_get_named_property(env, exc, "message", &text); - msg = js_cstring(env, text); - if (is_fault) { - napi_value v; - napi_get_named_property(env, exc, "code", &v); - napi_get_value_int32(env, v, &code); - napi_get_named_property(env, exc, "payload", &v); - bool typed = false; - napi_is_typedarray(env, v, &typed); - if (typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, &payload_len, &data, NULL, NULL); - payload = (const uint8_t*)data; - } - } - napi_is_exception_pending(env, &pending); - if (pending) napi_get_and_clear_last_exception(env, &exc); - } - docs_everywhere_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); - if (payload != NULL) docs_everywhere_error_set_payload(out_err, payload, payload_len); - free(msg); -} - -/* Look up and call `method` on the adapter; false when it threw. */ -JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, - const napi_value* argv, napi_value* result) { - napi_value adapter, fn; - napi_get_reference_value(env, cb->ref, &adapter); - napi_get_named_property(env, adapter, method, &fn); - return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; -} - -/* Hand `len` bytes to the producer through a callback's out slots, as a run - * from {p}_alloc that the producer adopts. */ -JS_HELPER void js_give(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = docs_everywhere_alloc(len); - if (len > 0) memcpy(run, data, len); - *out_ptr = run; - *out_len = len; -} - -/* A callback's string return: false (with a pending exception) when the - * value isn't a string. */ -JS_HELPER bool js_ret_str(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - js_str s = JS_STR_INIT; - if (!js_arg_str(env, v, &s)) return false; - js_give(s.ptr, s.len, out_ptr, out_len); - js_str_free(&s); - return true; -} - -/* A callback's bytes or buffer return: a Uint8Array. */ -JS_HELPER bool js_ret_bytes(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - const uint8_t* ptr = NULL; - size_t len = 0; - if (!js_arg_bytes(env, v, &ptr, &len)) return false; - js_give(ptr, len, out_ptr, out_len); - return true; -} - -/* --------------------------------------------------------------------------- - * The runtime symbols every library exports. - * ------------------------------------------------------------------------- */ - -static napi_value js_abi_version(napi_env env, napi_callback_info info) { - (void)info; - return js_new_u32(env, docs_everywhere_abi_version()); -} - -static napi_value js_debug_live(napi_env env, napi_callback_info info) { - JS_ARGS(1); - int32_t kind = 0; - if (!js_arg_i32(env, argv[0], &kind)) return NULL; - return js_new_u64(env, docs_everywhere_debug_live(kind)); -} - -static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { - (void)info; - return js_new_handle(env, docs_everywhere_cancel_token_create()); -} - -static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - docs_everywhere_cancel_token_cancel((docs_everywhere_cancel_token*)token); - return js_undefined(env); -} - -static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - docs_everywhere_cancel_token_destroy((docs_everywhere_cancel_token*)token); - return js_undefined(env); -} - -#define JS_RUNTIME_EXPORTS \ - {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ - {"docs_everywhere_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ - {"docs_everywhere_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ - {"docs_everywhere_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"docs_everywhere_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"docs_everywhere_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ - napi_default, NULL} - -static napi_value nx_docs_everywhere_docs_contract(napi_env env, napi_callback_info info) { - (void)info; - size_t len = 0; - const docs_everywhere_contract_entry* table = docs_everywhere_docs_contract(&len); - return js_new_contract(env, table, len); -} - -typedef struct { - js_error* out_err; - int64_t p0; - int32_t p1; -} frame_docs_everywhere_docs_SavedListener_on_saved; - -static void invoke_docs_everywhere_docs_SavedListener_on_saved(napi_env env, js_cb* cb, frame_docs_everywhere_docs_SavedListener_on_saved* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[3]; - argv[0] = js_new_i64(env, f->p0); - argv[1] = js_new_i32(env, (int32_t)f->p1); - napi_value result; - if (!js_cb_call(env, cb, "on_saved", 2, argv, &result)) { - js_cb_report(env, f->out_err, "SavedListener.onSaved failed"); - } - napi_close_handle_scope(env, scope); -} - -static void tramp_docs_everywhere_docs_SavedListener_on_saved(void* ctx, int64_t p0, int32_t p1, docs_everywhere_error* out_err) { - frame_docs_everywhere_docs_SavedListener_on_saved f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - f.p1 = p1; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_docs_everywhere_docs_SavedListener_on_saved(cb->env, cb, &f); - } else { - js_cb_hop(cb, 0, &f, out_err, "SavedListener.onSaved"); - } -} - -static void dispatch_docs_everywhere_docs_SavedListener(napi_env env, napi_value fn, void* context, void* data) { - (void)fn; - (void)context; - js_cb_req* req = (js_cb_req*)data; - if (req->method < 0) { - js_cb_release(env, req->cb); - free(req); - return; - } - if (!js_cb_take(req)) return; - if (env == NULL) { - docs_everywhere_error_set(req->out_err, -4, "the JavaScript environment is shutting down"); - } else { - switch (req->method) { - case 0: invoke_docs_everywhere_docs_SavedListener_on_saved(env, req->cb, (frame_docs_everywhere_docs_SavedListener_on_saved*)req->frame); break; - default: break; - } - } - js_cb_finish(req); -} - -static const docs_everywhere_docs_SavedListener_vtable vtable_docs_everywhere_docs_SavedListener = {sizeof(docs_everywhere_docs_SavedListener_vtable), 0, js_cb_free, tramp_docs_everywhere_docs_SavedListener_on_saved}; - -static napi_value nx_docs_everywhere_docs_Notebook_clone(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - docs_everywhere_docs_Notebook* r = docs_everywhere_docs_Notebook_clone((const docs_everywhere_docs_Notebook*)h); - js_sync_end(); - return js_new_handle(env, r); -} - -static napi_value nx_docs_everywhere_docs_Notebook_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - docs_everywhere_docs_Notebook_destroy((docs_everywhere_docs_Notebook*)h); - js_sync_end(); - return js_undefined(env); -} - -static napi_value nx_docs_everywhere_docs_create_document(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - js_str a0 = JS_STR_INIT; - const uint8_t* a1 = NULL; - size_t a1_len = 0; - if (!js_arg_str(env, argv[0], &a0)) goto done; - if (!js_arg_bytes(env, argv[1], &a1, &a1_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = docs_everywhere_docs_create_document(JS_STR_PTR(a0), a0.len, a1, a1_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - js_str_free(&a0); - return ret; -} - -static napi_value nx_docs_everywhere_docs_get_document(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - int64_t a0 = 0; - if (!js_arg_i64(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = docs_everywhere_docs_get_document(a0, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static void done_docs_everywhere_docs_publish_async(void* context, docs_everywhere_error* err, bool result) { - js_async* a = (js_async*)context; - a->v.b = result; - js_async_done(a, err); -} - -static napi_value nx_docs_everywhere_docs_publish_async(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - int64_t a0 = 0; - if (!js_arg_i64(env, argv[0], &a0)) goto done; - { - js_async* a = js_async_begin(env, JS_R_BOOL, "docs_everywhere_docs_publish_async", &ret); - js_sync_begin(); - docs_everywhere_docs_publish_async(a0, done_docs_everywhere_docs_publish_async, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static napi_value nx_docs_everywhere_docs_watch(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - js_cb* a0 = NULL; - if (!js_arg_cb(env, argv[0], "docs_everywhere_docs_SavedListener", dispatch_docs_everywhere_docs_SavedListener, false, &a0)) goto done; - { - js_error err = {0}; - js_sync_begin(); - docs_everywhere_docs_watch((void*)a0, &vtable_docs_everywhere_docs_SavedListener, &err); - js_sync_end(); - a0 = NULL; - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_undefined(env); - } - goto done; -done: - if (a0 != NULL) js_cb_release(env, a0); - return ret; -} - -static napi_value nx_docs_everywhere_docs_Notebook_new(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - docs_everywhere_docs_Notebook* r = docs_everywhere_docs_Notebook_new(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_docs_everywhere_docs_Notebook_insert(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - void* self_h = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - if (!js_arg_bytes(env, argv[1], &a0, &a0_len)) goto done; - { - js_error err = {0}; - js_sync_begin(); - int32_t r = docs_everywhere_docs_Notebook_insert((const docs_everywhere_docs_Notebook*)self_h, a0, a0_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_i32(env, (int32_t)r); - } - goto done; -done: - return ret; -} - -static napi_value nx_docs_everywhere_docs_Notebook_find(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - void* self_h = NULL; - int64_t a0 = 0; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - if (!js_arg_i64(env, argv[1], &a0)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = docs_everywhere_docs_Notebook_find((const docs_everywhere_docs_Notebook*)self_h, a0, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_docs_everywhere_docs_Notebook_size(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* self_h = NULL; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - { - js_error err = {0}; - js_sync_begin(); - int32_t r = docs_everywhere_docs_Notebook_size((const docs_everywhere_docs_Notebook*)self_h, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_i32(env, (int32_t)r); - } - goto done; -done: - return ret; -} - -static napi_value nx_docs_everywhere_docs_Notebook_max_documents(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - int32_t r = docs_everywhere_docs_Notebook_max_documents(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_i32(env, (int32_t)r); - } - goto done; -done: - return ret; -} - -NAPI_MODULE_INIT() { - js_env_init(env); - napi_property_descriptor props[] = { - JS_RUNTIME_EXPORTS, - {"docs_everywhere_docs_contract", NULL, nx_docs_everywhere_docs_contract, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_clone", NULL, nx_docs_everywhere_docs_Notebook_clone, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_destroy", NULL, nx_docs_everywhere_docs_Notebook_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_create_document", NULL, nx_docs_everywhere_docs_create_document, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_get_document", NULL, nx_docs_everywhere_docs_get_document, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_publish_async", NULL, nx_docs_everywhere_docs_publish_async, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_watch", NULL, nx_docs_everywhere_docs_watch, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_new", NULL, nx_docs_everywhere_docs_Notebook_new, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_insert", NULL, nx_docs_everywhere_docs_Notebook_insert, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_find", NULL, nx_docs_everywhere_docs_Notebook_find, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_size", NULL, nx_docs_everywhere_docs_Notebook_size, NULL, NULL, NULL, napi_default, NULL}, - {"docs_everywhere_docs_Notebook_max_documents", NULL, nx_docs_everywhere_docs_Notebook_max_documents, NULL, NULL, NULL, napi_default, NULL}, - }; - napi_define_properties(env, exports, sizeof props / sizeof props[0], props); - return exports; -} - -// END docs_everywhere_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_d_ts.snap deleted file mode 100644 index 9a6a8519..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_d_ts.snap +++ /dev/null @@ -1,149 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class DocsEverywhereError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends DocsEverywhereError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof DocsEverywhereError; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -/** Document storage: create, publish, and observe documents. */ -export declare namespace docs { - /** The errors of the `docs` error domain. */ - export class DocsErrorsError extends $Error { - constructor(code: number, message?: string); - } - /** Returned when no document matches the requested id */ - export class NotFoundError extends docs.DocsErrorsError { - static readonly CODE: 1; - constructor(message?: string); - } - /** Raised when attempting to move into a forbidden state */ - export class InvalidStatusError extends docs.DocsErrorsError { - static readonly CODE: 2; - constructor(message?: string); - } - /** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ - export enum Status { - /** Document is being edited */ - Draft = 0, - /** Document is publicly visible */ - Published = 1, - /** Document is hidden from listings */ - Archived = 2, - } - /** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ - export interface Document { - /** Stable opaque identifier */ - id: bigint; - /** Human-readable title shown in lists */ - title: string; - /** Current lifecycle status */ - status: docs.Status; - /** Free-form tags for grouping */ - tags: string[]; - } - /** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - */ - export interface SavedListener { - /** - * Fires after a document has been persisted successfully - * @param docId Identifier of the saved document - * @param revision Monotonically increasing revision - */ - onSaved(docId: bigint, revision: number): void; - } - /** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ - export class Notebook { - /** Create an empty notebook */ - constructor(); - /** - * Add a document, returning its position in the notebook - * @param document The document to add - */ - insert(document: docs.Document): number; - /** - * Look up a document by identifier, failing when none matches - * @param id Identifier of the document to find - * @throws {docs.DocsErrorsError} - */ - find(id: bigint): docs.Document; - /** The number of documents currently held */ - size(): number; - /** The largest number of documents one notebook will hold */ - static maxDocuments(): number; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; - } - /** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - */ - export function createDocument(title: string, tags: string[]): docs.Document; - /** - * Look up a document by identifier - * @param id Identifier returned from create_document - */ - export function getDocument(id: bigint): docs.Document | null; - /** - * Publish a document in the background - * @param id Identifier of the document to publish - */ - export function publishAsync(id: bigint): Promise; - /** - * Register a listener for saved notifications - * @param listener Receiver of saved notifications - */ - export function watch(listener: docs.SavedListener): void; -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap deleted file mode 100644 index 2fd23bcd..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap +++ /dev/null @@ -1,271 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - DocsEverywhereError as $Error, -} from './runtime.js'; -export { DocsEverywhereError, CancelledError } from './runtime.js'; - -import { createRequire } from 'node:module'; - -// The native entry points: the N-API addon built from docs_everywhere_node.c. A prebuilt -// copy comes first (from the `docs_everywhere--` package `npm install` -// picks for this platform, or under prebuilds/-/), then the one -// node-gyp compiles at install time when none matches. -function $loadAddon(name) { - const require = createRequire(import.meta.url); - const platform = `${process.platform}-${process.arch}`; - const candidates = [ - `docs_everywhere-${platform}/${name}`, - `./prebuilds/${platform}/${name}`, - `./build/Release/${name}`, - `./build/Debug/${name}`, - ]; - for (const candidate of candidates) { - try { - return require(candidate); - } catch (e) { - if (e.code !== 'MODULE_NOT_FOUND') throw e; - } - } - throw new Error( - `the native addon ${name} was not found (tried ${candidates.join(', ')}); ` + - 'install the package for this platform, or build the addon with `npm install` ' + - 'or `node-gyp rebuild` in this package', - ); -} - -const $raw = $loadAddon('docs_everywhere_node.node'); -$raw.$setup($Fault); - -// Object tokens in value buffers are already bigint handles here. -const $token = (t) => t; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['docs', [ - [0x00b2187fd75634ecn, 0x2263c6f787bf6081n, 'docs.publish_async'], - [0x12fc242b9dc6444cn, 0x96405f484a642b31n, 'docs.Notebook.insert'], - [0x2213822ed05cd5bdn, 0x26c5833392aa63cen, 'docs.Notebook.new'], - [0x2bd762bcdaa207ffn, 0x1b0ebab2d04c49b0n, 'docs.SavedListener'], - [0x2e086c45e9d617den, 0x4d43bf32358c01ffn, 'docs.Notebook.find'], - [0x4817dc98197ad960n, 0xf8017b5a7d58b2c7n, 'docs.Notebook.size'], - [0x614de47452d5a55cn, 0xd9aae1472494ffefn, 'docs.get_document'], - [0x67c5cb9fc119bcd5n, 0x08d5b25a1a22a7fan, 'docs.Document'], - [0x8d484bf2d78c9b70n, 0xe9093cf96912194cn, 'docs.Notebook.max_documents'], - [0xaea61d6451691c67n, 0xe4704128b3524b3dn, 'docs.watch'], - [0xd68d6ad1540cdec5n, 0x01afeb40b0a8bfe1n, 'docs.Notebook'], - [0xdb0601444a4760ccn, 0xe56c69629b444797n, 'docs.create_document'], - [0xe12d6f4838347b3en, 0xa9bab86646d84f51n, 'docs.Status'], - [0xee2f8d25b3313b50n, 0x9691232f35439a2bn, 'docs.DocsErrors'], - ]], -]; -$verify($raw, 'docs_everywhere', 'docs_everywhere', 4, $contract); - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.docs_everywhere_debug_live(kind); -} - -const docs$DocsErrorsError = class DocsErrorsError extends $Error {}; -const docs$NotFoundError = class NotFoundError extends docs$DocsErrorsError { - static CODE = 1; - constructor(message = 'Document not found') { - super(1, message); - } -}; -const docs$InvalidStatusError = class InvalidStatusError extends docs$DocsErrorsError { - static CODE = 2; - constructor(message = 'Invalid status transition') { - super(2, message); - } -}; -const $codes$docs$DocsErrors = new Map([ - [1, docs$NotFoundError], - [2, docs$InvalidStatusError], -]); -const $payloads$docs$DocsErrors = new Map(); -function $from$docs$DocsErrors(e) { - return $domain(e, $codes$docs$DocsErrors, $payloads$docs$DocsErrors); -} - -const docs$Status = Object.freeze({ - Draft: 0, - Published: 1, - Archived: 2, - 0: 'Draft', - 1: 'Published', - 2: 'Archived', -}); - -function $w$docs$Document(w, v) { - w.writeI64(v.id); - w.writeString(v.title); - w.writeI32(v.status); - $w_list_string(w, v.tags); -} -function $r$docs$Document(r) { - return { - id: r.readI64(), - title: r.readString(), - status: r.readI32(), - tags: $r_list_string(r), - }; -} -/** `[string]` */ -function $w_list_string(w, v) { - w.writeList(v, $W.String); -} -function $r_list_string(r) { - return r.readList($R.String); -} -/** `Document?` */ -function $w_opt_Document(w, v) { - w.writeOpt(v, $w$docs$Document); -} -function $r_opt_Document(r) { - return r.readOpt($r$docs$Document); -} - -function $adapt$docs$SavedListener(impl) { - $impl(impl, 'SavedListener'); - return { - on_saved($a0, $a1) { - impl.onSaved($a0, $a1); - }, - }; -} - -const docs$Notebook = class Notebook extends $Object { - constructor() { - super(); - try { - $own(this, $raw.docs_everywhere_docs_Notebook_new()); - } catch ($e) { - throw $fault($e); - } - } - static $destroy(h) { - $raw.docs_everywhere_docs_Notebook_destroy(h); - } - static $clone(h) { - return $raw.docs_everywhere_docs_Notebook_clone(h); - } - insert(document) { - const $self = $lend(this, docs$Notebook); - try { - return $raw.docs_everywhere_docs_Notebook_insert($self, $encode(document, $w$docs$Document)); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - find(id) { - const $self = $lend(this, docs$Notebook); - try { - return $decode($raw.docs_everywhere_docs_Notebook_find($self, id), $r$docs$Document); - } catch ($e) { - throw $from$docs$DocsErrors($e); - } finally { - $unlend(this); - } - } - size() { - const $self = $lend(this, docs$Notebook); - try { - return $raw.docs_everywhere_docs_Notebook_size($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - static maxDocuments() { - try { - return $raw.docs_everywhere_docs_Notebook_max_documents(); - } catch ($e) { - throw $fault($e); - } - } -}; - -const docs$createDocument = function createDocument(title, tags) { - try { - return $decode($raw.docs_everywhere_docs_create_document(title, $encode(tags, $w_list_string)), $r$docs$Document); - } catch ($e) { - throw $fault($e); - } -}; - -const docs$getDocument = function getDocument(id) { - try { - return $decode($raw.docs_everywhere_docs_get_document(id), $r_opt_Document); - } catch ($e) { - throw $fault($e); - } -}; - -const docs$publishAsync = async function publishAsync(id) { - try { - return await $raw.docs_everywhere_docs_publish_async(id); - } catch ($e) { - throw $fault($e); - } -}; - -const docs$watch = function watch(listener) { - try { - $raw.docs_everywhere_docs_watch($adapt$docs$SavedListener(listener)); - } catch ($e) { - throw $fault($e); - } -}; - -export const docs = Object.freeze({ - DocsErrorsError: docs$DocsErrorsError, - NotFoundError: docs$NotFoundError, - InvalidStatusError: docs$InvalidStatusError, - Status: docs$Status, - Notebook: docs$Notebook, - createDocument: docs$createDocument, - getDocument: docs$getDocument, - publishAsync: docs$publishAsync, - watch: docs$watch, -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_node_c.snap deleted file mode 100644 index 78ff2d0c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_node_c.snap +++ /dev/null @@ -1,2157 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/* libuv's Unix headers use POSIX types (pthread_rwlock_t, struct addrinfo) - * that glibc hides under a strict -std=c11; ask for them explicitly. */ -#if defined(__linux__) && !defined(_GNU_SOURCE) -#define _GNU_SOURCE -#endif -#ifndef NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include -#include -#include -#include -#include -#include - -/* The addon wraps deprecated functions too; the JavaScript API carries the - * deprecation instead. */ -#define EDGE_CASES_DEPRECATED(msg) -#include "edge_cases.h" - -/* - * The N-API transport of the JavaScript bindings. Every C symbol of the - * library is exported to `index.js` under its own name and called with the - * raw convention the shared JavaScript layer documents: direct values as - * numbers, bigints, and booleans; strings as strings; bytes and value - * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint - * handles (null when absent); callback interfaces as adapter objects (null - * for an absent optional one). A failure throws an instance of the - * runtime's `$Fault` class. - */ - -typedef edge_cases_error js_error; - -/* Helpers an API may not need. */ -#if defined(__GNUC__) || defined(__clang__) -#define JS_HELPER static inline __attribute__((unused)) -#define JS_TLS __thread -#else -#define JS_HELPER static inline -#define JS_TLS __declspec(thread) -#endif - -/* --------------------------------------------------------------------------- - * JavaScript threads. One `js_thread` per thread that runs JavaScript and - * loaded the addon (the main thread and each worker). It counts the - * synchronous calls into the library in progress on that thread, which a - * callback made on another thread consults while it waits for the - * JavaScript thread (see js_cb_hop). Each environment and each callback - * registration holds a reference. - * ------------------------------------------------------------------------- */ - -typedef struct { - uv_mutex_t mu; - uv_cond_t cv; /* broadcast when a call begins or ends, or a hop finishes */ - unsigned depth; /* synchronous calls into the library in progress */ - uint64_t calls; /* outermost synchronous calls begun so far */ - unsigned waiters; /* producer threads waiting in js_cb_hop */ - unsigned refs; -} js_thread; - -/* The state of the JavaScript thread this is, or NULL on any other thread. */ -static JS_TLS js_thread* js_current = NULL; - -static js_thread* js_thread_new(void) { - js_thread* t = (js_thread*)calloc(1, sizeof *t); - uv_mutex_init(&t->mu); - uv_cond_init(&t->cv); - t->refs = 1; - return t; -} - -static void js_thread_retain(js_thread* t) { - uv_mutex_lock(&t->mu); - t->refs++; - uv_mutex_unlock(&t->mu); -} - -static void js_thread_release(js_thread* t) { - uv_mutex_lock(&t->mu); - bool last = --t->refs == 0; - uv_mutex_unlock(&t->mu); - if (!last) return; - uv_cond_destroy(&t->cv); - uv_mutex_destroy(&t->mu); - free(t); -} - -/* Bracket every synchronous call into the library. */ -JS_HELPER void js_sync_begin(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - if (t->depth++ == 0) t->calls++; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -JS_HELPER void js_sync_end(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - t->depth--; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* Per-environment state (one per main thread or worker): the `$Fault` - * constructor `index.js` registers through `$setup`, and the thread. */ -typedef struct { - napi_ref fault; - js_thread* thread; -} js_env; - -static void js_env_free(napi_env env, void* data, void* hint) { - (void)hint; - js_env* e = (js_env*)data; - if (e->fault != NULL) napi_delete_reference(env, e->fault); - if (js_current == e->thread) js_current = NULL; - js_thread_release(e->thread); - free(e); -} - -/* Set up the environment of the thread loading the addon. */ -static void js_env_init(napi_env env) { - js_env* e = (js_env*)calloc(1, sizeof *e); - e->thread = js_thread_new(); - js_current = e->thread; - napi_set_instance_data(env, e, js_env_free, NULL); -} - -static js_env* js_env_of(napi_env env) { - js_env* e = NULL; - napi_get_instance_data(env, (void**)&e); - return e; -} - -JS_HELPER napi_value js_undefined(napi_env env) { - napi_value v; - napi_get_undefined(env, &v); - return v; -} - -static napi_value js_setup(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value argv[1]; - napi_get_cb_info(env, info, &argc, argv, NULL, NULL); - js_env* e = js_env_of(env); - if (e->fault != NULL) napi_delete_reference(env, e->fault); - napi_create_reference(env, argv[0], 1, &e->fault); - return js_undefined(env); -} - -/* The `$Fault` constructor, or NULL before `$setup`. */ -static napi_value js_fault_class(napi_env env) { - js_env* e = js_env_of(env); - napi_value ctor = NULL; - if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok) { - return NULL; - } - return ctor; -} - -/* --------------------------------------------------------------------------- - * Value conversions - * ------------------------------------------------------------------------- */ - -JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len, &data, &ab); - if (len > 0) memcpy(data, ptr, len); - napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out; - napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); - return out; -} - -JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { - napi_value out; - napi_create_int32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { - napi_value out; - napi_create_uint32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { - napi_value out; - napi_create_bigint_int64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { - napi_value out; - napi_create_bigint_uint64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_f64(napi_env env, double v) { - napi_value out; - napi_create_double(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_bool(napi_env env, bool v) { - napi_value out; - napi_get_boolean(env, v, &out); - return out; -} - -/* An object, iterator, or cancel token handle: the pointer as a bigint, or - * null for NULL. */ -JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { - napi_value out; - if (p == NULL) { - napi_get_null(env, &out); - } else { - napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); - } - return out; -} - -/* Returned strings and buffers: convert, then release the producer's copy. */ -JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_str(env, ptr, len); - if (ptr != NULL) edge_cases_free_bytes((uint8_t*)ptr, len); - return out; -} - -JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_bytes(env, ptr, len); - if (ptr != NULL) edge_cases_free_bytes((uint8_t*)ptr, len); - return out; -} - -/* A module's contract table as a BigUint64Array of `id, hash` pairs. */ -JS_HELPER napi_value js_new_contract(napi_env env, const edge_cases_contract_entry* table, - size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len * 2 * sizeof(uint64_t), &data, &ab); - uint64_t* words = (uint64_t*)data; - for (size_t i = 0; i < len; i++) { - words[2 * i] = table[i].id; - words[2 * i + 1] = table[i].hash; - } - napi_create_typedarray(env, napi_biguint64_array, len * 2, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, - const uint8_t* payload, size_t payload_len) { - napi_value argv[3], ctor, out = NULL; - napi_create_int32(env, code, &argv[0]); - napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); - if (payload != NULL) { - argv[2] = js_new_bytes(env, payload, payload_len); - } else { - napi_get_null(env, &argv[2]); - } - ctor = js_fault_class(env); - if (ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { - napi_create_error(env, NULL, argv[1], &out); - napi_set_named_property(env, out, "code", argv[0]); - } - return out; -} - -/* Throw the error a call reported, then release it. */ -JS_HELPER napi_value js_throw(napi_env env, js_error* err) { - napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); - edge_cases_error_clear(err); - return NULL; -} - -JS_HELPER bool js_type_error(napi_env env, const char* expected) { - bool pending = false; - napi_is_exception_pending(env, &pending); - if (!pending) napi_throw_type_error(env, NULL, expected); - return false; -} - -JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { - return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { - return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int8_t)t; - return true; -} - -JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int16_t)t; - return true; -} - -JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint8_t)t; - return true; -} - -JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint16_t)t; - return true; -} - -/* 64-bit integers: a bigint that fits, or an integral number. */ -JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_int64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { - *out = (int64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_uint64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && - d == (double)(uint64_t)d) { - *out = (uint64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { - return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { - double d = 0; - if (!js_arg_f64(env, v, &d)) return false; - *out = (float)d; - return true; -} - -JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { - return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); -} - -/* A handle argument; null or undefined is NULL when `nullable`. */ -JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return nullable || js_type_error(env, "expected an object handle"); - } - uint64_t raw = 0; - bool lossless = false; - if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || - raw == 0) { - return js_type_error(env, "expected an object handle"); - } - *out = (void*)(uintptr_t)raw; - return true; -} - -/* A borrowed byte run: a Uint8Array (or Buffer). */ -JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { - bool is_typed = false; - napi_is_typedarray(env, v, &is_typed); - if (is_typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); - if (type == napi_uint8_array || type == napi_uint8_clamped_array) { - *ptr = (const uint8_t*)data; - return true; - } - } - return js_type_error(env, "expected a Uint8Array"); -} - -/* A string argument encoded as UTF-8: short strings live in the inline - * buffer, longer ones on the heap until js_str_free. */ -typedef struct { - char* ptr; - size_t len; - char inline_buf[256]; -} js_str; - -#define JS_STR_INIT {NULL, 0, {0}} - -JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { - size_t len = 0; - if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { - return js_type_error(env, "expected a string"); - } - s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); - napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); - return true; -} - -JS_HELPER void js_str_free(js_str* s) { - if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); - s->ptr = NULL; -} - -#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) - -/* Read up to `n` arguments; missing ones are undefined. */ -#define JS_ARGS(n) \ - size_t argc = (n); \ - napi_value argv[(n) + 1]; \ - napi_get_cb_info(env, info, &argc, argv, NULL, NULL) - -/* --------------------------------------------------------------------------- - * Async calls: the completion may run on any thread, so it only records the - * result and queues the settlement on the JavaScript thread through a - * thread-safe function (which also keeps the event loop alive meanwhile). - * ------------------------------------------------------------------------- */ - -typedef enum { - JS_R_VOID, - JS_R_I32, - JS_R_U32, - JS_R_I64, - JS_R_U64, - JS_R_F64, - JS_R_BOOL, - JS_R_STR, - JS_R_BYTES, - JS_R_HANDLE -} js_kind; - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - js_kind kind; - js_error* err; - union { - int64_t i; - uint64_t u; - double f; - bool b; - const void* p; - } v; - size_t len; -} js_async; - -JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { - (void)cb; - (void)context; - js_async* a = (js_async*)data; - bool failed = a->err != NULL && a->err->code != 0; - if (env != NULL && failed) { - napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, - a->err->payload_ptr, a->err->payload_len)); - } else if (env != NULL) { - napi_value v = NULL; - switch (a->kind) { - case JS_R_VOID: v = js_undefined(env); break; - case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; - case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; - case JS_R_I64: v = js_new_i64(env, a->v.i); break; - case JS_R_U64: v = js_new_u64(env, a->v.u); break; - case JS_R_F64: v = js_new_f64(env, a->v.f); break; - case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; - case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; - } - napi_resolve_deferred(env, a->deferred, v); - } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { - /* The environment is shutting down: just release the result. */ - edge_cases_free_bytes((uint8_t*)a->v.p, a->len); - } - edge_cases_error_free(a->err); - napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); - free(a); -} - -/* Create the promise and its settlement queue for one async call. */ -JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { - js_async* a = (js_async*)calloc(1, sizeof *a); - a->kind = kind; - napi_create_promise(env, &a->deferred, promise); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, - js_async_settle, &a->tsfn); - return a; -} - -/* Called by each completion once it has recorded its result. */ -JS_HELPER void js_async_done(js_async* a, js_error* err) { - a->err = err; - napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); -} - -/* --------------------------------------------------------------------------- - * Callback interfaces. One registration per implementation passed to the - * producer: a reference to the adapter object, the JavaScript thread it - * belongs to, and a thread-safe function that runs calls made from any other - * thread on that thread while the caller waits. - * - * Waiting is what makes a call from another thread safe, and also what could - * deadlock it: if the JavaScript thread is itself inside a synchronous call - * into the library that waits for the calling thread, neither can proceed. - * A waiting call therefore watches the JavaScript thread. While that thread - * stays inside one synchronous call for JS_CB_DEADLOCK_MS without having - * started the callback, the wait is abandoned and the callback fails with - * code -4 instead of hanging (a shorter synchronous call, or one that - * returns to the event loop, just delays the callback). - * ------------------------------------------------------------------------- */ - -#define JS_CB_DEADLOCK_MS 1000 - -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; - uv_thread_t js_thread; - js_thread* thread; -} js_cb; - -typedef enum { JS_REQ_QUEUED, JS_REQ_RUNNING, JS_REQ_DONE, JS_REQ_ABANDONED } js_req_state; - -/* A call queued from another thread: `method` is the vtable index, or -1 to - * release the registration; `frame` holds the method's slots. The request is - * heap-allocated and holds its own reference to the thread state, so a - * caller that abandons it can return (and the producer can then release the - * registration) while it is still queued: the dispatcher frees it without - * touching the frame or the registration. */ -typedef struct { - js_cb* cb; - js_thread* thread; - int method; - void* frame; - js_error* out_err; - js_req_state state; -} js_cb_req; - -JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &cb->js_thread) != 0; -} - -JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, - napi_threadsafe_function_call_js dispatch) { - js_cb* cb = (js_cb*)calloc(1, sizeof *cb); - cb->env = env; - cb->js_thread = uv_thread_self(); - cb->thread = js_env_of(env)->thread; - js_thread_retain(cb->thread); - napi_create_reference(env, adapter, 1, &cb->ref); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, - &cb->tsfn); - /* A live implementation must not keep the process alive by itself. */ - napi_unref_threadsafe_function(env, cb->tsfn); - return cb; -} - -JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { - if (env != NULL) napi_delete_reference(env, cb->ref); - napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); - js_thread_release(cb->thread); - free(cb); -} - -/* A callback-interface argument: the adapter object `index.js` built, or - * null for an absent optional one. */ -JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, - napi_threadsafe_function_call_js dispatch, bool nullable, js_cb** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (nullable && (t == napi_null || t == napi_undefined)) { - *out = NULL; - return true; - } - if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); - *out = js_cb_register(env, v, name, dispatch); - return true; -} - -/* The vtable's `free`: the producer is done with the implementation. The - * reference can only be deleted on its own thread. */ -JS_HELPER void js_cb_free(void* ctx) { - js_cb* cb = (js_cb*)ctx; - if (cb == NULL) return; - if (js_cb_on_js_thread(cb)) { - js_cb_release(cb->env, cb); - return; - } - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->method = -1; - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); -} - -/* Run method `method` of `cb` on its JavaScript thread and wait for it, - * failing it with code -4 rather than deadlocking (see above). `what` names - * the method in that failure. */ -JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, - const char* what) { - js_thread* t = cb->thread; - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->thread = t; - js_thread_retain(t); - req->method = method; - req->frame = frame; - req->out_err = out_err; - req->state = JS_REQ_QUEUED; - uv_mutex_lock(&t->mu); - t->waiters++; - uv_mutex_unlock(&t->mu); - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - uv_mutex_lock(&t->mu); - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); - edge_cases_error_set(out_err, -4, "the callback implementation is no longer reachable"); - return; - } - const uint64_t limit = (uint64_t)JS_CB_DEADLOCK_MS * 1000000u; - bool blocked = false; - uint64_t call = 0, since = 0; - uv_mutex_lock(&t->mu); - while (req->state != JS_REQ_DONE) { - if (req->state == JS_REQ_QUEUED && t->depth > 0) { - uint64_t now = uv_hrtime(); - if (!blocked || call != t->calls) { - blocked = true; - call = t->calls; - since = now; - } - if (now - since >= limit) { - req->state = JS_REQ_ABANDONED; /* the dispatcher frees it */ - t->waiters--; - uv_mutex_unlock(&t->mu); - char msg[512]; - snprintf(msg, sizeof msg, - "%s was called on another thread while the JavaScript thread was blocked in a " - "synchronous call into the library for %d ms; the call likely waits for this " - "callback, so waiting longer would deadlock (call it asynchronously, or call " - "back on the calling thread)", - what, JS_CB_DEADLOCK_MS); - edge_cases_error_set(out_err, -4, msg); - return; - } - uv_cond_timedwait(&t->cv, &t->mu, limit - (now - since)); - } else { - blocked = false; - uv_cond_wait(&t->cv, &t->mu); - } - } - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); -} - -/* The dispatcher's side of a hop. js_cb_take claims a queued request (false - * when its caller gave up, in which case the request is freed); - * js_cb_finish reports it done. */ -JS_HELPER bool js_cb_take(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - bool abandoned = req->state == JS_REQ_ABANDONED; - if (!abandoned) req->state = JS_REQ_RUNNING; - uv_mutex_unlock(&t->mu); - if (abandoned) { - free(req); - js_thread_release(t); - } - return !abandoned; -} - -JS_HELPER void js_cb_finish(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - req->state = JS_REQ_DONE; - uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* A JavaScript value's text as a heap-allocated C string (NULL on failure). */ -JS_HELPER char* js_cstring(napi_env env, napi_value v) { - napi_value str; - size_t len = 0; - if (napi_coerce_to_string(env, v, &str) != napi_ok || - napi_get_value_string_utf8(env, str, NULL, 0, &len) != napi_ok) { - return NULL; - } - char* out = (char*)malloc(len + 1); - napi_get_value_string_utf8(env, str, out, len + 1, NULL); - return out; -} - -/* Report the pending JavaScript exception (or `fallback`) through - * `out_err`; nothing unwinds through the C frame. A `$Fault` (a domain error - * a throwing method raised) reports its code, message, and payload; any - * other exception is a foreign failure, code -4, with its message. */ -JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { - int32_t code = -4; - char* msg = NULL; - const uint8_t* payload = NULL; - size_t payload_len = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc, text; - napi_get_and_clear_last_exception(env, &exc); - napi_value fault = js_fault_class(env); - bool is_fault = false; - if (fault != NULL) napi_instanceof(env, exc, fault, &is_fault); - napi_valuetype t; - napi_typeof(env, exc, &t); - text = exc; - if (t == napi_object) napi_get_named_property(env, exc, "message", &text); - msg = js_cstring(env, text); - if (is_fault) { - napi_value v; - napi_get_named_property(env, exc, "code", &v); - napi_get_value_int32(env, v, &code); - napi_get_named_property(env, exc, "payload", &v); - bool typed = false; - napi_is_typedarray(env, v, &typed); - if (typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, &payload_len, &data, NULL, NULL); - payload = (const uint8_t*)data; - } - } - napi_is_exception_pending(env, &pending); - if (pending) napi_get_and_clear_last_exception(env, &exc); - } - edge_cases_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); - if (payload != NULL) edge_cases_error_set_payload(out_err, payload, payload_len); - free(msg); -} - -/* Look up and call `method` on the adapter; false when it threw. */ -JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, - const napi_value* argv, napi_value* result) { - napi_value adapter, fn; - napi_get_reference_value(env, cb->ref, &adapter); - napi_get_named_property(env, adapter, method, &fn); - return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; -} - -/* Hand `len` bytes to the producer through a callback's out slots, as a run - * from {p}_alloc that the producer adopts. */ -JS_HELPER void js_give(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = edge_cases_alloc(len); - if (len > 0) memcpy(run, data, len); - *out_ptr = run; - *out_len = len; -} - -/* A callback's string return: false (with a pending exception) when the - * value isn't a string. */ -JS_HELPER bool js_ret_str(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - js_str s = JS_STR_INIT; - if (!js_arg_str(env, v, &s)) return false; - js_give(s.ptr, s.len, out_ptr, out_len); - js_str_free(&s); - return true; -} - -/* A callback's bytes or buffer return: a Uint8Array. */ -JS_HELPER bool js_ret_bytes(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - const uint8_t* ptr = NULL; - size_t len = 0; - if (!js_arg_bytes(env, v, &ptr, &len)) return false; - js_give(ptr, len, out_ptr, out_len); - return true; -} - -/* --------------------------------------------------------------------------- - * The runtime symbols every library exports. - * ------------------------------------------------------------------------- */ - -static napi_value js_abi_version(napi_env env, napi_callback_info info) { - (void)info; - return js_new_u32(env, edge_cases_abi_version()); -} - -static napi_value js_debug_live(napi_env env, napi_callback_info info) { - JS_ARGS(1); - int32_t kind = 0; - if (!js_arg_i32(env, argv[0], &kind)) return NULL; - return js_new_u64(env, edge_cases_debug_live(kind)); -} - -static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { - (void)info; - return js_new_handle(env, edge_cases_cancel_token_create()); -} - -static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - edge_cases_cancel_token_cancel((edge_cases_cancel_token*)token); - return js_undefined(env); -} - -static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - edge_cases_cancel_token_destroy((edge_cases_cancel_token*)token); - return js_undefined(env); -} - -#define JS_RUNTIME_EXPORTS \ - {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ - {"edge_cases_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ - {"edge_cases_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ - {"edge_cases_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"edge_cases_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"edge_cases_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ - napi_default, NULL} - -static napi_value nx_edge_cases_edge_cases_contract(napi_env env, napi_callback_info info) { - (void)info; - size_t len = 0; - const edge_cases_contract_entry* table = edge_cases_edge_cases_contract(&len); - return js_new_contract(env, table, len); -} - -typedef struct { - js_error* out_err; - const uint8_t* p0; - size_t p1; - const uint8_t* p2; - size_t p3; - const uint8_t* p4; - size_t p5; - edge_cases_edge_cases_Mode p6; -} frame_edge_cases_edge_cases_BatchListener_on_batch; - -static void invoke_edge_cases_edge_cases_BatchListener_on_batch(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_on_batch* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[5]; - argv[0] = js_new_bytes(env, f->p0, f->p1); - argv[1] = js_new_bytes(env, f->p2, f->p3); - argv[2] = js_new_bytes(env, f->p4, f->p5); - argv[3] = js_new_i32(env, (int32_t)f->p6); - napi_value result; - if (!js_cb_call(env, cb, "on_batch", 4, argv, &result)) { - js_cb_report(env, f->out_err, "BatchListener.onBatch failed"); - } - napi_close_handle_scope(env, scope); -} - -static void tramp_edge_cases_edge_cases_BatchListener_on_batch(void* ctx, const uint8_t* p0, size_t p1, const uint8_t* p2, size_t p3, const uint8_t* p4, size_t p5, edge_cases_edge_cases_Mode p6, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_BatchListener_on_batch f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - f.p1 = p1; - f.p2 = p2; - f.p3 = p3; - f.p4 = p4; - f.p5 = p5; - f.p6 = p6; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_BatchListener_on_batch(cb->env, cb, &f); - } else { - js_cb_hop(cb, 0, &f, out_err, "BatchListener.onBatch"); - } -} - -typedef struct { - js_error* out_err; - edge_cases_edge_cases_Widget* p0; - edge_cases_edge_cases_Widget* p1; - const uint8_t* p2; - size_t p3; -} frame_edge_cases_edge_cases_BatchListener_on_widget; - -static void invoke_edge_cases_edge_cases_BatchListener_on_widget(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_on_widget* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[4]; - argv[0] = js_new_handle(env, f->p0); - argv[1] = js_new_handle(env, f->p1); - argv[2] = js_new_bytes(env, f->p2, f->p3); - napi_value result; - if (!js_cb_call(env, cb, "on_widget", 3, argv, &result)) { - js_cb_report(env, f->out_err, "BatchListener.onWidget failed"); - } - napi_close_handle_scope(env, scope); -} - -static void tramp_edge_cases_edge_cases_BatchListener_on_widget(void* ctx, edge_cases_edge_cases_Widget* p0, edge_cases_edge_cases_Widget* p1, const uint8_t* p2, size_t p3, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_BatchListener_on_widget f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - f.p1 = p1; - f.p2 = p2; - f.p3 = p3; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_BatchListener_on_widget(cb->env, cb, &f); - } else { - js_cb_hop(cb, 1, &f, out_err, "BatchListener.onWidget"); - } -} - -typedef struct { - js_error* out_err; -} frame_edge_cases_edge_cases_BatchListener_on_tick; - -static void invoke_edge_cases_edge_cases_BatchListener_on_tick(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_on_tick* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[1]; - napi_value result; - if (!js_cb_call(env, cb, "on_tick", 0, argv, &result)) { - js_cb_report(env, f->out_err, "BatchListener.onTick failed"); - } - napi_close_handle_scope(env, scope); -} - -static void tramp_edge_cases_edge_cases_BatchListener_on_tick(void* ctx, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_BatchListener_on_tick f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_BatchListener_on_tick(cb->env, cb, &f); - } else { - js_cb_hop(cb, 2, &f, out_err, "BatchListener.onTick"); - } -} - -typedef struct { - js_error* out_err; - int32_t p0; - bool result; -} frame_edge_cases_edge_cases_BatchListener_should_continue; - -static void invoke_edge_cases_edge_cases_BatchListener_should_continue(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_should_continue* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[2]; - argv[0] = js_new_i32(env, (int32_t)f->p0); - napi_value result; - if (!js_cb_call(env, cb, "should_continue", 1, argv, &result)) { - js_cb_report(env, f->out_err, "BatchListener.shouldContinue failed"); - } else { - bool v = 0; - if (js_arg_bool(env, result, &v)) { - f->result = (bool)v; - } else { - js_cb_report(env, f->out_err, "BatchListener.shouldContinue returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static bool tramp_edge_cases_edge_cases_BatchListener_should_continue(void* ctx, int32_t p0, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_BatchListener_should_continue f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_BatchListener_should_continue(cb->env, cb, &f); - } else { - js_cb_hop(cb, 3, &f, out_err, "BatchListener.shouldContinue"); - } - return f.result; -} - -typedef struct { - js_error* out_err; - edge_cases_edge_cases_Mode result; -} frame_edge_cases_edge_cases_BatchListener_pick_mode; - -static void invoke_edge_cases_edge_cases_BatchListener_pick_mode(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_pick_mode* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[1]; - napi_value result; - if (!js_cb_call(env, cb, "pick_mode", 0, argv, &result)) { - js_cb_report(env, f->out_err, "BatchListener.pickMode failed"); - } else { - int32_t v = 0; - if (js_arg_i32(env, result, &v)) { - f->result = (edge_cases_edge_cases_Mode)v; - } else { - js_cb_report(env, f->out_err, "BatchListener.pickMode returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static edge_cases_edge_cases_Mode tramp_edge_cases_edge_cases_BatchListener_pick_mode(void* ctx, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_BatchListener_pick_mode f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_BatchListener_pick_mode(cb->env, cb, &f); - } else { - js_cb_hop(cb, 4, &f, out_err, "BatchListener.pickMode"); - } - return f.result; -} - -static void dispatch_edge_cases_edge_cases_BatchListener(napi_env env, napi_value fn, void* context, void* data) { - (void)fn; - (void)context; - js_cb_req* req = (js_cb_req*)data; - if (req->method < 0) { - js_cb_release(env, req->cb); - free(req); - return; - } - if (!js_cb_take(req)) return; - if (env == NULL) { - edge_cases_error_set(req->out_err, -4, "the JavaScript environment is shutting down"); - } else { - switch (req->method) { - case 0: invoke_edge_cases_edge_cases_BatchListener_on_batch(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_on_batch*)req->frame); break; - case 1: invoke_edge_cases_edge_cases_BatchListener_on_widget(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_on_widget*)req->frame); break; - case 2: invoke_edge_cases_edge_cases_BatchListener_on_tick(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_on_tick*)req->frame); break; - case 3: invoke_edge_cases_edge_cases_BatchListener_should_continue(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_should_continue*)req->frame); break; - case 4: invoke_edge_cases_edge_cases_BatchListener_pick_mode(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_pick_mode*)req->frame); break; - default: break; - } - } - js_cb_finish(req); -} - -static const edge_cases_edge_cases_BatchListener_vtable vtable_edge_cases_edge_cases_BatchListener = {sizeof(edge_cases_edge_cases_BatchListener_vtable), 0, js_cb_free, tramp_edge_cases_edge_cases_BatchListener_on_batch, tramp_edge_cases_edge_cases_BatchListener_on_widget, tramp_edge_cases_edge_cases_BatchListener_on_tick, tramp_edge_cases_edge_cases_BatchListener_should_continue, tramp_edge_cases_edge_cases_BatchListener_pick_mode}; - -typedef struct { - js_error* out_err; - int32_t p0; -} frame_edge_cases_edge_cases_Keyword_delete; - -static void invoke_edge_cases_edge_cases_Keyword_delete(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_Keyword_delete* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[2]; - argv[0] = js_new_i32(env, (int32_t)f->p0); - napi_value result; - if (!js_cb_call(env, cb, "delete", 1, argv, &result)) { - js_cb_report(env, f->out_err, "Keyword.delete failed"); - } - napi_close_handle_scope(env, scope); -} - -static void tramp_edge_cases_edge_cases_Keyword_delete(void* ctx, int32_t p0, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_Keyword_delete f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_Keyword_delete(cb->env, cb, &f); - } else { - js_cb_hop(cb, 0, &f, out_err, "Keyword.delete"); - } -} - -typedef struct { - js_error* out_err; - int32_t result; -} frame_edge_cases_edge_cases_Keyword_import; - -static void invoke_edge_cases_edge_cases_Keyword_import(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_Keyword_import* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value argv[1]; - napi_value result; - if (!js_cb_call(env, cb, "import", 0, argv, &result)) { - js_cb_report(env, f->out_err, "Keyword.import failed"); - } else { - int32_t v = 0; - if (js_arg_i32(env, result, &v)) { - f->result = (int32_t)v; - } else { - js_cb_report(env, f->out_err, "Keyword.import returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static int32_t tramp_edge_cases_edge_cases_Keyword_import(void* ctx, edge_cases_error* out_err) { - frame_edge_cases_edge_cases_Keyword_import f; - memset(&f, 0, sizeof f); - f.out_err = out_err; - js_cb* cb = (js_cb*)ctx; - if (js_cb_on_js_thread(cb)) { - invoke_edge_cases_edge_cases_Keyword_import(cb->env, cb, &f); - } else { - js_cb_hop(cb, 1, &f, out_err, "Keyword.import"); - } - return f.result; -} - -static void dispatch_edge_cases_edge_cases_Keyword(napi_env env, napi_value fn, void* context, void* data) { - (void)fn; - (void)context; - js_cb_req* req = (js_cb_req*)data; - if (req->method < 0) { - js_cb_release(env, req->cb); - free(req); - return; - } - if (!js_cb_take(req)) return; - if (env == NULL) { - edge_cases_error_set(req->out_err, -4, "the JavaScript environment is shutting down"); - } else { - switch (req->method) { - case 0: invoke_edge_cases_edge_cases_Keyword_delete(env, req->cb, (frame_edge_cases_edge_cases_Keyword_delete*)req->frame); break; - case 1: invoke_edge_cases_edge_cases_Keyword_import(env, req->cb, (frame_edge_cases_edge_cases_Keyword_import*)req->frame); break; - default: break; - } - } - js_cb_finish(req); -} - -static const edge_cases_edge_cases_Keyword_vtable vtable_edge_cases_edge_cases_Keyword = {sizeof(edge_cases_edge_cases_Keyword_vtable), 0, js_cb_free, tramp_edge_cases_edge_cases_Keyword_delete, tramp_edge_cases_edge_cases_Keyword_import}; - -static napi_value nx_edge_cases_edge_cases_Widget_clone(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_Widget_clone((const edge_cases_edge_cases_Widget*)h); - js_sync_end(); - return js_new_handle(env, r); -} - -static napi_value nx_edge_cases_edge_cases_Widget_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_Widget_destroy((edge_cases_edge_cases_Widget*)h); - js_sync_end(); - return js_undefined(env); -} - -static napi_value nx_edge_cases_edge_cases_Vault_clone(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_Vault* r = edge_cases_edge_cases_Vault_clone((const edge_cases_edge_cases_Vault*)h); - js_sync_end(); - return js_new_handle(env, r); -} - -static napi_value nx_edge_cases_edge_cases_Vault_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_Vault_destroy((edge_cases_edge_cases_Vault*)h); - js_sync_end(); - return js_undefined(env); -} - -static napi_value nx_edge_cases_edge_cases_noop(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_noop(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_undefined(env); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_select(napi_env env, napi_callback_info info) { - JS_ARGS(5); - napi_value ret = NULL; - int32_t a0 = 0; - js_str a1 = JS_STR_INIT; - const uint8_t* a2 = NULL; - size_t a2_len = 0; - const uint8_t* a3 = NULL; - size_t a3_len = 0; - bool a4 = 0; - if (!js_arg_i32(env, argv[0], &a0)) goto done; - if (!js_arg_str(env, argv[1], &a1)) goto done; - if (!js_arg_bytes(env, argv[2], &a2, &a2_len)) goto done; - if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; - if (!js_arg_bool(env, argv[4], &a4)) goto done; - { - js_error err = {0}; - js_sync_begin(); - int32_t r = edge_cases_edge_cases_select(a0, JS_STR_PTR(a1), a1.len, a2, a2_len, a3, a3_len, a4, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_i32(env, (int32_t)r); - } - goto done; -done: - js_str_free(&a1); - return ret; -} - -static napi_value nx_edge_cases_edge_cases_nested_optional(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_nested_optional(a0, a0_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_optional_params(napi_env env, napi_callback_info info) { - JS_ARGS(6); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - const uint8_t* a1 = NULL; - size_t a1_len = 0; - const uint8_t* a2 = NULL; - size_t a2_len = 0; - const uint8_t* a3 = NULL; - size_t a3_len = 0; - const uint8_t* a4 = NULL; - size_t a4_len = 0; - const uint8_t* a5 = NULL; - size_t a5_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - if (!js_arg_bytes(env, argv[1], &a1, &a1_len)) goto done; - if (!js_arg_bytes(env, argv[2], &a2, &a2_len)) goto done; - if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; - if (!js_arg_bytes(env, argv[4], &a4, &a4_len)) goto done; - if (!js_arg_bytes(env, argv[5], &a5, &a5_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_optional_params(a0, a0_len, a1, a1_len, a2, a2_len, a3, a3_len, a4, a4_len, a5, a5_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_register_batch(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - js_cb* a0 = NULL; - js_cb* a1 = NULL; - if (!js_arg_cb(env, argv[0], "edge_cases_edge_cases_BatchListener", dispatch_edge_cases_edge_cases_BatchListener, false, &a0)) goto done; - if (!js_arg_cb(env, argv[1], "edge_cases_edge_cases_Keyword", dispatch_edge_cases_edge_cases_Keyword, false, &a1)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_register_batch((void*)a0, &vtable_edge_cases_edge_cases_BatchListener, (void*)a1, &vtable_edge_cases_edge_cases_Keyword, &err); - js_sync_end(); - a0 = NULL; - a1 = NULL; - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_undefined(env); - } - goto done; -done: - if (a0 != NULL) js_cb_release(env, a0); - if (a1 != NULL) js_cb_release(env, a1); - return ret; -} - -static napi_value nx_edge_cases_edge_cases_widgets_everywhere(napi_env env, napi_callback_info info) { - JS_ARGS(4); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - const uint8_t* a1 = NULL; - size_t a1_len = 0; - const uint8_t* a2 = NULL; - size_t a2_len = 0; - const uint8_t* a3 = NULL; - size_t a3_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - if (!js_arg_bytes(env, argv[1], &a1, &a1_len)) goto done; - if (!js_arg_bytes(env, argv[2], &a2, &a2_len)) goto done; - if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_widgets_everywhere(a0, a0_len, a1, a1_len, a2, a2_len, a3, a3_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_widget_stream(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_WidgetStreamIterator* r = edge_cases_edge_cases_widget_stream(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_WidgetStreamIterator_next(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - edge_cases_edge_cases_Widget* item; - memset(&item, 0, sizeof item); - size_t item_len = 0; - js_error err = {0}; - js_sync_begin(); - int32_t has = edge_cases_edge_cases_WidgetStreamIterator_next((edge_cases_edge_cases_WidgetStreamIterator*)h, &item, &err); - js_sync_end(); - (void)item_len; - if (err.code != 0) return js_throw(env, &err); - if (has == 0) return js_undefined(env); - return js_new_handle(env, item); -} - -static napi_value nx_edge_cases_edge_cases_WidgetStreamIterator_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_WidgetStreamIterator_destroy((edge_cases_edge_cases_WidgetStreamIterator*)h); - js_sync_end(); - return js_undefined(env); -} - -static void done_edge_cases_edge_cases_async_widget(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result) { - js_async* a = (js_async*)context; - a->v.p = result; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_widget(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_async* a = js_async_begin(env, JS_R_HANDLE, "edge_cases_edge_cases_async_widget", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_widget(done_edge_cases_edge_cases_async_widget, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_make_widget(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - js_str a0 = JS_STR_INIT; - if (!js_arg_str(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_make_widget(JS_STR_PTR(a0), a0.len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - js_str_free(&a0); - return ret; -} - -static napi_value nx_edge_cases_edge_cases_find_widget(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - int64_t a0 = 0; - if (!js_arg_i64(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_find_widget(a0, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_inspect_widget(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - void* a0 = NULL; - void* a1 = NULL; - if (!js_arg_handle(env, argv[0], &a0, false)) goto done; - if (!js_arg_handle(env, argv[1], &a1, true)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_inspect_widget((const edge_cases_edge_cases_Widget*)a0, (const edge_cases_edge_cases_Widget*)a1, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_str(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_string_stream(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_StringStreamIterator* r = edge_cases_edge_cases_string_stream(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_StringStreamIterator_next(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - const uint8_t* item; - memset(&item, 0, sizeof item); - size_t item_len = 0; - js_error err = {0}; - js_sync_begin(); - int32_t has = edge_cases_edge_cases_StringStreamIterator_next((edge_cases_edge_cases_StringStreamIterator*)h, &item, &item_len, &err); - js_sync_end(); - if (err.code != 0) return js_throw(env, &err); - if (has == 0) return js_undefined(env); - return js_take_str(env, item, item_len); -} - -static napi_value nx_edge_cases_edge_cases_StringStreamIterator_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_StringStreamIterator_destroy((edge_cases_edge_cases_StringStreamIterator*)h); - js_sync_end(); - return js_undefined(env); -} - -static napi_value nx_edge_cases_edge_cases_scalar_stream(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_ScalarStreamIterator* r = edge_cases_edge_cases_scalar_stream(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_ScalarStreamIterator_next(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - int32_t item; - memset(&item, 0, sizeof item); - size_t item_len = 0; - js_error err = {0}; - js_sync_begin(); - int32_t has = edge_cases_edge_cases_ScalarStreamIterator_next((edge_cases_edge_cases_ScalarStreamIterator*)h, &item, &err); - js_sync_end(); - (void)item_len; - if (err.code != 0) return js_throw(env, &err); - if (has == 0) return js_undefined(env); - return js_new_i32(env, (int32_t)item); -} - -static napi_value nx_edge_cases_edge_cases_ScalarStreamIterator_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_ScalarStreamIterator_destroy((edge_cases_edge_cases_ScalarStreamIterator*)h); - js_sync_end(); - return js_undefined(env); -} - -static napi_value nx_edge_cases_edge_cases_mode_stream(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_ModeStreamIterator* r = edge_cases_edge_cases_mode_stream(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_ModeStreamIterator_next(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - edge_cases_edge_cases_Mode item; - memset(&item, 0, sizeof item); - size_t item_len = 0; - js_error err = {0}; - js_sync_begin(); - int32_t has = edge_cases_edge_cases_ModeStreamIterator_next((edge_cases_edge_cases_ModeStreamIterator*)h, &item, &err); - js_sync_end(); - (void)item_len; - if (err.code != 0) return js_throw(env, &err); - if (has == 0) return js_undefined(env); - return js_new_i32(env, (int32_t)item); -} - -static napi_value nx_edge_cases_edge_cases_ModeStreamIterator_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* h = NULL; - if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - js_sync_begin(); - edge_cases_edge_cases_ModeStreamIterator_destroy((edge_cases_edge_cases_ModeStreamIterator*)h); - js_sync_end(); - return js_undefined(env); -} - -static void done_edge_cases_edge_cases_async_void(void* context, edge_cases_error* err) { - js_async* a = (js_async*)context; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_void(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_async* a = js_async_begin(env, JS_R_VOID, "edge_cases_edge_cases_async_void", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_void(done_edge_cases_edge_cases_async_void, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static void done_edge_cases_edge_cases_async_bytes(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - js_async* a = (js_async*)context; - a->v.p = result_ptr; - a->len = result_len; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_bytes(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - uint32_t a0 = 0; - if (!js_arg_u32(env, argv[0], &a0)) goto done; - { - js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_bytes", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_bytes(a0, done_edge_cases_edge_cases_async_bytes, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static void done_edge_cases_edge_cases_async_struct(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - js_async* a = (js_async*)context; - a->v.p = result_ptr; - a->len = result_len; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_struct(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_struct", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_struct(done_edge_cases_edge_cases_async_struct, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static void done_edge_cases_edge_cases_async_list(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - js_async* a = (js_async*)context; - a->v.p = result_ptr; - a->len = result_len; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_list(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, true)) goto done; - { - js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_list", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_list((edge_cases_cancel_token*)token, done_edge_cases_edge_cases_async_list, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static void done_edge_cases_edge_cases_async_optional(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - js_async* a = (js_async*)context; - a->v.p = result_ptr; - a->len = result_len; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_optional(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_optional", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_optional(done_edge_cases_edge_cases_async_optional, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static void done_edge_cases_edge_cases_async_enum(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result) { - js_async* a = (js_async*)context; - a->v.i = result; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_enum(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_async* a = js_async_begin(env, JS_R_I32, "edge_cases_edge_cases_async_enum", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_enum(done_edge_cases_edge_cases_async_enum, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static void done_edge_cases_edge_cases_async_keywords(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - js_async* a = (js_async*)context; - a->v.p = result_ptr; - a->len = result_len; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_async_keywords(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - js_str a0 = JS_STR_INIT; - int32_t a1 = 0; - if (!js_arg_str(env, argv[0], &a0)) goto done; - if (!js_arg_i32(env, argv[1], &a1)) goto done; - { - js_async* a = js_async_begin(env, JS_R_STR, "edge_cases_edge_cases_async_keywords", &ret); - js_sync_begin(); - edge_cases_edge_cases_async_keywords(JS_STR_PTR(a0), a0.len, a1, done_edge_cases_edge_cases_async_keywords, a); - js_sync_end(); - } - goto done; -done: - js_str_free(&a0); - return ret; -} - -static napi_value nx_edge_cases_edge_cases_keywords_roundtrip(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_keywords_roundtrip(a0, a0_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_legacy_value(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - int32_t a0 = 0; - if (!js_arg_i32(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_legacy_value((edge_cases_edge_cases_Legacy)a0, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Widget_refresh(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* self_h = NULL; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_Widget_refresh((const edge_cases_edge_cases_Widget*)self_h, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_undefined(env); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Widget_rename(napi_env env, napi_callback_info info) { - JS_ARGS(3); - napi_value ret = NULL; - void* self_h = NULL; - js_str a0 = JS_STR_INIT; - const uint8_t* a1 = NULL; - size_t a1_len = 0; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - if (!js_arg_str(env, argv[1], &a0)) goto done; - if (!js_arg_bytes(env, argv[2], &a1, &a1_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_Widget_rename((const edge_cases_edge_cases_Widget*)self_h, JS_STR_PTR(a0), a0.len, a1, a1_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_str(env, r, out_len); - } - goto done; -done: - js_str_free(&a0); - return ret; -} - -static void done_edge_cases_edge_cases_Widget_fetch(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { - js_async* a = (js_async*)context; - a->v.p = result_ptr; - a->len = result_len; - js_async_done(a, err); -} - -static napi_value nx_edge_cases_edge_cases_Widget_fetch(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - void* self_h = NULL; - int64_t a0 = 0; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - if (!js_arg_i64(env, argv[1], &a0)) goto done; - { - js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_Widget_fetch", &ret); - js_sync_begin(); - edge_cases_edge_cases_Widget_fetch((const edge_cases_edge_cases_Widget*)self_h, a0, done_edge_cases_edge_cases_Widget_fetch, a); - js_sync_end(); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Widget_old_name(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* self_h = NULL; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_Widget_old_name((const edge_cases_edge_cases_Widget*)self_h, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_str(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Widget_close(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* self_h = NULL; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - { - js_error err = {0}; - js_sync_begin(); - bool r = edge_cases_edge_cases_Widget_close((const edge_cases_edge_cases_Widget*)self_h, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_bool(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Widget_dispose(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - void* self_h = NULL; - bool a0 = 0; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - if (!js_arg_bool(env, argv[1], &a0)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_Widget_dispose((const edge_cases_edge_cases_Widget*)self_h, a0, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_undefined(env); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Vault_open(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - js_str a0 = JS_STR_INIT; - if (!js_arg_str(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_Vault* r = edge_cases_edge_cases_Vault_open(JS_STR_PTR(a0), a0.len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - js_str_free(&a0); - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Vault_peek(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* self_h = NULL; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - { - js_error err = {0}; - js_sync_begin(); - edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_Vault_peek((const edge_cases_edge_cases_Vault*)self_h, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_handle(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Vault_handle(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - void* self_h = NULL; - if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; - { - js_error err = {0}; - js_sync_begin(); - int64_t r = edge_cases_edge_cases_Vault_handle((const edge_cases_edge_cases_Vault*)self_h, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_i64(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Vault_probe(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - js_sync_begin(); - bool r = edge_cases_edge_cases_Vault_probe(&err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_bool(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_edge_cases_edge_cases_Vault_to_string(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = edge_cases_edge_cases_Vault_to_string(&out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_str(env, r, out_len); - } - goto done; -done: - return ret; -} - -NAPI_MODULE_INIT() { - js_env_init(env); - napi_property_descriptor props[] = { - JS_RUNTIME_EXPORTS, - {"edge_cases_edge_cases_contract", NULL, nx_edge_cases_edge_cases_contract, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_clone", NULL, nx_edge_cases_edge_cases_Widget_clone, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_destroy", NULL, nx_edge_cases_edge_cases_Widget_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_clone", NULL, nx_edge_cases_edge_cases_Vault_clone, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_destroy", NULL, nx_edge_cases_edge_cases_Vault_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_noop", NULL, nx_edge_cases_edge_cases_noop, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_select", NULL, nx_edge_cases_edge_cases_select, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_nested_optional", NULL, nx_edge_cases_edge_cases_nested_optional, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_optional_params", NULL, nx_edge_cases_edge_cases_optional_params, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_register_batch", NULL, nx_edge_cases_edge_cases_register_batch, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_widgets_everywhere", NULL, nx_edge_cases_edge_cases_widgets_everywhere, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_widget_stream", NULL, nx_edge_cases_edge_cases_widget_stream, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_WidgetStreamIterator_next", NULL, nx_edge_cases_edge_cases_WidgetStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_WidgetStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_WidgetStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_widget", NULL, nx_edge_cases_edge_cases_async_widget, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_make_widget", NULL, nx_edge_cases_edge_cases_make_widget, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_find_widget", NULL, nx_edge_cases_edge_cases_find_widget, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_inspect_widget", NULL, nx_edge_cases_edge_cases_inspect_widget, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_string_stream", NULL, nx_edge_cases_edge_cases_string_stream, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_StringStreamIterator_next", NULL, nx_edge_cases_edge_cases_StringStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_StringStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_StringStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_scalar_stream", NULL, nx_edge_cases_edge_cases_scalar_stream, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_ScalarStreamIterator_next", NULL, nx_edge_cases_edge_cases_ScalarStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_ScalarStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_ScalarStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_mode_stream", NULL, nx_edge_cases_edge_cases_mode_stream, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_ModeStreamIterator_next", NULL, nx_edge_cases_edge_cases_ModeStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_ModeStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_ModeStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_void", NULL, nx_edge_cases_edge_cases_async_void, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_bytes", NULL, nx_edge_cases_edge_cases_async_bytes, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_struct", NULL, nx_edge_cases_edge_cases_async_struct, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_list", NULL, nx_edge_cases_edge_cases_async_list, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_optional", NULL, nx_edge_cases_edge_cases_async_optional, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_enum", NULL, nx_edge_cases_edge_cases_async_enum, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_async_keywords", NULL, nx_edge_cases_edge_cases_async_keywords, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_keywords_roundtrip", NULL, nx_edge_cases_edge_cases_keywords_roundtrip, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_legacy_value", NULL, nx_edge_cases_edge_cases_legacy_value, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_refresh", NULL, nx_edge_cases_edge_cases_Widget_refresh, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_rename", NULL, nx_edge_cases_edge_cases_Widget_rename, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_fetch", NULL, nx_edge_cases_edge_cases_Widget_fetch, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_old_name", NULL, nx_edge_cases_edge_cases_Widget_old_name, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_close", NULL, nx_edge_cases_edge_cases_Widget_close, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Widget_dispose", NULL, nx_edge_cases_edge_cases_Widget_dispose, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_open", NULL, nx_edge_cases_edge_cases_Vault_open, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_peek", NULL, nx_edge_cases_edge_cases_Vault_peek, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_handle", NULL, nx_edge_cases_edge_cases_Vault_handle, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_probe", NULL, nx_edge_cases_edge_cases_Vault_probe, NULL, NULL, NULL, napi_default, NULL}, - {"edge_cases_edge_cases_Vault_to_string", NULL, nx_edge_cases_edge_cases_Vault_to_string, NULL, NULL, NULL, napi_default, NULL}, - }; - napi_define_properties(env, exports, sizeof props / sizeof props[0], props); - return exports; -} - -// END edge_cases_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_d_ts.snap deleted file mode 100644 index a7c2071e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_d_ts.snap +++ /dev/null @@ -1,278 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class EdgeCasesError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends EdgeCasesError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof EdgeCasesError; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -export declare namespace edge_cases { - /** The errors of the `edge_cases` error domain. */ - export class EdgeErrorsError extends $Error { - constructor(code: number, message?: string); - } - /** Resource is busy */ - export class BusyError extends edge_cases.EdgeErrorsError { - static readonly CODE: 7; - constructor(message?: string); - } - /** - * Deprecated enum with non-contiguous values and keyword-like variants - * @deprecated Use Mode instead - */ - export enum Legacy { - None = 0, - Default = 5, - Class = 10, - } - export enum Mode { - Fast = 1, - Safe = 2, - } - export interface Leaf { - weight: number; - } - /** Middle layer of a three-deep struct nesting */ - export interface Inner { - leaf: edge_cases.Leaf; - leaves: edge_cases.Leaf[]; - } - export interface Outer { - inner: edge_cases.Inner; - maybe_inner: edge_cases.Inner | null; - by_name: Record; - modes: edge_cases.Mode[]; - matrix: number[][]; - sparse: (bigint | null)[]; - blob: Uint8Array | null; - note: string | null; - by_id: Record; - _hidden: boolean; - v2: number; - } - /** Every field is a reserved word somewhere */ - export interface Keywords { - class: number; - def: number; - import: number; - from: number; - func: number; - range: number; - register: number; - volatile: number; - new: number; - default: number; - end: number; - in: number; - is: number; - int: number; - var: number; - val: number; - self: number; - this: number; - object: number; - string: number; - case: number; - select: number; - switch: number; - void: number; - union: number; - namespace: number; - operator: number; - template: number; - package: number; - protocol: number; - guard: number; - defer: number; - inout: number; - internal: number; - when: number; - fun: number; - data: number; - late: number; - part: number; - out: number; - ref: number; - event: number; - base: number; - lambda: number; - yield: number; - nil: number; - null: number; - begin: number; - rescue: number; - module: number; - chan: number; - go: number; - let: number; - export: number; - delete: number; - typedef: number; - sizeof: number; - static: number; - super: number; - } - /** - * Deprecated struct - * @deprecated Use Outer instead - */ - export interface Retired { - id: bigint; - } - /** Record carrying objects in every buffered position */ - export interface WidgetHolder { - primary: edge_cases.Widget; - fallback: edge_cases.Widget | null; - extras: edge_cases.Widget[]; - by_name: Record; - } - /** Callback interface carrying composite payloads and objects */ - export interface BatchListener { - /** Composite payloads cross as value buffers */ - onBatch(items: edge_cases.Outer[], raw: Uint8Array, first: edge_cases.Outer | null, mode: edge_cases.Mode): void; - /** Objects cross as strong references the consumer must release */ - onWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null, children: edge_cases.Widget[]): void; - onTick(): void; - /** Scalar return travels back through the vtable */ - shouldContinue(class_: number): boolean; - /** Enum return travels back as its integer discriminant */ - pickMode(): edge_cases.Mode; - } - /** Callback interface whose name and methods are keywords */ - export interface Keyword { - delete(self: number): void; - import(): number; - } - /** Interface with no constructor; instances come from make_widget */ - export class Widget { - private constructor(); - /** - * Void method that can fail - * @throws {edge_cases.EdgeErrorsError} - */ - refresh(): void; - rename(class_: string, import_: string | null): string; - /** - * Async method with a struct result - * @throws {edge_cases.EdgeErrorsError} - */ - fetch(id: bigint): Promise; - /** @deprecated Use rename instead */ - oldName(): string; - /** Spelled like the release method of several wrappers */ - close_(): boolean; - /** Spelled like the release method of the Dart and .NET wrappers */ - dispose(force: boolean): void; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; - } - /** - * Deprecated interface with a throwing constructor - * @deprecated Use Widget instead - */ - export class Vault { - private constructor(); - /** - * Fails when the vault is busy - * @throws {edge_cases.EdgeErrorsError} - */ - static open(path: string): edge_cases.Vault; - peek(): edge_cases.Widget | null; - /** Spelled like the handle accessor of several wrappers */ - handle(): bigint; - /** - * Throwing static - * @throws {edge_cases.EdgeErrorsError} - */ - static probe(): boolean; - /** Static spelled like an inherited object member */ - static toString(): string; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; - } - /** No parameters and no result */ - export function noop(): void; - /** Function named after a keyword whose parameters are all keywords */ - export function select(class_: number, from: string, func: string[], range: number | null, type: boolean): number; - /** Optional list of optional structs, the deepest legal nesting */ - export function nestedOptional(items: (edge_cases.Outer | null)[] | null): (edge_cases.Outer | null)[] | null; - export function optionalParams(text: string | null, count: number | null, outer: edge_cases.Outer | null, tags: string[] | null, blob: Uint8Array | null, mode: edge_cases.Mode | null): edge_cases.Mode | null; - /** Two callback interface parameters on one function */ - export function registerBatch(listener: edge_cases.BatchListener, keyword: edge_cases.Keyword): void; - /** Objects in every buffered position */ - export function widgetsEverywhere(list: edge_cases.Widget[], byName: Record, sparse: (edge_cases.Widget | null)[], holder: edge_cases.WidgetHolder): edge_cases.WidgetHolder; - /** Iterator of objects */ - export function widgetStream(): IterableIterator; - /** - * Async returning an object - * @throws {edge_cases.EdgeErrorsError} - */ - export function asyncWidget(): Promise; - /** Factory for an interface with no constructor */ - export function makeWidget(label: string): edge_cases.Widget; - /** - * Optional interface return - * @throws {edge_cases.EdgeErrorsError} - */ - export function findWidget(id: bigint): edge_cases.Widget | null; - /** Interface parameter, including an optional one */ - export function inspectWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null): string; - /** Iterator of strings */ - export function stringStream(): IterableIterator; - /** Iterator of scalars */ - export function scalarStream(): IterableIterator; - /** Iterator of enums */ - export function modeStream(): IterableIterator; - /** Async with no result */ - export function asyncVoid(): Promise; - export function asyncBytes(size: number): Promise; - /** @throws {edge_cases.EdgeErrorsError} */ - export function asyncStruct(): Promise; - /** @throws {edge_cases.EdgeErrorsError} */ - export function asyncList(options?: { signal?: AbortSignal }): Promise; - export function asyncOptional(): Promise; - export function asyncEnum(): Promise; - export function asyncKeywords(class_: string, import_: number): Promise; - export function keywordsRoundtrip(value: edge_cases.Keywords): edge_cases.Keywords; - /** @deprecated Use Mode-based APIs */ - export function legacyValue(value: edge_cases.Legacy): edge_cases.Retired; -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap deleted file mode 100644 index fe279862..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap +++ /dev/null @@ -1,943 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - EdgeCasesError as $Error, -} from './runtime.js'; -export { EdgeCasesError, CancelledError } from './runtime.js'; - -import { createRequire } from 'node:module'; - -// The native entry points: the N-API addon built from edge_cases_node.c. A prebuilt -// copy comes first (from the `edge_cases--` package `npm install` -// picks for this platform, or under prebuilds/-/), then the one -// node-gyp compiles at install time when none matches. -function $loadAddon(name) { - const require = createRequire(import.meta.url); - const platform = `${process.platform}-${process.arch}`; - const candidates = [ - `edge_cases-${platform}/${name}`, - `./prebuilds/${platform}/${name}`, - `./build/Release/${name}`, - `./build/Debug/${name}`, - ]; - for (const candidate of candidates) { - try { - return require(candidate); - } catch (e) { - if (e.code !== 'MODULE_NOT_FOUND') throw e; - } - } - throw new Error( - `the native addon ${name} was not found (tried ${candidates.join(', ')}); ` + - 'install the package for this platform, or build the addon with `npm install` ' + - 'or `node-gyp rebuild` in this package', - ); -} - -const $raw = $loadAddon('edge_cases_node.node'); -$raw.$setup($Fault); - -// Object tokens in value buffers are already bigint handles here. -const $token = (t) => t; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['edge_cases', [ - [0x056a86511785ba2bn, 0x5cba3f16d57c6cdbn, 'edge_cases.make_widget'], - [0x07bfa9ecb2b720d2n, 0x3813b3f57c64bf35n, 'edge_cases.EdgeErrors'], - [0x0aaa11867e37f991n, 0xf090dd52146ebdf0n, 'edge_cases.Widget.refresh'], - [0x0f9701493898ae4cn, 0x6944f47a275c96e8n, 'edge_cases.Inner'], - [0x1226b1adac179f7dn, 0x0e95a00ab0bef637n, 'edge_cases.async_keywords'], - [0x16d36bef1541136cn, 0x5ea7021ec324298fn, 'edge_cases.find_widget'], - [0x1b7324c55e980febn, 0xabf87fe372358f42n, 'edge_cases.Widget.old_name'], - [0x1cbd731467c2bab8n, 0x4915b9989c2c9074n, 'edge_cases.Vault'], - [0x2c2a6de67d894b0an, 0x50a4496b1e7191can, 'edge_cases.Vault.open'], - [0x322b4cfcd2d6861cn, 0xca7527a379ae8465n, 'edge_cases.Keywords'], - [0x3f8e23a5f32906f4n, 0x8d4c908d796f130dn, 'edge_cases.Leaf'], - [0x42ab8a685457b97cn, 0xe5ce30500cadb32dn, 'edge_cases.Vault.handle'], - [0x4a68540e2836d3d0n, 0xa20e44c71e420ed2n, 'edge_cases.mode_stream'], - [0x4e030580ac0b111dn, 0xb68b7a18d910e175n, 'edge_cases.legacy_value'], - [0x4f5e4e5291864ef3n, 0x55a1a88b21a08f8bn, 'edge_cases.Legacy'], - [0x4f84b06171b4213dn, 0xbaedcfdebb6a2820n, 'edge_cases.scalar_stream'], - [0x5067a5ff2d9ef910n, 0xbfc79b18fc4714fcn, 'edge_cases.Widget'], - [0x59aff159112fbf5bn, 0x18e4def57a8cfa49n, 'edge_cases.optional_params'], - [0x5daf65b98fa77c48n, 0xc817a8cb202e59c2n, 'edge_cases.keywords_roundtrip'], - [0x5de8a5c9726c2c5an, 0x7c50b5b65f7a896dn, 'edge_cases.register_batch'], - [0x618a620309994c3cn, 0x826ad0b1d208d723n, 'edge_cases.async_bytes'], - [0x648285cdba0a7b8en, 0xcf6e80e783a4d8ben, 'edge_cases.WidgetHolder'], - [0x649c78079824b10dn, 0x5b12dd26e0634b69n, 'edge_cases.async_widget'], - [0x7da7a7357d122a37n, 0xf544e025ffaa3d20n, 'edge_cases.async_void'], - [0x7e4015bc3a472e2dn, 0xfe22291d90b82e4an, 'edge_cases.widget_stream'], - [0x805276e5d81fdba7n, 0xd9b64e360bd03b2bn, 'edge_cases.Outer'], - [0x80e1183326542df4n, 0xe8a57e12cc6ed276n, 'edge_cases.select'], - [0x923ce5e9bcd769a1n, 0x0b973f5dc07dda8bn, 'edge_cases.Vault.to_string'], - [0x98ca8c01f5239404n, 0xfe11c93eb6b303efn, 'edge_cases.Widget.rename'], - [0xa13e22e98ea278d4n, 0xf1fae215d7387800n, 'edge_cases.widgets_everywhere'], - [0xa84a8c9bf2a881e1n, 0xbe1dc840bf221996n, 'edge_cases.async_optional'], - [0xad958c478fbf205dn, 0xe386b0716d24f19bn, 'edge_cases.Retired'], - [0xb2fc3964ef56ab39n, 0x73c325c8bc351236n, 'edge_cases.async_list'], - [0xb3403af521cf0006n, 0x6abc30d48206a3ban, 'edge_cases.Widget.close'], - [0xb794304a22a406b8n, 0xf6fb98711c516105n, 'edge_cases.async_struct'], - [0xba3936106a49c47dn, 0xb7025f2fadd6288en, 'edge_cases.Vault.peek'], - [0xc155b0c3cfb164f4n, 0xc777ae57d92ebc47n, 'edge_cases.noop'], - [0xcdc18607777019ddn, 0xb81a31e5c20aa7a8n, 'edge_cases.inspect_widget'], - [0xd164a82aeb64b8ccn, 0xded02c68005c4737n, 'edge_cases.nested_optional'], - [0xd978ccad2468038fn, 0x30c39c65d6235153n, 'edge_cases.Mode'], - [0xdbe9d947431c9124n, 0x39537fb848fdf980n, 'edge_cases.BatchListener'], - [0xeae8b7862c08bb07n, 0xfe12eeaa830452ean, 'edge_cases.Keyword'], - [0xf1e155a05ae262b8n, 0xb575eb818a423d42n, 'edge_cases.async_enum'], - [0xf22e210bdbce7343n, 0x2a5e0f801bc8c768n, 'edge_cases.Widget.dispose'], - [0xf3c7cf675563ee6cn, 0x0ecffeef399facc8n, 'edge_cases.Vault.probe'], - [0xfd34bc67acac379en, 0x576caa14f3c0901an, 'edge_cases.Widget.fetch'], - [0xff2e9d2bff941786n, 0x7e96764ef1f5fbaan, 'edge_cases.string_stream'], - ]], -]; -$verify($raw, 'edge_cases', 'edge_cases', 4, $contract); - -// The native cancel tokens behind `AbortSignal`s. -const $tokens = { - create: () => $raw.edge_cases_cancel_token_create(), - cancel: (t) => $raw.edge_cases_cancel_token_cancel(t), - destroy: (t) => $raw.edge_cases_cancel_token_destroy(t), -}; - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.edge_cases_debug_live(kind); -} - -const edge_cases$EdgeErrorsError = class EdgeErrorsError extends $Error {}; -const edge_cases$BusyError = class BusyError extends edge_cases$EdgeErrorsError { - static CODE = 7; - constructor(message = 'Resource is busy') { - super(7, message); - } -}; -const $codes$edge_cases$EdgeErrors = new Map([ - [7, edge_cases$BusyError], -]); -const $payloads$edge_cases$EdgeErrors = new Map(); -function $from$edge_cases$EdgeErrors(e) { - return $domain(e, $codes$edge_cases$EdgeErrors, $payloads$edge_cases$EdgeErrors); -} - -const edge_cases$Legacy = Object.freeze({ - None: 0, - Default: 5, - Class: 10, - 0: 'None', - 5: 'Default', - 10: 'Class', -}); - -const edge_cases$Mode = Object.freeze({ - Fast: 1, - Safe: 2, - 1: 'Fast', - 2: 'Safe', -}); - -function $w$edge_cases$Leaf(w, v) { - w.writeF32(v.weight); -} -function $r$edge_cases$Leaf(r) { - return { - weight: r.readF32(), - }; -} -function $w$edge_cases$Inner(w, v) { - $w$edge_cases$Leaf(w, v.leaf); - $w_list_Leaf(w, v.leaves); -} -function $r$edge_cases$Inner(r) { - return { - leaf: $r$edge_cases$Leaf(r), - leaves: $r_list_Leaf(r), - }; -} -function $w$edge_cases$Outer(w, v) { - $w$edge_cases$Inner(w, v.inner); - $w_opt_Inner(w, v.maybe_inner); - $w_map_string_Inner(w, v.by_name); - $w_list_Mode(w, v.modes); - $w_list_list_i32(w, v.matrix); - $w_list_opt_i64(w, v.sparse); - $w_opt_bytes(w, v.blob); - $w_opt_string(w, v.note); - $w_map_i64_string(w, v.by_id); - w.writeBool(v._hidden); - w.writeU8(v.v2); -} -function $r$edge_cases$Outer(r) { - return { - inner: $r$edge_cases$Inner(r), - maybe_inner: $r_opt_Inner(r), - by_name: $r_map_string_Inner(r), - modes: $r_list_Mode(r), - matrix: $r_list_list_i32(r), - sparse: $r_list_opt_i64(r), - blob: $r_opt_bytes(r), - note: $r_opt_string(r), - by_id: $r_map_i64_string(r), - _hidden: r.readBool(), - v2: r.readU8(), - }; -} -function $w$edge_cases$Keywords(w, v) { - w.writeI32(v.class); - w.writeI32(v.def); - w.writeI32(v.import); - w.writeI32(v.from); - w.writeI32(v.func); - w.writeI32(v.range); - w.writeI32(v.register); - w.writeI32(v.volatile); - w.writeI32(v.new); - w.writeI32(v.default); - w.writeI32(v.end); - w.writeI32(v.in); - w.writeI32(v.is); - w.writeI32(v.int); - w.writeI32(v.var); - w.writeI32(v.val); - w.writeI32(v.self); - w.writeI32(v.this); - w.writeI32(v.object); - w.writeI32(v.string); - w.writeI32(v.case); - w.writeI32(v.select); - w.writeI32(v.switch); - w.writeI32(v.void); - w.writeI32(v.union); - w.writeI32(v.namespace); - w.writeI32(v.operator); - w.writeI32(v.template); - w.writeI32(v.package); - w.writeI32(v.protocol); - w.writeI32(v.guard); - w.writeI32(v.defer); - w.writeI32(v.inout); - w.writeI32(v.internal); - w.writeI32(v.when); - w.writeI32(v.fun); - w.writeI32(v.data); - w.writeI32(v.late); - w.writeI32(v.part); - w.writeI32(v.out); - w.writeI32(v.ref); - w.writeI32(v.event); - w.writeI32(v.base); - w.writeI32(v.lambda); - w.writeI32(v.yield); - w.writeI32(v.nil); - w.writeI32(v.null); - w.writeI32(v.begin); - w.writeI32(v.rescue); - w.writeI32(v.module); - w.writeI32(v.chan); - w.writeI32(v.go); - w.writeI32(v.let); - w.writeI32(v.export); - w.writeI32(v.delete); - w.writeI32(v.typedef); - w.writeI32(v.sizeof); - w.writeI32(v.static); - w.writeI32(v.super); -} -function $r$edge_cases$Keywords(r) { - return { - class: r.readI32(), - def: r.readI32(), - import: r.readI32(), - from: r.readI32(), - func: r.readI32(), - range: r.readI32(), - register: r.readI32(), - volatile: r.readI32(), - new: r.readI32(), - default: r.readI32(), - end: r.readI32(), - in: r.readI32(), - is: r.readI32(), - int: r.readI32(), - var: r.readI32(), - val: r.readI32(), - self: r.readI32(), - this: r.readI32(), - object: r.readI32(), - string: r.readI32(), - case: r.readI32(), - select: r.readI32(), - switch: r.readI32(), - void: r.readI32(), - union: r.readI32(), - namespace: r.readI32(), - operator: r.readI32(), - template: r.readI32(), - package: r.readI32(), - protocol: r.readI32(), - guard: r.readI32(), - defer: r.readI32(), - inout: r.readI32(), - internal: r.readI32(), - when: r.readI32(), - fun: r.readI32(), - data: r.readI32(), - late: r.readI32(), - part: r.readI32(), - out: r.readI32(), - ref: r.readI32(), - event: r.readI32(), - base: r.readI32(), - lambda: r.readI32(), - yield: r.readI32(), - nil: r.readI32(), - null: r.readI32(), - begin: r.readI32(), - rescue: r.readI32(), - module: r.readI32(), - chan: r.readI32(), - go: r.readI32(), - let: r.readI32(), - export: r.readI32(), - delete: r.readI32(), - typedef: r.readI32(), - sizeof: r.readI32(), - static: r.readI32(), - super: r.readI32(), - }; -} -function $w$edge_cases$Retired(w, v) { - w.writeI64(v.id); -} -function $r$edge_cases$Retired(r) { - return { - id: r.readI64(), - }; -} -function $w$edge_cases$WidgetHolder(w, v) { - $w$edge_cases$Widget(w, v.primary); - $w_opt_Widget(w, v.fallback); - $w_list_Widget(w, v.extras); - $w_map_string_Widget(w, v.by_name); -} -function $r$edge_cases$WidgetHolder(r) { - return { - primary: $r$edge_cases$Widget(r), - fallback: $r_opt_Widget(r), - extras: $r_list_Widget(r), - by_name: $r_map_string_Widget(r), - }; -} -function $w$edge_cases$Widget(w, v) { - w.writeU64(BigInt($clone(v, edge_cases$Widget))); -} -function $r$edge_cases$Widget(r) { - return $adopt(edge_cases$Widget, $token(r.readU64())); -} -/** `[Leaf]` */ -function $w_list_Leaf(w, v) { - w.writeList(v, $w$edge_cases$Leaf); -} -function $r_list_Leaf(r) { - return r.readList($r$edge_cases$Leaf); -} -/** `[Mode]` */ -function $w_list_Mode(w, v) { - w.writeList(v, $W.I32); -} -function $r_list_Mode(r) { - return r.readList($R.I32); -} -/** `[Outer]` */ -function $w_list_Outer(w, v) { - w.writeList(v, $w$edge_cases$Outer); -} -function $r_list_Outer(r) { - return r.readList($r$edge_cases$Outer); -} -/** `[Widget]` */ -function $w_list_Widget(w, v) { - w.writeList(v, $w$edge_cases$Widget); -} -function $r_list_Widget(r) { - return r.readList($r$edge_cases$Widget); -} -/** `[i32]` */ -function $w_list_i32(w, v) { - w.writeList(v, $W.I32); -} -function $r_list_i32(r) { - return r.readList($R.I32); -} -/** `[[i32]]` */ -function $w_list_list_i32(w, v) { - w.writeList(v, $w_list_i32); -} -function $r_list_list_i32(r) { - return r.readList($r_list_i32); -} -/** `[Outer?]` */ -function $w_list_opt_Outer(w, v) { - w.writeList(v, $w_opt_Outer); -} -function $r_list_opt_Outer(r) { - return r.readList($r_opt_Outer); -} -/** `[Widget?]` */ -function $w_list_opt_Widget(w, v) { - w.writeList(v, $w_opt_Widget); -} -function $r_list_opt_Widget(r) { - return r.readList($r_opt_Widget); -} -/** `[i64?]` */ -function $w_list_opt_i64(w, v) { - w.writeList(v, $w_opt_i64); -} -function $r_list_opt_i64(r) { - return r.readList($r_opt_i64); -} -/** `[string]` */ -function $w_list_string(w, v) { - w.writeList(v, $W.String); -} -function $r_list_string(r) { - return r.readList($R.String); -} -/** `{i64:string}` */ -function $w_map_i64_string(w, v) { - w.writeMap(v, $WK.I64, $W.String); -} -function $r_map_i64_string(r) { - return r.readMap($R.I64, $R.String); -} -/** `{string:Inner}` */ -function $w_map_string_Inner(w, v) { - w.writeMap(v, $WK.String, $w$edge_cases$Inner); -} -function $r_map_string_Inner(r) { - return r.readMap($R.String, $r$edge_cases$Inner); -} -/** `{string:Widget}` */ -function $w_map_string_Widget(w, v) { - w.writeMap(v, $WK.String, $w$edge_cases$Widget); -} -function $r_map_string_Widget(r) { - return r.readMap($R.String, $r$edge_cases$Widget); -} -/** `Inner?` */ -function $w_opt_Inner(w, v) { - w.writeOpt(v, $w$edge_cases$Inner); -} -function $r_opt_Inner(r) { - return r.readOpt($r$edge_cases$Inner); -} -/** `Mode?` */ -function $w_opt_Mode(w, v) { - w.writeOpt(v, $W.I32); -} -function $r_opt_Mode(r) { - return r.readOpt($R.I32); -} -/** `Outer?` */ -function $w_opt_Outer(w, v) { - w.writeOpt(v, $w$edge_cases$Outer); -} -function $r_opt_Outer(r) { - return r.readOpt($r$edge_cases$Outer); -} -/** `Widget?` */ -function $w_opt_Widget(w, v) { - w.writeOpt(v, $w$edge_cases$Widget); -} -function $r_opt_Widget(r) { - return r.readOpt($r$edge_cases$Widget); -} -/** `bytes?` */ -function $w_opt_bytes(w, v) { - w.writeOpt(v, $W.Bytes); -} -function $r_opt_bytes(r) { - return r.readOpt($R.Bytes); -} -/** `i32?` */ -function $w_opt_i32(w, v) { - w.writeOpt(v, $W.I32); -} -function $r_opt_i32(r) { - return r.readOpt($R.I32); -} -/** `i64?` */ -function $w_opt_i64(w, v) { - w.writeOpt(v, $W.I64); -} -function $r_opt_i64(r) { - return r.readOpt($R.I64); -} -/** `[Outer?]?` */ -function $w_opt_list_opt_Outer(w, v) { - w.writeOpt(v, $w_list_opt_Outer); -} -function $r_opt_list_opt_Outer(r) { - return r.readOpt($r_list_opt_Outer); -} -/** `[string]?` */ -function $w_opt_list_string(w, v) { - w.writeOpt(v, $w_list_string); -} -function $r_opt_list_string(r) { - return r.readOpt($r_list_string); -} -/** `string?` */ -function $w_opt_string(w, v) { - w.writeOpt(v, $W.String); -} -function $r_opt_string(r) { - return r.readOpt($R.String); -} - -function $adapt$edge_cases$BatchListener(impl) { - $impl(impl, 'BatchListener'); - return { - on_batch($a0, $a1, $a2, $a3) { - impl.onBatch($decode($a0, $r_list_Outer), $a1, $decode($a2, $r_opt_Outer), $a3); - }, - on_widget($a0, $a1, $a2) { - impl.onWidget($adopt(edge_cases$Widget, $a0), $adoptOpt(edge_cases$Widget, $a1), $decode($a2, $r_list_Widget)); - }, - on_tick() { - impl.onTick(); - }, - should_continue($a0) { - return $ret.Bool(impl.shouldContinue($a0), 'BatchListener.shouldContinue'); - }, - pick_mode() { - return $ret.I32(impl.pickMode(), 'BatchListener.pickMode'); - }, - }; -} - -function $adapt$edge_cases$Keyword(impl) { - $impl(impl, 'Keyword'); - return { - delete($a0) { - impl.delete($a0); - }, - import() { - return $ret.I32(impl.import(), 'Keyword.import'); - }, - }; -} - -const edge_cases$Widget = class Widget extends $Object { - constructor() { - super(); - throw new TypeError('Widget has no public constructor; use its factory functions'); - } - static $destroy(h) { - $raw.edge_cases_edge_cases_Widget_destroy(h); - } - static $clone(h) { - return $raw.edge_cases_edge_cases_Widget_clone(h); - } - refresh() { - const $self = $lend(this, edge_cases$Widget); - try { - $raw.edge_cases_edge_cases_Widget_refresh($self); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } finally { - $unlend(this); - } - } - rename(class_, import_) { - const $self = $lend(this, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_Widget_rename($self, class_, $encode(import_, $w_opt_string)); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - async fetch(id) { - const $self = $lend(this, edge_cases$Widget); - try { - return $decode(await $raw.edge_cases_edge_cases_Widget_fetch($self, id), $r$edge_cases$Outer); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } finally { - $unlend(this); - } - } - oldName() { - const $self = $lend(this, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_Widget_old_name($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - close_() { - const $self = $lend(this, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_Widget_close($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - dispose(force) { - const $self = $lend(this, edge_cases$Widget); - try { - $raw.edge_cases_edge_cases_Widget_dispose($self, force); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } -}; - -const edge_cases$Vault = class Vault extends $Object { - constructor() { - super(); - throw new TypeError('Vault has no public constructor; use its factory functions'); - } - static $destroy(h) { - $raw.edge_cases_edge_cases_Vault_destroy(h); - } - static $clone(h) { - return $raw.edge_cases_edge_cases_Vault_clone(h); - } - static open(path) { - try { - return $adopt(edge_cases$Vault, $raw.edge_cases_edge_cases_Vault_open(path)); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } - } - peek() { - const $self = $lend(this, edge_cases$Vault); - try { - return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_Vault_peek($self)); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - handle() { - const $self = $lend(this, edge_cases$Vault); - try { - return $raw.edge_cases_edge_cases_Vault_handle($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - static probe() { - try { - return $raw.edge_cases_edge_cases_Vault_probe(); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } - } - static toString() { - try { - return $raw.edge_cases_edge_cases_Vault_to_string(); - } catch ($e) { - throw $fault($e); - } - } -}; - -const edge_cases$noop = function noop() { - try { - $raw.edge_cases_edge_cases_noop(); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$select = function select(class_, from, func, range, type) { - try { - return $raw.edge_cases_edge_cases_select(class_, from, $encode(func, $w_list_string), $encode(range, $w_opt_i32), type); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$nestedOptional = function nestedOptional(items) { - try { - return $decode($raw.edge_cases_edge_cases_nested_optional($encode(items, $w_opt_list_opt_Outer)), $r_opt_list_opt_Outer); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$optionalParams = function optionalParams(text, count, outer, tags, blob, mode) { - try { - return $decode($raw.edge_cases_edge_cases_optional_params($encode(text, $w_opt_string), $encode(count, $w_opt_i32), $encode(outer, $w_opt_Outer), $encode(tags, $w_opt_list_string), $encode(blob, $w_opt_bytes), $encode(mode, $w_opt_Mode)), $r_opt_Mode); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$registerBatch = function registerBatch(listener, keyword) { - try { - $raw.edge_cases_edge_cases_register_batch($adapt$edge_cases$BatchListener(listener), $adapt$edge_cases$Keyword(keyword)); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$widgetsEverywhere = function widgetsEverywhere(list, byName, sparse, holder) { - try { - return $decode($raw.edge_cases_edge_cases_widgets_everywhere($encode(list, $w_list_Widget), $encode(byName, $w_map_string_Widget), $encode(sparse, $w_list_opt_Widget), $encode(holder, $w$edge_cases$WidgetHolder)), $r$edge_cases$WidgetHolder); - } catch ($e) { - throw $fault($e); - } -}; - -const $it$edge_cases$widget_stream = { - next: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_destroy(h), - map: $fault, - convert: (v) => $adopt(edge_cases$Widget, v), -}; -const edge_cases$widgetStream = function widgetStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_widget_stream(), $it$edge_cases$widget_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncWidget = async function asyncWidget() { - try { - return $adopt(edge_cases$Widget, await $raw.edge_cases_edge_cases_async_widget()); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$makeWidget = function makeWidget(label) { - try { - return $adopt(edge_cases$Widget, $raw.edge_cases_edge_cases_make_widget(label)); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$findWidget = function findWidget(id) { - try { - return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_find_widget(id)); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$inspectWidget = function inspectWidget(widget, fallback) { - const $o0 = $lend(widget, edge_cases$Widget); - try { - const $o1 = $lendOpt(fallback, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_inspect_widget($o0, $o1); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(fallback); - } - } finally { - $unlend(widget); - } -}; - -const $it$edge_cases$string_stream = { - next: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_destroy(h), - map: $fault, - convert: null, -}; -const edge_cases$stringStream = function stringStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_string_stream(), $it$edge_cases$string_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const $it$edge_cases$scalar_stream = { - next: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_destroy(h), - map: $fault, - convert: null, -}; -const edge_cases$scalarStream = function scalarStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_scalar_stream(), $it$edge_cases$scalar_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const $it$edge_cases$mode_stream = { - next: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_destroy(h), - map: $fault, - convert: null, -}; -const edge_cases$modeStream = function modeStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_mode_stream(), $it$edge_cases$mode_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncVoid = async function asyncVoid() { - try { - await $raw.edge_cases_edge_cases_async_void(); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncBytes = async function asyncBytes(size) { - try { - return await $raw.edge_cases_edge_cases_async_bytes(size); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncStruct = async function asyncStruct() { - try { - return $decode(await $raw.edge_cases_edge_cases_async_struct(), $r$edge_cases$Outer); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$asyncList = async function asyncList($options) { - try { - return $decode(await $cancellable($options?.signal, $tokens, ($t) => $raw.edge_cases_edge_cases_async_list($t)), $r_list_Outer); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$asyncOptional = async function asyncOptional() { - try { - return $decode(await $raw.edge_cases_edge_cases_async_optional(), $r_opt_i64); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncEnum = async function asyncEnum() { - try { - return await $raw.edge_cases_edge_cases_async_enum(); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncKeywords = async function asyncKeywords(class_, import_) { - try { - return await $raw.edge_cases_edge_cases_async_keywords(class_, import_); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$keywordsRoundtrip = function keywordsRoundtrip(value) { - try { - return $decode($raw.edge_cases_edge_cases_keywords_roundtrip($encode(value, $w$edge_cases$Keywords)), $r$edge_cases$Keywords); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$legacyValue = function legacyValue(value) { - try { - return $decode($raw.edge_cases_edge_cases_legacy_value(value), $r$edge_cases$Retired); - } catch ($e) { - throw $fault($e); - } -}; - -export const edge_cases = Object.freeze({ - EdgeErrorsError: edge_cases$EdgeErrorsError, - BusyError: edge_cases$BusyError, - Legacy: edge_cases$Legacy, - Mode: edge_cases$Mode, - Widget: edge_cases$Widget, - Vault: edge_cases$Vault, - noop: edge_cases$noop, - select: edge_cases$select, - nestedOptional: edge_cases$nestedOptional, - optionalParams: edge_cases$optionalParams, - registerBatch: edge_cases$registerBatch, - widgetsEverywhere: edge_cases$widgetsEverywhere, - widgetStream: edge_cases$widgetStream, - asyncWidget: edge_cases$asyncWidget, - makeWidget: edge_cases$makeWidget, - findWidget: edge_cases$findWidget, - inspectWidget: edge_cases$inspectWidget, - stringStream: edge_cases$stringStream, - scalarStream: edge_cases$scalarStream, - modeStream: edge_cases$modeStream, - asyncVoid: edge_cases$asyncVoid, - asyncBytes: edge_cases$asyncBytes, - asyncStruct: edge_cases$asyncStruct, - asyncList: edge_cases$asyncList, - asyncOptional: edge_cases$asyncOptional, - asyncEnum: edge_cases$asyncEnum, - asyncKeywords: edge_cases$asyncKeywords, - keywordsRoundtrip: edge_cases$keywordsRoundtrip, - legacyValue: edge_cases$legacyValue, -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__README_md.snap deleted file mode 100644 index f44cd897..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__README_md.snap +++ /dev/null @@ -1,24 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink - -Native bindings for kitchen_sink - -Node.js bindings for the `kitchen_sink` native library: an ES module over a small N-API addon (`kitchen_sink_node.c`). `npm install` uses a prebuilt addon when one matches the platform (from a per-platform package, or under `prebuilds/-/`) and otherwise compiles it with node-gyp. A compiled addon links `kitchen_sink`; set `KITCHEN_SINK_LIBRARY` to the library file (or its directory) when installing, or pass `--kitchen_sink-library=` to npm. - -```sh -KITCHEN_SINK_LIBRARY=/path/to/libkitchen_sink.so npm install kitchen_sink -``` - -Each top-level module of the API is a namespace export: - -```js -import { shared, kitchen } from 'kitchen_sink'; -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap deleted file mode 100644 index 0aacaa7e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -{ - "variables": { - "native_dir%": " { const path = require('path'); const p = process.env.KITCHEN_SINK_LIBRARY || process.env.npm_config_kitchen_sink_library; if (!p) return path.resolve('.'); return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); })()\")" - }, - "targets": [ - { - "target_name": "kitchen_sink_node", - "sources": [ - "kitchen_sink_node.c" - ], - "include_dirs": [ - "." - ], - "library_dirs": [ - "<(native_dir)" - ], - "libraries": [ - "-lkitchen_sink" - ], - "conditions": [ - ["OS=='mac'", { "xcode_settings": { "OTHER_LDFLAGS": ["-Wl,-rpath,@loader_path", "-Wl,-rpath,<(native_dir)"] } }], - ["OS=='linux'", { "ldflags": ["-Wl,-rpath,'$$ORIGIN'", "-Wl,-rpath,<(native_dir)"] }] - ] - } - ] -} - -# END binding.gyp diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap index 94b7afa1..b232ac0c 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap @@ -9,9 +9,9 @@ source: crates/weaveffi-cli/tests/snapshots.rs /** * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). + * code: positive for a domain error, negative for a runtime failure (-1 + * an untyped failure, -2 producer panic, -3 marshalling failure, -4 a + * callback implementation failed, -5 cancelled). */ export declare class KitchenSinkError extends Error { readonly code: number; @@ -24,12 +24,17 @@ export declare class CancelledError extends KitchenSinkError { /** The root error class, under a name no namespace member can shadow. */ declare const $Error: typeof KitchenSinkError; /** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal + * A lazy iterator over a native `iter`: each `next()` makes one native + * call. The native iterator is released when it is exhausted or fails, by + * `return()` (which `for...of` calls on `break`), by `close()` or a `using` + * declaration, or when the iterator is garbage collected. */ -export declare function __debugLive(kind: number): bigint; +export interface NativeIterator extends IterableIterator { + /** Release the native iterator now. Safe to call more than once. */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; +} export declare namespace shared { /** A reusable token referenced across modules */ @@ -42,17 +47,32 @@ export declare namespace shared { } export declare namespace kitchen { - /** The errors of the `kitchen` error domain. */ - export class KitchenErrorsError extends $Error { + /** The errors of the `KitchenErrors` domain (an unknown code is this class itself). */ + export class KitchenError extends $Error { constructor(code: number, message?: string); } /** Item not found */ - export class NotFoundError extends kitchen.KitchenErrorsError { + export class NotFoundError extends kitchen.KitchenError { static readonly CODE: 1; constructor(message?: string); } /** Invalid input */ - export class InvalidInputError extends kitchen.KitchenErrorsError { + export class InvalidInputError extends kitchen.KitchenError { + static readonly CODE: 2; + constructor(message?: string); + } + /** The errors of the `PantryError` domain (an unknown code is this class itself). */ + export class PantryError extends $Error { + constructor(code: number, message?: string); + } + /** Out of stock */ + export class OutOfStockError extends kitchen.PantryError { + static readonly CODE: 1; + readonly item: string; + constructor(fields: { item: string }, message?: string); + } + /** Spoiled */ + export class SpoiledError extends kitchen.PantryError { static readonly CODE: 2; constructor(message?: string); } @@ -96,7 +116,7 @@ export declare namespace kitchen { onItem(item: kitchen.Item, gadget: kitchen.Gadget): boolean; /** * A display label, failing with a kitchen error - * @throws {kitchen.KitchenErrorsError} to report that error, with its fields, to the library + * @throws {kitchen.KitchenError} to report one of its codes, with its fields, to the library */ label(): string; /** The most recent item the listener kept, if any */ @@ -104,6 +124,21 @@ export declare namespace kitchen { /** The listener's favorite gadget */ favorite(): kitchen.Gadget; } + /** Consumer-implemented source of scalar samples */ + export interface Sampler { + /** The cutoff for a level, if the sampler has one */ + threshold(level: number | null): number | null; + /** + * Up to `limit` samples, failing when the pantry is empty + * @throws {kitchen.PantryError} to report one of its codes, with its fields, to the library + */ + samples(limit: number | null, weights: number[]): readonly number[] | Int16Array; + /** + * A free-form description, failing with an untyped error + * @throws to report a failure, with its message, to the library + */ + describe(): string; + } /** A pocket-sized interface exercising the object surface */ export class Gadget { /** Create a gadget with the given id */ @@ -112,9 +147,14 @@ export declare namespace kitchen { describe(): string; /** * Poke the gadget a number of times, failing on a negative count - * @throws {kitchen.KitchenErrorsError} + * @throws {kitchen.KitchenError} when the call fails with one of its codes */ poke(times: number): number; + /** + * The gadget's sensor readings, optionally limited + * @throws {KitchenSinkError} (code -1) when the call fails + */ + readings(limit: number | null): number[]; /** The gadget subsystem version string */ static version(): string; /** @@ -143,10 +183,10 @@ export declare namespace kitchen { /** A list of objects */ export function gadgets(): kitchen.Gadget[]; /** An iterator over objects */ - export function streamGadgets(): IterableIterator; + export function streamGadgets(): NativeIterator; /** * Optional struct return - * @throws {kitchen.KitchenErrorsError} + * @throws {kitchen.KitchenError} when the call fails with one of its codes */ export function maybeItem(id: bigint): kitchen.Item | null; /** List of structs */ @@ -154,7 +194,7 @@ export declare namespace kitchen { /** Map return type */ export function getAttrs(): Record; /** Iterator return type */ - export function streamItems(): IterableIterator; + export function streamItems(): NativeIterator; /** Returns the shared Token type from another module */ export function crossModuleToken(): shared.Token; /** Async operation */ @@ -163,11 +203,48 @@ export declare namespace kitchen { export function doCancellable(input: string, options?: { signal?: AbortSignal }): Promise; /** * Legacy operation kept for compatibility - * @deprecated Use new_op instead + * @deprecated Use `newOp` instead */ export function legacyOp(): number; - /** Replacement for legacy_op */ + /** Replacement for `legacyOp` */ export function newOp(): number; + /** Optional scalar in both directions */ + export function optI32Id(x: number | null): number | null; + export function optBoolId(x: boolean | null): boolean | null; + /** Optional C-style enum in both directions */ + export function optPriority(p: kitchen.Priority | null): kitchen.Priority | null; + /** Optional scalars next to ordinary parameters */ + export function optMixed(base: number, scale: number | null, offset: bigint | null, label: string | null): number | null; + /** A typed-array parameter */ + export function sumF64(xs: readonly number[] | Float64Array): number; + /** Typed arrays in both directions */ + export function scaleI32(xs: readonly number[] | Int32Array, factor: number): number[]; + export function idsU64(ids: readonly bigint[] | BigUint64Array): bigint[]; + /** Every slice element type */ + export function mixedSlices(a: readonly number[] | Int8Array, b: readonly number[] | Int16Array, c: readonly number[] | Uint16Array, d: readonly number[] | Uint32Array, e: readonly bigint[] | BigInt64Array, f: readonly number[] | Float32Array): number[]; + /** Async returning an optional scalar */ + export function asyncMaybeCount(): Promise; + /** Async returning a typed array */ + export function asyncSamples(n: number, options?: { signal?: AbortSignal }): Promise; + /** Iterator of optional scalars */ + export function streamMaybe(): NativeIterator; + /** Iterator of typed arrays */ + export function streamChunks(): NativeIterator; + /** + * Drive a consumer-implemented sampler + * @throws {kitchen.PantryError} when the call fails with one of its codes + */ + export function sampleWith(sampler: kitchen.Sampler, level: number | null): number[]; + /** + * Fails with the second domain + * @throws {kitchen.PantryError} when the call fails with one of its codes + */ + export function takeFromPantry(item: string): number; + /** + * Fails with an untyped error + * @throws {KitchenSinkError} (code -1) when the call fails + */ + export function parseAny(text: string): bigint; export namespace nested { /** Trivial nested-module function */ export function hello(): string; diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap index 1fbbf700..953642fc 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap @@ -10,9 +10,13 @@ import { $fault, $domain, $raise, + $untyped, $W, $WK, $R, + $check, + $opt, + $slice, $encode, $decode, $Object, @@ -27,8 +31,8 @@ import { $Iterator, $cancellable, $impl, - $ret, $verify, + $setLive, KitchenSinkError as $Error, } from './runtime.js'; export { KitchenSinkError, CancelledError } from './runtime.js'; @@ -69,48 +73,78 @@ $raw.$setup($Fault); const $token = (t) => t; // The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). +// module's table function, as [id, hash, path]: the library's tables must +// hold each one (see $verify). const $contract = [ - ['shared', [ - [0x709e234caa962446n, 0x3894f2ca452e9f98n, 'shared.ping'], - [0xcfb306fa5801ee5bn, 0x1cbd9e10983cac2cn, 'shared.Token'], + ['kitchen_sink_shared_contract', [ + [0x709e234caa962446n, 0x3894f2ca452e9f98n, 'shared.ping'], // function ping() -> string + [0xcfb306fa5801ee5bn, 0x18e9fc204a13df6fn, 'shared.Token'], // record Token {i64, string} ]], - ['kitchen', [ - [0x01d4f09bbddc986fn, 0x29eb88fecc66544dn, 'kitchen.Gadget.new'], - [0x10472d8fe748c2een, 0xb265832b6a759fc4n, 'kitchen.maybe_gadget'], - [0x11368eec07a74078n, 0xfa049f78b77e146an, 'kitchen.gadgets'], - [0x137f08a29cde6021n, 0xda8de7aeb2dbb8a3n, 'kitchen.stream_gadgets'], - [0x2111b54980826dd7n, 0x452783f3e6ca6e1dn, 'kitchen.do_cancellable'], - [0x29dddbc60bf8d7dbn, 0x452a3e573d57f6dbn, 'kitchen.Priority'], - [0x2b227003b74a0d16n, 0x12332957470e9782n, 'kitchen.maybe_subscribe'], - [0x2dd66cb0064f0494n, 0x60197363fefd92ddn, 'kitchen.Item'], - [0x33c265bbf079b0e9n, 0xc3dcd0927e6c1debn, 'kitchen.u32_id'], - [0x33e6381d01660dfen, 0x077c63c31fd7f90an, 'kitchen.KitchenErrors'], - [0x3f5fc99d73504b76n, 0x2df88d710f1ea10en, 'kitchen.Gadget.poke'], - [0x4c5764cf109528d1n, 0x2a65529af4338a58n, 'kitchen.maybe_item'], - [0x4de17dfb93fd494dn, 0xba0ad0dd52ed74afn, 'kitchen.i32_id'], - [0x504e95b7f7e65570n, 0x01b3e8e0c9a10e23n, 'kitchen.stream_items'], - [0x68cdb9a4bd3e3dd5n, 0xc678b9164348d497n, 'kitchen.bool_id'], - [0x69e7cd703d16eb85n, 0xf3b28dd22ae9e689n, 'kitchen.do_async'], - [0x70865079ce41be82n, 0xbe3b69464672af46n, 'kitchen.Gadget.describe'], - [0x71c5a1116cb7c49an, 0xcba1837108a89b84n, 'kitchen.echo_bytes'], - [0x735ee1e0049ee91an, 0xca74320bc53eddabn, 'kitchen.echo_string'], - [0x76d7fc58f4e41d84n, 0x7373a0e47d30e690n, 'kitchen.i64_id'], - [0x79abfe1aa960fd6an, 0xbca7a9e5eb26f3a6n, 'kitchen.nested.hello'], - [0x7d3dd7d5cf6749e3n, 0xa192f5a7298adb39n, 'kitchen.new_op'], - [0x89a5e664bc25c67bn, 0x5764a95e81ecfd31n, 'kitchen.f64_id'], - [0x8cfdbdcecac5f7f1n, 0x07e272f09c41fffcn, 'kitchen.Gadget.version'], - [0x974b9f3e823c1738n, 0xb81b91e975695f27n, 'kitchen.cross_module_token'], - [0xaca0655fac15d230n, 0x11c4d2b28abe07bbn, 'kitchen.list_items'], - [0xc883b4afae20543en, 0xc40560e5411ff392n, 'kitchen.legacy_op'], - [0xf2f7753e6e1dffc2n, 0xc9047a95bdf49e93n, 'kitchen.get_attrs'], - [0xfae25d3391922eadn, 0x1c7f95739f3210b8n, 'kitchen.subscribe'], - [0xfd0059b05cb2fcfbn, 0x248bf8404a5823b4n, 'kitchen.Gadget'], - [0xfed9c551fb9fd2d6n, 0x59aec980ef3f3291n, 'kitchen.ReadyListener'], + ['kitchen_sink_kitchen_contract', [ + [0x01d4f09bbddc986fn, 0xffcf2bd3b04aa002n, 'kitchen.Gadget.new'], // constructor new(i64) -> Gadget + [0x02bbd5f31c953704n, 0x2cfa679ea1373e09n, 'kitchen.Sampler.samples'], // callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + [0x10472d8fe748c2een, 0xcc570a2649bbf7dcn, 'kitchen.maybe_gadget'], // function maybe_gadget(Gadget?) -> Gadget? + [0x11368eec07a74078n, 0xfa049f78b77e146an, 'kitchen.gadgets'], // function gadgets() -> [Gadget] + [0x137f08a29cde6021n, 0xda8de7aeb2dbb8a3n, 'kitchen.stream_gadgets'], // function stream_gadgets() -> iter + [0x184bfc4ab0e3fe45n, 0xb64587477022696bn, 'kitchen.ids_u64'], // function ids_u64([u64]) -> [u64] + [0x2111b54980826dd7n, 0x627bf01ab365b209n, 'kitchen.do_cancellable'], // function do_cancellable(string) -> string async cancellable + [0x21c70f9371374d74n, 0x25d5d88a30fe0a79n, 'kitchen.ReadyListener.favorite'], // callback_method #4 favorite() -> Gadget + [0x23de5bd597372f46n, 0xcf1bc6bb37872798n, 'kitchen.stream_chunks'], // function stream_chunks() -> iter<[i32]> + [0x249f1ddaec6a6cben, 0x0710d6c9a4de93dan, 'kitchen.take_from_pantry'], // function take_from_pantry(string) -> i32 throws PantryError + [0x29dddbc60bf8d7dbn, 0x452a3e573d57f6dbn, 'kitchen.Priority'], // enum Priority {Low = 0, Normal = 1, High = 2} + [0x2b227003b74a0d16n, 0x5dae036b8a6aed30n, 'kitchen.maybe_subscribe'], // function maybe_subscribe(ReadyListener?) -> bool + [0x2dd66cb0064f0494n, 0x74b131724dd0d867n, 'kitchen.Item'], // record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + [0x33c265bbf079b0e9n, 0x97c32b764693f2cbn, 'kitchen.u32_id'], // function u32_id(u32) -> u32 + [0x33e6381d01660dfen, 0x05e35a563089bf7dn, 'kitchen.KitchenErrors'], // errors KitchenErrors + [0x3d969ae76db6cb23n, 0x65a325cfc772296bn, 'kitchen.ReadyListener.latest'], // callback_method #3 latest() -> Item? + [0x3f5fc99d73504b76n, 0x417e4a2acd3210b3n, 'kitchen.Gadget.poke'], // method poke(i32) -> i32 throws KitchenErrors + [0x4082f0085db28781n, 0x18848e701962912en, 'kitchen.Sampler'], // callback Sampler + [0x4340a6b1e7ec8c50n, 0xa59d0f32a66dc7a2n, 'kitchen.Sampler.describe'], // callback_method #2 describe() -> string throws any + [0x4c5764cf109528d1n, 0xb374accf14676d90n, 'kitchen.maybe_item'], // function maybe_item(i64) -> Item? throws KitchenErrors + [0x4de17dfb93fd494dn, 0x78be3203bcd10727n, 'kitchen.i32_id'], // function i32_id(i32) -> i32 + [0x504e95b7f7e65570n, 0x01b3e8e0c9a10e23n, 'kitchen.stream_items'], // function stream_items() -> iter + [0x5dafabb7775fcb2dn, 0x6c4b78c7d3f858c1n, 'kitchen.PantryError.Spoiled'], // code Spoiled = 2 + [0x5e0a8f6be4b00600n, 0x18ea1a42036c96a0n, 'kitchen.Sampler.threshold'], // callback_method #0 threshold(u32?) -> f64? + [0x5ec0af1d4ecc6fedn, 0x6540bdb74246420cn, 'kitchen.async_samples'], // function async_samples(u32) -> [f32] async cancellable + [0x61a358b0571e0a4en, 0x11a24c4b50d11996n, 'kitchen.async_maybe_count'], // function async_maybe_count() -> i64? async + [0x68cdb9a4bd3e3dd5n, 0xcde022fe6bc3b1bfn, 'kitchen.bool_id'], // function bool_id(bool) -> bool + [0x69e7cd703d16eb85n, 0xb13cb8d39e702c79n, 'kitchen.do_async'], // function do_async(string) -> string async + [0x70865079ce41be82n, 0xbe3b69464672af46n, 'kitchen.Gadget.describe'], // method describe() -> string + [0x71c5a1116cb7c49an, 0x1506cfe6f548fac4n, 'kitchen.echo_bytes'], // function echo_bytes(bytes) -> bytes + [0x735ee1e0049ee91an, 0xa713dbd1be707752n, 'kitchen.echo_string'], // function echo_string(string) -> string + [0x76d7fc58f4e41d84n, 0x6bdb4ccea2841d5en, 'kitchen.i64_id'], // function i64_id(i64) -> i64 + [0x78d91fd617e162b8n, 0xd88e2753503b36dfn, 'kitchen.stream_maybe'], // function stream_maybe() -> iter + [0x79abfe1aa960fd6an, 0xbca7a9e5eb26f3a6n, 'kitchen.nested.hello'], // function hello() -> string + [0x7b3865b536da998cn, 0x1c4e79d60a20e74fn, 'kitchen.mixed_slices'], // function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + [0x7ba266dc26a58989n, 0x0f86001cac0c21e8n, 'kitchen.KitchenErrors.InvalidInput'], // code InvalidInput = 2 + [0x7d3dd7d5cf6749e3n, 0xa192f5a7298adb39n, 'kitchen.new_op'], // function new_op() -> i32 + [0x81ded69adee0abf2n, 0x8c71141a931906f6n, 'kitchen.scale_i32'], // function scale_i32([i32], i32) -> [i32] + [0x81fa72769330fed6n, 0x89681e305b55a4f4n, 'kitchen.ReadyListener.label'], // callback_method #2 label() -> string throws KitchenErrors + [0x89a5e664bc25c67bn, 0x357fd9b65ad879a9n, 'kitchen.f64_id'], // function f64_id(f64) -> f64 + [0x8cfdbdcecac5f7f1n, 0x07e272f09c41fffcn, 'kitchen.Gadget.version'], // static version() -> string + [0x8dc42397e47614b9n, 0xcddf6fede09b2e7fn, 'kitchen.opt_priority'], // function opt_priority(Priority?) -> Priority? + [0x92f060ef68961fc1n, 0x4af6ce5b4f9fa508n, 'kitchen.ReadyListener.on_item'], // callback_method #1 on_item(Item, Gadget) -> bool + [0x974b9f3e823c1738n, 0xb81b91e975695f27n, 'kitchen.cross_module_token'], // function cross_module_token() -> Token + [0x9f0f001aa85f97d5n, 0x55815247f895bd45n, 'kitchen.sum_f64'], // function sum_f64([f64]) -> f64 + [0xab2a29fd1ef5458bn, 0x471a34aa4c09ae2bn, 'kitchen.KitchenErrors.NotFound'], // code NotFound = 1 + [0xaca0655fac15d230n, 0x11c4d2b28abe07bbn, 'kitchen.list_items'], // function list_items() -> [Item] + [0xb57c8e6ae3fb4383n, 0x3f857851ab75366cn, 'kitchen.PantryError'], // errors PantryError + [0xbdc795b195edd428n, 0x36127462f8b0969fn, 'kitchen.opt_mixed'], // function opt_mixed(f64, f32?, i64?, string?) -> u16? + [0xbe4821ab3c340e71n, 0xe1d32e38b766e623n, 'kitchen.ReadyListener.on_ready'], // callback_method #0 on_ready(i32, string) -> void + [0xc2ad53f2a6a36323n, 0x071e29abee823f17n, 'kitchen.opt_i32_id'], // function opt_i32_id(i32?) -> i32? + [0xc7ab409554fd3d7bn, 0xfea12b6bc219fc46n, 'kitchen.parse_any'], // function parse_any(string) -> i64 throws any + [0xc82a7f2d85f99f4en, 0xa9bcb8bacedb036cn, 'kitchen.sample_with'], // function sample_with(Sampler, u32?) -> [i16] throws PantryError + [0xc883b4afae20543en, 0xc40560e5411ff392n, 'kitchen.legacy_op'], // function legacy_op() -> i32 + [0xd180bf646bff59e2n, 0xa4515a6d00d7c600n, 'kitchen.PantryError.OutOfStock'], // code OutOfStock = 1 {string} + [0xe6778df0f5c1b82bn, 0x38eaafdb350559e1n, 'kitchen.opt_bool_id'], // function opt_bool_id(bool?) -> bool? + [0xf2f7753e6e1dffc2n, 0xc9047a95bdf49e93n, 'kitchen.get_attrs'], // function get_attrs() -> {string:i32} + [0xfae25d3391922eadn, 0xcc034cf506cae182n, 'kitchen.subscribe'], // function subscribe(ReadyListener) -> i32 + [0xfd0059b05cb2fcfbn, 0x248bf8404a5823b4n, 'kitchen.Gadget'], // interface Gadget + [0xfed9c551fb9fd2d6n, 0xe1020573f084dc1dn, 'kitchen.ReadyListener'], // callback ReadyListener + [0xff3a99b52597dee2n, 0x14c4ecac97bdfc9bn, 'kitchen.Gadget.readings'], // method readings(u32?) -> [f64] throws any ]], ]; -$verify($raw, 'kitchen_sink', 'kitchen_sink', 4, $contract); +$verify($raw, 'kitchen_sink', 'kitchen_sink', 5, $contract); // The native cancel tokens behind `AbortSignal`s. const $tokens = { @@ -119,24 +153,17 @@ const $tokens = { destroy: (t) => $raw.kitchen_sink_cancel_token_destroy(t), }; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.kitchen_sink_debug_live(kind); -} +// The leak counters behind the package's `./debug` export. +$setLive((kind) => $raw.kitchen_sink_debug_live(kind)); -const kitchen$KitchenErrorsError = class KitchenErrorsError extends $Error {}; -const kitchen$NotFoundError = class NotFoundError extends kitchen$KitchenErrorsError { +const kitchen$KitchenError = class KitchenError extends $Error {}; +const kitchen$NotFoundError = class NotFoundError extends kitchen$KitchenError { static CODE = 1; constructor(message = 'Item not found') { super(1, message); } }; -const kitchen$InvalidInputError = class InvalidInputError extends kitchen$KitchenErrorsError { +const kitchen$InvalidInputError = class InvalidInputError extends kitchen$KitchenError { static CODE = 2; constructor(message = 'Invalid input') { super(2, message); @@ -148,11 +175,42 @@ const $codes$kitchen$KitchenErrors = new Map([ ]); const $payloads$kitchen$KitchenErrors = new Map(); function $from$kitchen$KitchenErrors(e) { - return $domain(e, $codes$kitchen$KitchenErrors, $payloads$kitchen$KitchenErrors); + return $domain(e, kitchen$KitchenError, $codes$kitchen$KitchenErrors, $payloads$kitchen$KitchenErrors); } const $fields$kitchen$KitchenErrors = new Map(); function $raise$kitchen$KitchenErrors(e) { - return $raise(e, kitchen$KitchenErrorsError, $codes$kitchen$KitchenErrors, $fields$kitchen$KitchenErrors); + return $raise(e, kitchen$KitchenError, $codes$kitchen$KitchenErrors, $fields$kitchen$KitchenErrors); +} + +const kitchen$PantryError = class PantryError extends $Error {}; +const kitchen$OutOfStockError = class OutOfStockError extends kitchen$PantryError { + static CODE = 1; + constructor(fields, message = 'Out of stock') { + super(1, message); + this.item = fields.item; + } +}; +const kitchen$SpoiledError = class SpoiledError extends kitchen$PantryError { + static CODE = 2; + constructor(message = 'Spoiled') { + super(2, message); + } +}; +const $codes$kitchen$PantryError = new Map([ + [1, kitchen$OutOfStockError], + [2, kitchen$SpoiledError], +]); +const $payloads$kitchen$PantryError = new Map([ + [1, (r) => ({ item: r.readString() })], +]); +function $from$kitchen$PantryError(e) { + return $domain(e, kitchen$PantryError, $codes$kitchen$PantryError, $payloads$kitchen$PantryError); +} +const $fields$kitchen$PantryError = new Map([ + [1, (w, e) => { w.writeString(e.item); }], +]); +function $raise$kitchen$PantryError(e) { + return $raise(e, kitchen$PantryError, $codes$kitchen$PantryError, $fields$kitchen$PantryError); } const kitchen$Priority = Object.freeze({ @@ -212,20 +270,6 @@ function $w$kitchen$Gadget(w, v) { function $r$kitchen$Gadget(r) { return $adopt(kitchen$Gadget, $token(r.readU64())); } -/** `[Gadget]` */ -function $w_list_Gadget(w, v) { - w.writeList(v, $w$kitchen$Gadget); -} -function $r_list_Gadget(r) { - return r.readList($r$kitchen$Gadget); -} -/** `[Item]` */ -function $w_list_Item(w, v) { - w.writeList(v, $w$kitchen$Item); -} -function $r_list_Item(r) { - return r.readList($r$kitchen$Item); -} /** `[string]` */ function $w_list_string(w, v) { w.writeList(v, $W.String); @@ -233,13 +277,6 @@ function $w_list_string(w, v) { function $r_list_string(r) { return r.readList($R.String); } -/** `{string:i32}` */ -function $w_map_string_i32(w, v) { - w.writeMap(v, $WK.String, $W.I32); -} -function $r_map_string_i32(r) { - return r.readMap($R.String, $R.I32); -} /** `{string:string}` */ function $w_map_string_string(w, v) { w.writeMap(v, $WK.String, $W.String); @@ -247,6 +284,13 @@ function $w_map_string_string(w, v) { function $r_map_string_string(r) { return r.readMap($R.String, $R.String); } +/** `i64?` */ +function $w_opt_i64(w, v) { + w.writeOpt(v, $W.I64); +} +function $r_opt_i64(r) { + return r.readOpt($R.I64); +} /** `Gadget?` */ function $w_opt_Gadget(w, v) { w.writeOpt(v, $w$kitchen$Gadget); @@ -254,6 +298,13 @@ function $w_opt_Gadget(w, v) { function $r_opt_Gadget(r) { return r.readOpt($r$kitchen$Gadget); } +/** `[Gadget]` */ +function $w_list_Gadget(w, v) { + w.writeList(v, $w$kitchen$Gadget); +} +function $r_list_Gadget(r) { + return r.readList($r$kitchen$Gadget); +} /** `Item?` */ function $w_opt_Item(w, v) { w.writeOpt(v, $w$kitchen$Item); @@ -261,12 +312,26 @@ function $w_opt_Item(w, v) { function $r_opt_Item(r) { return r.readOpt($r$kitchen$Item); } -/** `i64?` */ -function $w_opt_i64(w, v) { - w.writeOpt(v, $W.I64); +/** `[Item]` */ +function $w_list_Item(w, v) { + w.writeList(v, $w$kitchen$Item); } -function $r_opt_i64(r) { - return r.readOpt($R.I64); +function $r_list_Item(r) { + return r.readList($r$kitchen$Item); +} +/** `{string:i32}` */ +function $w_map_string_i32(w, v) { + w.writeMap(v, $WK.String, $W.I32); +} +function $r_map_string_i32(r) { + return r.readMap($R.String, $R.I32); +} +/** `string?` */ +function $w_opt_string(w, v) { + w.writeOpt(v, $W.String); +} +function $r_opt_string(r) { + return r.readOpt($R.String); } const shared$ping = function ping() { @@ -284,11 +349,11 @@ function $adapt$kitchen$ReadyListener(impl) { impl.onReady($a0, $a1); }, on_item($a0, $a1) { - return $ret.Bool(impl.onItem($decode($a0, $r$kitchen$Item), $adopt(kitchen$Gadget, $a1)), 'ReadyListener.onItem'); + return $check.Bool(impl.onItem($decode($a0, $r$kitchen$Item), $adopt(kitchen$Gadget, $a1)), 'the return value of ReadyListener.onItem'); }, label() { try { - return $ret.String(impl.label(), 'ReadyListener.label'); + return $check.String(impl.label(), 'the return value of ReadyListener.label'); } catch ($e) { throw $raise$kitchen$KitchenErrors($e); } @@ -302,6 +367,29 @@ function $adapt$kitchen$ReadyListener(impl) { }; } +function $adapt$kitchen$Sampler(impl) { + $impl(impl, 'Sampler'); + return { + threshold($a0) { + return $opt(impl.threshold($a0), $check.F64, 'the return value of Sampler.threshold'); + }, + samples($a0, $a1) { + try { + return $slice.I16(impl.samples($a0, Array.from($a1)), 'the return value of Sampler.samples'); + } catch ($e) { + throw $raise$kitchen$PantryError($e); + } + }, + describe() { + try { + return $check.String(impl.describe(), 'the return value of Sampler.describe'); + } catch ($e) { + throw $untyped($e); + } + }, + }; +} + const kitchen$Gadget = class Gadget extends $Object { constructor(id) { super(); @@ -337,6 +425,16 @@ const kitchen$Gadget = class Gadget extends $Object { $unlend(this); } } + readings(limit) { + const $self = $lend(this, kitchen$Gadget); + try { + return Array.from($raw.kitchen_sink_kitchen_Gadget_readings($self, limit)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } static version() { try { return $raw.kitchen_sink_kitchen_Gadget_version(); @@ -529,6 +627,138 @@ const kitchen$newOp = function newOp() { } }; +const kitchen$optI32Id = function optI32Id(x) { + try { + return $raw.kitchen_sink_kitchen_opt_i32_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$optBoolId = function optBoolId(x) { + try { + return $raw.kitchen_sink_kitchen_opt_bool_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$optPriority = function optPriority(p) { + try { + return $raw.kitchen_sink_kitchen_opt_priority(p); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$optMixed = function optMixed(base, scale, offset, label) { + try { + return $raw.kitchen_sink_kitchen_opt_mixed(base, scale, offset, $encode(label, $w_opt_string)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$sumF64 = function sumF64(xs) { + try { + return $raw.kitchen_sink_kitchen_sum_f64($slice.F64(xs, 'xs')); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$scaleI32 = function scaleI32(xs, factor) { + try { + return Array.from($raw.kitchen_sink_kitchen_scale_i32($slice.I32(xs, 'xs'), factor)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$idsU64 = function idsU64(ids) { + try { + return Array.from($raw.kitchen_sink_kitchen_ids_u64($slice.U64(ids, 'ids'))); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$mixedSlices = function mixedSlices(a, b, c, d, e, f) { + try { + return Array.from($raw.kitchen_sink_kitchen_mixed_slices($slice.I8(a, 'a'), $slice.I16(b, 'b'), $slice.U16(c, 'c'), $slice.U32(d, 'd'), $slice.I64(e, 'e'), $slice.F32(f, 'f'))); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$asyncMaybeCount = async function asyncMaybeCount() { + try { + return await $raw.kitchen_sink_kitchen_async_maybe_count(); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$asyncSamples = async function asyncSamples(n, $options) { + try { + return Array.from(await $cancellable($options?.signal, $tokens, ($t) => $raw.kitchen_sink_kitchen_async_samples(n, $t))); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$kitchen$stream_maybe = { + next: (h) => $raw.kitchen_sink_kitchen_StreamMaybeIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamMaybeIterator_destroy(h), + map: $fault, + convert: null, +}; +const kitchen$streamMaybe = function streamMaybe() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_maybe(), $it$kitchen$stream_maybe); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$kitchen$stream_chunks = { + next: (h) => $raw.kitchen_sink_kitchen_StreamChunksIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamChunksIterator_destroy(h), + map: $fault, + convert: (v) => Array.from(v), +}; +const kitchen$streamChunks = function streamChunks() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_chunks(), $it$kitchen$stream_chunks); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$sampleWith = function sampleWith(sampler, level) { + try { + return Array.from($raw.kitchen_sink_kitchen_sample_with($adapt$kitchen$Sampler(sampler), level)); + } catch ($e) { + throw $from$kitchen$PantryError($e); + } +}; + +const kitchen$takeFromPantry = function takeFromPantry(item) { + try { + return $raw.kitchen_sink_kitchen_take_from_pantry(item); + } catch ($e) { + throw $from$kitchen$PantryError($e); + } +}; + +const kitchen$parseAny = function parseAny(text) { + try { + return $raw.kitchen_sink_kitchen_parse_any(text); + } catch ($e) { + throw $fault($e); + } +}; + const kitchen$nested$hello = function hello() { try { return $raw.kitchen_sink_kitchen_nested_hello(); @@ -542,9 +772,12 @@ export const shared = Object.freeze({ }); export const kitchen = Object.freeze({ - KitchenErrorsError: kitchen$KitchenErrorsError, + KitchenError: kitchen$KitchenError, NotFoundError: kitchen$NotFoundError, InvalidInputError: kitchen$InvalidInputError, + PantryError: kitchen$PantryError, + OutOfStockError: kitchen$OutOfStockError, + SpoiledError: kitchen$SpoiledError, Priority: kitchen$Priority, Gadget: kitchen$Gadget, boolId: kitchen$boolId, @@ -568,6 +801,21 @@ export const kitchen = Object.freeze({ doCancellable: kitchen$doCancellable, legacyOp: kitchen$legacyOp, newOp: kitchen$newOp, + optI32Id: kitchen$optI32Id, + optBoolId: kitchen$optBoolId, + optPriority: kitchen$optPriority, + optMixed: kitchen$optMixed, + sumF64: kitchen$sumF64, + scaleI32: kitchen$scaleI32, + idsU64: kitchen$idsU64, + mixedSlices: kitchen$mixedSlices, + asyncMaybeCount: kitchen$asyncMaybeCount, + asyncSamples: kitchen$asyncSamples, + streamMaybe: kitchen$streamMaybe, + streamChunks: kitchen$streamChunks, + sampleWith: kitchen$sampleWith, + takeFromPantry: kitchen$takeFromPantry, + parseAny: kitchen$parseAny, nested: Object.freeze({ hello: kitchen$nested$hello, }), diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap index 29c1081a..c0eb3f3a 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap @@ -15,6 +15,7 @@ source: crates/weaveffi-cli/tests/snapshots.rs #endif #include #include +#include #include #include #include @@ -30,11 +31,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs * The N-API transport of the JavaScript bindings. Every C symbol of the * library is exported to `index.js` under its own name and called with the * raw convention the shared JavaScript layer documents: direct values as - * numbers, bigints, and booleans; strings as strings; bytes and value - * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint - * handles (null when absent); callback interfaces as adapter objects (null - * for an absent optional one). A failure throws an instance of the - * runtime's `$Fault` class. + * numbers, bigints, and booleans (integers range-checked); optional scalars + * as the value or null; typed arrays (numeric lists) as the matching + * TypedArray; strings as strings; bytes and value buffers as Uint8Arrays; + * objects, iterators, and cancel tokens as bigint handles (null when + * absent); callback interfaces as adapter objects (null for an absent + * optional one). A failure throws an instance of the runtime's `$Fault` + * class. */ typedef kitchen_sink_error js_error; @@ -223,6 +226,12 @@ JS_HELPER napi_value js_new_bool(napi_env env, bool v) { return out; } +JS_HELPER napi_value js_null(napi_env env) { + napi_value out; + napi_get_null(env, &out); + return out; +} + /* An object, iterator, or cancel token handle: the pointer as a bigint, or * null for NULL. */ JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { @@ -248,6 +257,26 @@ JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) return out; } +/* A typed array (numeric list) of `count` elements of `size` bytes: a + * TypedArray of `type` holding a copy. A zero-length run is never read. */ +JS_HELPER napi_value js_new_slice(napi_env env, napi_typedarray_type type, const void* ptr, + size_t count, size_t size) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, count * size, &data, &ab); + if (count > 0) memcpy(data, ptr, count * size); + napi_create_typedarray(env, type, count, ab, 0, &out); + return out; +} + +/* A returned typed array: copy it, then release the producer's run. */ +JS_HELPER napi_value js_take_slice(napi_env env, napi_typedarray_type type, const void* ptr, + size_t count, size_t size) { + napi_value out = js_new_slice(env, type, ptr, count, size); + if (ptr != NULL) kitchen_sink_free_bytes((uint8_t*)ptr, count * size); + return out; +} + /* A module's contract table as a BigUint64Array of `id, hash` pairs. */ JS_HELPER napi_value js_new_contract(napi_env env, const kitchen_sink_contract_entry* table, size_t len) { @@ -263,11 +292,13 @@ JS_HELPER napi_value js_new_contract(napi_env env, const kitchen_sink_contract_e return out; } -JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, - const uint8_t* payload, size_t payload_len) { +/* A `$Fault` for an error the library reported: its code, its message (a + * length-delimited UTF-8 run, NULL for none), and its payload. */ +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const uint8_t* message, + size_t message_len, const uint8_t* payload, size_t payload_len) { napi_value argv[3], ctor, out = NULL; napi_create_int32(env, code, &argv[0]); - napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + argv[1] = js_new_str(env, message, message != NULL ? message_len : 0); if (payload != NULL) { argv[2] = js_new_bytes(env, payload, payload_len); } else { @@ -283,7 +314,8 @@ JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, /* Throw the error a call reported, then release it. */ JS_HELPER napi_value js_throw(napi_env env, js_error* err) { - napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + napi_throw(env, js_fault(env, err->code, err->message_ptr, err->message_len, err->payload_ptr, + err->payload_len)); kitchen_sink_error_clear(err); return NULL; } @@ -295,59 +327,97 @@ JS_HELPER bool js_type_error(napi_env env, const char* expected) { return false; } -JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { - return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +JS_HELPER bool js_range_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_range_error(env, NULL, expected); + return false; } -JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { - return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +/* An integer argument of at most 32 bits: a number that is an integer in + * [lo, hi]. The range is checked on the double before any conversion, so + * nothing wraps or truncates, on every platform. */ +JS_HELPER bool js_arg_int(napi_env env, napi_value v, double lo, double hi, const char* expected, + double* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_number) return js_type_error(env, "expected a number"); + double d = 0; + napi_get_value_double(env, v, &d); + if (!(d >= lo && d <= hi) || d != floor(d)) return js_range_error(env, expected); + *out = d; + return true; } JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int8_t)t; + double d = 0; + if (!js_arg_int(env, v, -128.0, 127.0, "expected an integer in [-128, 127]", &d)) return false; + *out = (int8_t)d; return true; } JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int16_t)t; + double d = 0; + if (!js_arg_int(env, v, -32768.0, 32767.0, "expected an integer in [-32768, 32767]", &d)) { + return false; + } + *out = (int16_t)d; + return true; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + double d = 0; + if (!js_arg_int(env, v, -2147483648.0, 2147483647.0, + "expected an integer in [-2147483648, 2147483647]", &d)) { + return false; + } + *out = (int32_t)d; return true; } JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint8_t)t; + double d = 0; + if (!js_arg_int(env, v, 0.0, 255.0, "expected an integer in [0, 255]", &d)) return false; + *out = (uint8_t)d; return true; } JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint16_t)t; + double d = 0; + if (!js_arg_int(env, v, 0.0, 65535.0, "expected an integer in [0, 65535]", &d)) return false; + *out = (uint16_t)d; return true; } -/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + double d = 0; + if (!js_arg_int(env, v, 0.0, 4294967295.0, "expected an integer in [0, 4294967295]", &d)) { + return false; + } + *out = (uint32_t)d; + return true; +} + +/* 64-bit integers: a bigint that fits, or a number that is an integer in + * range. The double is range-checked before it is converted (converting + * an out-of-range double is undefined behavior in C); 2^63 and 2^64 are + * exact doubles, so the bounds are exclusive. */ JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { napi_valuetype t; napi_typeof(env, v, &t); if (t == napi_bigint) { bool lossless = false; napi_get_value_bigint_int64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return false; + return lossless || js_range_error(env, "expected a signed 64-bit integer"); } + if (t != napi_number) return js_type_error(env, "expected a bigint"); double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { - *out = (int64_t)d; - return true; + napi_get_value_double(env, v, &d); + if (!(d >= -9223372036854775808.0 && d < 9223372036854775808.0) || d != floor(d)) { + return js_range_error(env, "expected a signed 64-bit integer"); } - return js_type_error(env, "expected a bigint"); + *out = (int64_t)d; + return true; } JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { @@ -356,27 +426,42 @@ JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { if (t == napi_bigint) { bool lossless = false; napi_get_value_bigint_uint64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return false; + return lossless || js_range_error(env, "expected an unsigned 64-bit integer"); } + if (t != napi_number) return js_type_error(env, "expected a bigint"); double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && - d == (double)(uint64_t)d) { - *out = (uint64_t)d; - return true; + napi_get_value_double(env, v, &d); + if (!(d >= 0.0 && d < 18446744073709551616.0) || d != floor(d)) { + return js_range_error(env, "expected an unsigned 64-bit integer"); } - return js_type_error(env, "expected a bigint"); + *out = (uint64_t)d; + return true; } JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); } +/* f32: any number, rounded to the nearest float as Float32Array rounds it. + * Converting a double outside the float range is undefined behavior in C, + * so the overflow cases are explicit: at or past FLT_MAX plus half an ulp a + * value rounds to an infinity, and below that to FLT_MAX. */ JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + const double max = 3.4028234663852886e38; /* FLT_MAX */ + const double overflow = 3.4028235677973366e38; /* FLT_MAX + half an ulp */ double d = 0; if (!js_arg_f64(env, v, &d)) return false; - *out = (float)d; + if (d >= overflow) { + *out = HUGE_VALF; + } else if (d <= -overflow) { + *out = -HUGE_VALF; + } else if (d > max) { + *out = (float)max; + } else if (d < -max) { + *out = (float)-max; + } else { + *out = (float)d; + } return true; } @@ -384,6 +469,15 @@ JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); } +/* Whether an optional argument is present (neither null nor undefined), + * also stored in `*has`. */ +JS_HELPER bool js_present(napi_env env, napi_value v, bool* has) { + napi_valuetype t; + napi_typeof(env, v, &t); + *has = t != napi_null && t != napi_undefined; + return *has; +} + /* A handle argument; null or undefined is NULL when `nullable`. */ JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { napi_valuetype t; @@ -418,6 +512,24 @@ JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, siz return js_type_error(env, "expected a Uint8Array"); } +/* A borrowed typed array (numeric list) of `type`: its data and element + * count. */ +JS_HELPER bool js_arg_slice(napi_env env, napi_value v, napi_typedarray_type type, const void** ptr, + size_t* count) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type actual; + void* data = NULL; + napi_get_typedarray_info(env, v, &actual, count, &data, NULL, NULL); + if (actual == type) { + *ptr = data; + return true; + } + } + return js_type_error(env, "expected a typed array of the parameter's element type"); +} + /* A string argument encoded as UTF-8: short strings live in the inline * buffer, longer ones on the heap until js_str_free. */ typedef struct { @@ -451,6 +563,11 @@ JS_HELPER void js_str_free(js_str* s) { napi_value argv[(n) + 1]; \ napi_get_cb_info(env, info, &argc, argv, NULL, NULL) +/* Set `err` to `code` with a NUL-terminated message. */ +JS_HELPER void js_error_set(js_error* err, int32_t code, const char* message) { + kitchen_sink_error_set(err, code, (const uint8_t*)message, strlen(message)); +} + /* --------------------------------------------------------------------------- * Async calls: the completion may run on any thread, so it only records the * result and queues the settlement on the JavaScript thread through a @@ -467,9 +584,13 @@ typedef enum { JS_R_BOOL, JS_R_STR, JS_R_BYTES, - JS_R_HANDLE + JS_R_HANDLE, + JS_R_SLICE } js_kind; +/* One async call. The completion records its result here: the value (`v`, + * plus `len` for a run), whether an optional scalar is present (`opt` and + * `has`), and a typed array's type and element size. */ typedef struct { napi_deferred deferred; napi_threadsafe_function tsfn; @@ -483,6 +604,10 @@ typedef struct { const void* p; } v; size_t len; + bool opt; + bool has; + napi_typedarray_type slice_type; + size_t slice_size; } js_async; JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { @@ -491,8 +616,11 @@ JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* js_async* a = (js_async*)data; bool failed = a->err != NULL && a->err->code != 0; if (env != NULL && failed) { - napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, - a->err->payload_ptr, a->err->payload_len)); + napi_reject_deferred(env, a->deferred, + js_fault(env, a->err->code, a->err->message_ptr, a->err->message_len, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL && a->opt && !a->has) { + napi_resolve_deferred(env, a->deferred, js_null(env)); } else if (env != NULL) { napi_value v = NULL; switch (a->kind) { @@ -506,11 +634,16 @@ JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + case JS_R_SLICE: + v = js_take_slice(env, a->slice_type, a->v.p, a->len, a->slice_size); + break; } napi_resolve_deferred(env, a->deferred, v); } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { /* The environment is shutting down: just release the result. */ kitchen_sink_free_bytes((uint8_t*)a->v.p, a->len); + } else if (!failed && a->kind == JS_R_SLICE && a->v.p != NULL) { + kitchen_sink_free_bytes((uint8_t*)a->v.p, a->len * a->slice_size); } kitchen_sink_error_free(a->err); napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); @@ -660,7 +793,7 @@ JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, uv_mutex_unlock(&t->mu); free(req); js_thread_release(t); - kitchen_sink_error_set(out_err, -4, "the callback implementation is no longer reachable"); + js_error_set(out_err, -4, "the callback implementation is no longer reachable"); return; } const uint64_t limit = (uint64_t)JS_CB_DEADLOCK_MS * 1000000u; @@ -686,7 +819,7 @@ JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, "callback, so waiting longer would deadlock (call it asynchronously, or call " "back on the calling thread)", what, JS_CB_DEADLOCK_MS); - kitchen_sink_error_set(out_err, -4, msg); + js_error_set(out_err, -4, msg); return; } uv_cond_timedwait(&t->cv, &t->mu, limit - (now - since)); @@ -739,9 +872,11 @@ JS_HELPER char* js_cstring(napi_env env, napi_value v) { } /* Report the pending JavaScript exception (or `fallback`) through - * `out_err`; nothing unwinds through the C frame. A `$Fault` (a domain error - * a throwing method raised) reports its code, message, and payload; any - * other exception is a foreign failure, code -4, with its message. */ + * `out_err`; nothing unwinds through the C frame. A `$Fault` (the adapter's + * report of a method declared `throws`: a domain code with its payload, or + * -1) reports its code, message, and payload; any other exception (from a + * method that doesn't throw, or a return of the wrong type) is a callback + * failure, code -4, with its message. */ JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { int32_t code = -4; char* msg = NULL; @@ -777,7 +912,7 @@ JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallbac napi_is_exception_pending(env, &pending); if (pending) napi_get_and_clear_last_exception(env, &exc); } - kitchen_sink_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); + js_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); if (payload != NULL) kitchen_sink_error_set_payload(out_err, payload, payload_len); free(msg); } @@ -819,6 +954,21 @@ JS_HELPER bool js_ret_bytes(napi_env env, napi_value v, uint8_t** out_ptr, size_ return true; } +/* A callback's typed-array return: a TypedArray of `type`, copied into a + * {p}_alloc run of `count * size` bytes the producer adopts. `*run` and + * `*count` are the run and its element count (NULL and 0 when empty). */ +JS_HELPER bool js_ret_slice(napi_env env, napi_value v, napi_typedarray_type type, size_t size, + void** run, size_t* count) { + const void* data = NULL; + size_t n = 0; + if (!js_arg_slice(env, v, type, &data, &n)) return false; + uint8_t* out = kitchen_sink_alloc(n * size); + if (n > 0) memcpy(out, data, n * size); + *run = out; + *count = n; + return true; +} + /* --------------------------------------------------------------------------- * The runtime symbols every library exports. * ------------------------------------------------------------------------- */ @@ -882,45 +1032,45 @@ static napi_value nx_kitchen_sink_kitchen_contract(napi_env env, napi_callback_i } typedef struct { - js_error* out_err; - int32_t p0; - const uint8_t* p1; - size_t p2; + int32_t p_code; + const uint8_t* p_msg_ptr; + size_t p_msg_len; + kitchen_sink_error* p_out_err; } frame_kitchen_sink_kitchen_ReadyListener_on_ready; static void invoke_kitchen_sink_kitchen_ReadyListener_on_ready(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_ReadyListener_on_ready* f) { napi_handle_scope scope; napi_open_handle_scope(env, &scope); napi_value argv[3]; - argv[0] = js_new_i32(env, (int32_t)f->p0); - argv[1] = js_new_str(env, f->p1, f->p2); + argv[0] = js_new_i32(env, (int32_t)f->p_code); + argv[1] = js_new_str(env, f->p_msg_ptr, f->p_msg_len); napi_value result; if (!js_cb_call(env, cb, "on_ready", 2, argv, &result)) { - js_cb_report(env, f->out_err, "ReadyListener.onReady failed"); + js_cb_report(env, f->p_out_err, "ReadyListener.onReady failed"); } napi_close_handle_scope(env, scope); } -static void tramp_kitchen_sink_kitchen_ReadyListener_on_ready(void* ctx, int32_t p0, const uint8_t* p1, size_t p2, kitchen_sink_error* out_err) { +static void tramp_kitchen_sink_kitchen_ReadyListener_on_ready(void* ctx, int32_t p_code, const uint8_t* p_msg_ptr, size_t p_msg_len, kitchen_sink_error* p_out_err) { frame_kitchen_sink_kitchen_ReadyListener_on_ready f; memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - f.p1 = p1; - f.p2 = p2; + f.p_code = p_code; + f.p_msg_ptr = p_msg_ptr; + f.p_msg_len = p_msg_len; + f.p_out_err = p_out_err; js_cb* cb = (js_cb*)ctx; if (js_cb_on_js_thread(cb)) { invoke_kitchen_sink_kitchen_ReadyListener_on_ready(cb->env, cb, &f); } else { - js_cb_hop(cb, 0, &f, out_err, "ReadyListener.onReady"); + js_cb_hop(cb, 0, &f, f.p_out_err, "ReadyListener.onReady"); } } typedef struct { - js_error* out_err; - const uint8_t* p0; - size_t p1; - kitchen_sink_kitchen_Gadget* p2; + const uint8_t* p_item_ptr; + size_t p_item_len; + kitchen_sink_kitchen_Gadget* p_gadget; + kitchen_sink_error* p_out_err; bool result; } frame_kitchen_sink_kitchen_ReadyListener_on_item; @@ -928,42 +1078,42 @@ static void invoke_kitchen_sink_kitchen_ReadyListener_on_item(napi_env env, js_c napi_handle_scope scope; napi_open_handle_scope(env, &scope); napi_value argv[3]; - argv[0] = js_new_bytes(env, f->p0, f->p1); - argv[1] = js_new_handle(env, f->p2); + argv[0] = js_new_bytes(env, f->p_item_ptr, f->p_item_len); + argv[1] = js_new_handle(env, f->p_gadget); napi_value result; if (!js_cb_call(env, cb, "on_item", 2, argv, &result)) { - js_cb_report(env, f->out_err, "ReadyListener.onItem failed"); + js_cb_report(env, f->p_out_err, "ReadyListener.onItem failed"); } else { bool v = 0; if (js_arg_bool(env, result, &v)) { - f->result = (bool)v; + f->result = v; } else { - js_cb_report(env, f->out_err, "ReadyListener.onItem returned a value of the wrong type"); + js_cb_report(env, f->p_out_err, "ReadyListener.onItem returned a value of the wrong type"); } } napi_close_handle_scope(env, scope); } -static bool tramp_kitchen_sink_kitchen_ReadyListener_on_item(void* ctx, const uint8_t* p0, size_t p1, kitchen_sink_kitchen_Gadget* p2, kitchen_sink_error* out_err) { +static bool tramp_kitchen_sink_kitchen_ReadyListener_on_item(void* ctx, const uint8_t* p_item_ptr, size_t p_item_len, kitchen_sink_kitchen_Gadget* p_gadget, kitchen_sink_error* p_out_err) { frame_kitchen_sink_kitchen_ReadyListener_on_item f; memset(&f, 0, sizeof f); - f.out_err = out_err; - f.p0 = p0; - f.p1 = p1; - f.p2 = p2; + f.p_item_ptr = p_item_ptr; + f.p_item_len = p_item_len; + f.p_gadget = p_gadget; + f.p_out_err = p_out_err; js_cb* cb = (js_cb*)ctx; if (js_cb_on_js_thread(cb)) { invoke_kitchen_sink_kitchen_ReadyListener_on_item(cb->env, cb, &f); } else { - js_cb_hop(cb, 1, &f, out_err, "ReadyListener.onItem"); + js_cb_hop(cb, 1, &f, f.p_out_err, "ReadyListener.onItem"); } return f.result; } typedef struct { - js_error* out_err; - uint8_t** out_ptr; - size_t* out_len; + uint8_t** p_out_ptr; + size_t* p_out_len; + kitchen_sink_error* p_out_err; } frame_kitchen_sink_kitchen_ReadyListener_label; static void invoke_kitchen_sink_kitchen_ReadyListener_label(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_ReadyListener_label* f) { @@ -972,33 +1122,33 @@ static void invoke_kitchen_sink_kitchen_ReadyListener_label(napi_env env, js_cb* napi_value argv[1]; napi_value result; if (!js_cb_call(env, cb, "label", 0, argv, &result)) { - js_cb_report(env, f->out_err, "ReadyListener.label failed"); + js_cb_report(env, f->p_out_err, "ReadyListener.label failed"); } else { - if (!js_ret_str(env, result, f->out_ptr, f->out_len)) { - js_cb_report(env, f->out_err, "ReadyListener.label returned a value of the wrong type"); + if (!js_ret_str(env, result, f->p_out_ptr, f->p_out_len)) { + js_cb_report(env, f->p_out_err, "ReadyListener.label returned a value of the wrong type"); } } napi_close_handle_scope(env, scope); } -static void tramp_kitchen_sink_kitchen_ReadyListener_label(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { +static void tramp_kitchen_sink_kitchen_ReadyListener_label(void* ctx, uint8_t** p_out_ptr, size_t* p_out_len, kitchen_sink_error* p_out_err) { frame_kitchen_sink_kitchen_ReadyListener_label f; memset(&f, 0, sizeof f); - f.out_err = out_err; - f.out_ptr = out_ptr; - f.out_len = out_len; + f.p_out_ptr = p_out_ptr; + f.p_out_len = p_out_len; + f.p_out_err = p_out_err; js_cb* cb = (js_cb*)ctx; if (js_cb_on_js_thread(cb)) { invoke_kitchen_sink_kitchen_ReadyListener_label(cb->env, cb, &f); } else { - js_cb_hop(cb, 2, &f, out_err, "ReadyListener.label"); + js_cb_hop(cb, 2, &f, f.p_out_err, "ReadyListener.label"); } } typedef struct { - js_error* out_err; - uint8_t** out_ptr; - size_t* out_len; + uint8_t** p_out_ptr; + size_t* p_out_len; + kitchen_sink_error* p_out_err; } frame_kitchen_sink_kitchen_ReadyListener_latest; static void invoke_kitchen_sink_kitchen_ReadyListener_latest(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_ReadyListener_latest* f) { @@ -1007,31 +1157,31 @@ static void invoke_kitchen_sink_kitchen_ReadyListener_latest(napi_env env, js_cb napi_value argv[1]; napi_value result; if (!js_cb_call(env, cb, "latest", 0, argv, &result)) { - js_cb_report(env, f->out_err, "ReadyListener.latest failed"); + js_cb_report(env, f->p_out_err, "ReadyListener.latest failed"); } else { - if (!js_ret_bytes(env, result, f->out_ptr, f->out_len)) { - js_cb_report(env, f->out_err, "ReadyListener.latest returned a value of the wrong type"); + if (!js_ret_bytes(env, result, f->p_out_ptr, f->p_out_len)) { + js_cb_report(env, f->p_out_err, "ReadyListener.latest returned a value of the wrong type"); } } napi_close_handle_scope(env, scope); } -static void tramp_kitchen_sink_kitchen_ReadyListener_latest(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { +static void tramp_kitchen_sink_kitchen_ReadyListener_latest(void* ctx, uint8_t** p_out_ptr, size_t* p_out_len, kitchen_sink_error* p_out_err) { frame_kitchen_sink_kitchen_ReadyListener_latest f; memset(&f, 0, sizeof f); - f.out_err = out_err; - f.out_ptr = out_ptr; - f.out_len = out_len; + f.p_out_ptr = p_out_ptr; + f.p_out_len = p_out_len; + f.p_out_err = p_out_err; js_cb* cb = (js_cb*)ctx; if (js_cb_on_js_thread(cb)) { invoke_kitchen_sink_kitchen_ReadyListener_latest(cb->env, cb, &f); } else { - js_cb_hop(cb, 3, &f, out_err, "ReadyListener.latest"); + js_cb_hop(cb, 3, &f, f.p_out_err, "ReadyListener.latest"); } } typedef struct { - js_error* out_err; + kitchen_sink_error* p_out_err; kitchen_sink_kitchen_Gadget* result; } frame_kitchen_sink_kitchen_ReadyListener_favorite; @@ -1041,27 +1191,27 @@ static void invoke_kitchen_sink_kitchen_ReadyListener_favorite(napi_env env, js_ napi_value argv[1]; napi_value result; if (!js_cb_call(env, cb, "favorite", 0, argv, &result)) { - js_cb_report(env, f->out_err, "ReadyListener.favorite failed"); + js_cb_report(env, f->p_out_err, "ReadyListener.favorite failed"); } else { void* h = NULL; if (js_arg_handle(env, result, &h, false)) { f->result = (kitchen_sink_kitchen_Gadget*)h; } else { - js_cb_report(env, f->out_err, "ReadyListener.favorite returned a value of the wrong type"); + js_cb_report(env, f->p_out_err, "ReadyListener.favorite returned a value of the wrong type"); } } napi_close_handle_scope(env, scope); } -static kitchen_sink_kitchen_Gadget* tramp_kitchen_sink_kitchen_ReadyListener_favorite(void* ctx, kitchen_sink_error* out_err) { +static kitchen_sink_kitchen_Gadget* tramp_kitchen_sink_kitchen_ReadyListener_favorite(void* ctx, kitchen_sink_error* p_out_err) { frame_kitchen_sink_kitchen_ReadyListener_favorite f; memset(&f, 0, sizeof f); - f.out_err = out_err; + f.p_out_err = p_out_err; js_cb* cb = (js_cb*)ctx; if (js_cb_on_js_thread(cb)) { invoke_kitchen_sink_kitchen_ReadyListener_favorite(cb->env, cb, &f); } else { - js_cb_hop(cb, 4, &f, out_err, "ReadyListener.favorite"); + js_cb_hop(cb, 4, &f, f.p_out_err, "ReadyListener.favorite"); } return f.result; } @@ -1077,7 +1227,7 @@ static void dispatch_kitchen_sink_kitchen_ReadyListener(napi_env env, napi_value } if (!js_cb_take(req)) return; if (env == NULL) { - kitchen_sink_error_set(req->out_err, -4, "the JavaScript environment is shutting down"); + js_error_set(req->out_err, -4, "the JavaScript environment is shutting down"); } else { switch (req->method) { case 0: invoke_kitchen_sink_kitchen_ReadyListener_on_ready(env, req->cb, (frame_kitchen_sink_kitchen_ReadyListener_on_ready*)req->frame); break; @@ -1093,6 +1243,161 @@ static void dispatch_kitchen_sink_kitchen_ReadyListener(napi_env env, napi_value static const kitchen_sink_kitchen_ReadyListener_vtable vtable_kitchen_sink_kitchen_ReadyListener = {sizeof(kitchen_sink_kitchen_ReadyListener_vtable), 0, js_cb_free, tramp_kitchen_sink_kitchen_ReadyListener_on_ready, tramp_kitchen_sink_kitchen_ReadyListener_on_item, tramp_kitchen_sink_kitchen_ReadyListener_label, tramp_kitchen_sink_kitchen_ReadyListener_latest, tramp_kitchen_sink_kitchen_ReadyListener_favorite}; +typedef struct { + bool p_has_level; + uint32_t p_level; + double* p_out_value; + kitchen_sink_error* p_out_err; + bool result; +} frame_kitchen_sink_kitchen_Sampler_threshold; + +static void invoke_kitchen_sink_kitchen_Sampler_threshold(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_Sampler_threshold* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[2]; + argv[0] = f->p_has_level ? js_new_u32(env, (uint32_t)f->p_level) : js_null(env); + napi_value result; + if (!js_cb_call(env, cb, "threshold", 1, argv, &result)) { + js_cb_report(env, f->p_out_err, "Sampler.threshold failed"); + } else { + bool has = false; + double v = 0; + if (js_present(env, result, &has) && !js_arg_f64(env, result, &v)) { + js_cb_report(env, f->p_out_err, "Sampler.threshold returned a value of the wrong type"); + } else if (has) { + *f->p_out_value = v; + f->result = true; + } + } + napi_close_handle_scope(env, scope); +} + +static bool tramp_kitchen_sink_kitchen_Sampler_threshold(void* ctx, bool p_has_level, uint32_t p_level, double* p_out_value, kitchen_sink_error* p_out_err) { + frame_kitchen_sink_kitchen_Sampler_threshold f; + memset(&f, 0, sizeof f); + f.p_has_level = p_has_level; + f.p_level = p_level; + f.p_out_value = p_out_value; + f.p_out_err = p_out_err; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_kitchen_sink_kitchen_Sampler_threshold(cb->env, cb, &f); + } else { + js_cb_hop(cb, 0, &f, f.p_out_err, "Sampler.threshold"); + } + return f.result; +} + +typedef struct { + bool p_has_limit; + uint32_t p_limit; + const double* p_weights_ptr; + size_t p_weights_len; + int16_t** p_out_ptr; + size_t* p_out_len; + kitchen_sink_error* p_out_err; +} frame_kitchen_sink_kitchen_Sampler_samples; + +static void invoke_kitchen_sink_kitchen_Sampler_samples(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_Sampler_samples* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[3]; + argv[0] = f->p_has_limit ? js_new_u32(env, (uint32_t)f->p_limit) : js_null(env); + argv[1] = js_new_slice(env, napi_float64_array, f->p_weights_ptr, f->p_weights_len, sizeof(double)); + napi_value result; + if (!js_cb_call(env, cb, "samples", 2, argv, &result)) { + js_cb_report(env, f->p_out_err, "Sampler.samples failed"); + } else { + void* run = NULL; + size_t n = 0; + if (js_ret_slice(env, result, napi_int16_array, sizeof(int16_t), &run, &n)) { + *f->p_out_ptr = (int16_t*)run; + *f->p_out_len = n; + } else { + js_cb_report(env, f->p_out_err, "Sampler.samples returned a value of the wrong type"); + } + } + napi_close_handle_scope(env, scope); +} + +static void tramp_kitchen_sink_kitchen_Sampler_samples(void* ctx, bool p_has_limit, uint32_t p_limit, const double* p_weights_ptr, size_t p_weights_len, int16_t** p_out_ptr, size_t* p_out_len, kitchen_sink_error* p_out_err) { + frame_kitchen_sink_kitchen_Sampler_samples f; + memset(&f, 0, sizeof f); + f.p_has_limit = p_has_limit; + f.p_limit = p_limit; + f.p_weights_ptr = p_weights_ptr; + f.p_weights_len = p_weights_len; + f.p_out_ptr = p_out_ptr; + f.p_out_len = p_out_len; + f.p_out_err = p_out_err; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_kitchen_sink_kitchen_Sampler_samples(cb->env, cb, &f); + } else { + js_cb_hop(cb, 1, &f, f.p_out_err, "Sampler.samples"); + } +} + +typedef struct { + uint8_t** p_out_ptr; + size_t* p_out_len; + kitchen_sink_error* p_out_err; +} frame_kitchen_sink_kitchen_Sampler_describe; + +static void invoke_kitchen_sink_kitchen_Sampler_describe(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_Sampler_describe* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[1]; + napi_value result; + if (!js_cb_call(env, cb, "describe", 0, argv, &result)) { + js_cb_report(env, f->p_out_err, "Sampler.describe failed"); + } else { + if (!js_ret_str(env, result, f->p_out_ptr, f->p_out_len)) { + js_cb_report(env, f->p_out_err, "Sampler.describe returned a value of the wrong type"); + } + } + napi_close_handle_scope(env, scope); +} + +static void tramp_kitchen_sink_kitchen_Sampler_describe(void* ctx, uint8_t** p_out_ptr, size_t* p_out_len, kitchen_sink_error* p_out_err) { + frame_kitchen_sink_kitchen_Sampler_describe f; + memset(&f, 0, sizeof f); + f.p_out_ptr = p_out_ptr; + f.p_out_len = p_out_len; + f.p_out_err = p_out_err; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_kitchen_sink_kitchen_Sampler_describe(cb->env, cb, &f); + } else { + js_cb_hop(cb, 2, &f, f.p_out_err, "Sampler.describe"); + } +} + +static void dispatch_kitchen_sink_kitchen_Sampler(napi_env env, napi_value fn, void* context, void* data) { + (void)fn; + (void)context; + js_cb_req* req = (js_cb_req*)data; + if (req->method < 0) { + js_cb_release(env, req->cb); + free(req); + return; + } + if (!js_cb_take(req)) return; + if (env == NULL) { + js_error_set(req->out_err, -4, "the JavaScript environment is shutting down"); + } else { + switch (req->method) { + case 0: invoke_kitchen_sink_kitchen_Sampler_threshold(env, req->cb, (frame_kitchen_sink_kitchen_Sampler_threshold*)req->frame); break; + case 1: invoke_kitchen_sink_kitchen_Sampler_samples(env, req->cb, (frame_kitchen_sink_kitchen_Sampler_samples*)req->frame); break; + case 2: invoke_kitchen_sink_kitchen_Sampler_describe(env, req->cb, (frame_kitchen_sink_kitchen_Sampler_describe*)req->frame); break; + default: break; + } + } + js_cb_finish(req); +} + +static const kitchen_sink_kitchen_Sampler_vtable vtable_kitchen_sink_kitchen_Sampler = {sizeof(kitchen_sink_kitchen_Sampler_vtable), 0, js_cb_free, tramp_kitchen_sink_kitchen_Sampler_threshold, tramp_kitchen_sink_kitchen_Sampler_samples, tramp_kitchen_sink_kitchen_Sampler_describe}; + static napi_value nx_kitchen_sink_shared_ping(napi_env env, napi_callback_info info) { JS_ARGS(0); napi_value ret = NULL; @@ -1394,17 +1699,14 @@ static napi_value nx_kitchen_sink_kitchen_StreamGadgetsIterator_next(napi_env en JS_ARGS(1); void* h = NULL; if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - kitchen_sink_kitchen_Gadget* item; - memset(&item, 0, sizeof item); - size_t item_len = 0; + kitchen_sink_kitchen_Gadget* out_item = NULL; js_error err = {0}; js_sync_begin(); - int32_t has = kitchen_sink_kitchen_StreamGadgetsIterator_next((kitchen_sink_kitchen_StreamGadgetsIterator*)h, &item, &err); + int32_t has = kitchen_sink_kitchen_StreamGadgetsIterator_next((kitchen_sink_kitchen_StreamGadgetsIterator*)h, &out_item, &err); js_sync_end(); - (void)item_len; if (err.code != 0) return js_throw(env, &err); if (has == 0) return js_undefined(env); - return js_new_handle(env, item); + return js_new_handle(env, out_item); } static napi_value nx_kitchen_sink_kitchen_StreamGadgetsIterator_destroy(napi_env env, napi_callback_info info) { @@ -1502,16 +1804,15 @@ static napi_value nx_kitchen_sink_kitchen_StreamItemsIterator_next(napi_env env, JS_ARGS(1); void* h = NULL; if (!js_arg_handle(env, argv[0], &h, false)) return NULL; - const uint8_t* item; - memset(&item, 0, sizeof item); - size_t item_len = 0; + const uint8_t* out_item = NULL; + size_t out_len = 0; js_error err = {0}; js_sync_begin(); - int32_t has = kitchen_sink_kitchen_StreamItemsIterator_next((kitchen_sink_kitchen_StreamItemsIterator*)h, &item, &item_len, &err); + int32_t has = kitchen_sink_kitchen_StreamItemsIterator_next((kitchen_sink_kitchen_StreamItemsIterator*)h, &out_item, &out_len, &err); js_sync_end(); if (err.code != 0) return js_throw(env, &err); if (has == 0) return js_undefined(env); - return js_take_bytes(env, item, item_len); + return js_take_bytes(env, out_item, out_len); } static napi_value nx_kitchen_sink_kitchen_StreamItemsIterator_destroy(napi_env env, napi_callback_info info) { @@ -1632,6 +1933,422 @@ done: return ret; } +static napi_value nx_kitchen_sink_kitchen_opt_i32_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + bool a0_has = false; + int32_t a0 = 0; + if (js_present(env, argv[0], &a0_has) && !js_arg_i32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + int32_t out_value = 0; + js_sync_begin(); + bool r = kitchen_sink_kitchen_opt_i32_id(a0_has, a0, &out_value, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = r ? js_new_i32(env, (int32_t)out_value) : js_null(env); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_opt_bool_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + bool a0_has = false; + bool a0 = 0; + if (js_present(env, argv[0], &a0_has) && !js_arg_bool(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + bool out_value = false; + js_sync_begin(); + bool r = kitchen_sink_kitchen_opt_bool_id(a0_has, a0, &out_value, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = r ? js_new_bool(env, out_value) : js_null(env); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_opt_priority(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + bool a0_has = false; + kitchen_sink_kitchen_Priority a0 = 0; + if (js_present(env, argv[0], &a0_has) && !js_arg_i32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + kitchen_sink_kitchen_Priority out_value = 0; + js_sync_begin(); + bool r = kitchen_sink_kitchen_opt_priority(a0_has, a0, &out_value, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = r ? js_new_i32(env, (int32_t)out_value) : js_null(env); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_opt_mixed(napi_env env, napi_callback_info info) { + JS_ARGS(4); + napi_value ret = NULL; + double a0 = 0; + bool a1_has = false; + float a1 = 0; + bool a2_has = false; + int64_t a2 = 0; + const uint8_t* a3 = NULL; + size_t a3_len = 0; + if (!js_arg_f64(env, argv[0], &a0)) goto done; + if (js_present(env, argv[1], &a1_has) && !js_arg_f32(env, argv[1], &a1)) goto done; + if (js_present(env, argv[2], &a2_has) && !js_arg_i64(env, argv[2], &a2)) goto done; + if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; + { + js_error err = {0}; + uint16_t out_value = 0; + js_sync_begin(); + bool r = kitchen_sink_kitchen_opt_mixed(a0, a1_has, a1, a2_has, a2, a3, a3_len, &out_value, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = r ? js_new_u32(env, (uint32_t)out_value) : js_null(env); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_sum_f64(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const void* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_slice(env, argv[0], napi_float64_array, &a0, &a0_len)) goto done; + { + js_error err = {0}; + js_sync_begin(); + double r = kitchen_sink_kitchen_sum_f64((const double*)a0, a0_len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_f64(env, (double)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_scale_i32(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + const void* a0 = NULL; + size_t a0_len = 0; + int32_t a1 = 0; + if (!js_arg_slice(env, argv[0], napi_int32_array, &a0, &a0_len)) goto done; + if (!js_arg_i32(env, argv[1], &a1)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + js_sync_begin(); + int32_t* r = kitchen_sink_kitchen_scale_i32((const int32_t*)a0, a0_len, a1, &out_len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_slice(env, napi_int32_array, r, out_len, sizeof(int32_t)); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_ids_u64(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const void* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_slice(env, argv[0], napi_biguint64_array, &a0, &a0_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + js_sync_begin(); + uint64_t* r = kitchen_sink_kitchen_ids_u64((const uint64_t*)a0, a0_len, &out_len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_slice(env, napi_biguint64_array, r, out_len, sizeof(uint64_t)); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_mixed_slices(napi_env env, napi_callback_info info) { + JS_ARGS(6); + napi_value ret = NULL; + const void* a0 = NULL; + size_t a0_len = 0; + const void* a1 = NULL; + size_t a1_len = 0; + const void* a2 = NULL; + size_t a2_len = 0; + const void* a3 = NULL; + size_t a3_len = 0; + const void* a4 = NULL; + size_t a4_len = 0; + const void* a5 = NULL; + size_t a5_len = 0; + if (!js_arg_slice(env, argv[0], napi_int8_array, &a0, &a0_len)) goto done; + if (!js_arg_slice(env, argv[1], napi_int16_array, &a1, &a1_len)) goto done; + if (!js_arg_slice(env, argv[2], napi_uint16_array, &a2, &a2_len)) goto done; + if (!js_arg_slice(env, argv[3], napi_uint32_array, &a3, &a3_len)) goto done; + if (!js_arg_slice(env, argv[4], napi_bigint64_array, &a4, &a4_len)) goto done; + if (!js_arg_slice(env, argv[5], napi_float32_array, &a5, &a5_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + js_sync_begin(); + uint32_t* r = kitchen_sink_kitchen_mixed_slices((const int8_t*)a0, a0_len, (const int16_t*)a1, a1_len, (const uint16_t*)a2, a2_len, (const uint32_t*)a3, a3_len, (const int64_t*)a4, a4_len, (const float*)a5, a5_len, &out_len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_slice(env, napi_uint32_array, r, out_len, sizeof(uint32_t)); + } + goto done; +done: + return ret; +} + +static void done_kitchen_sink_kitchen_async_maybe_count(void* context, kitchen_sink_error* err, bool has_result, int64_t result) { + js_async* a = (js_async*)context; + a->opt = true; + a->has = has_result; + a->v.i = result; + js_async_done(a, err); +} + +static napi_value nx_kitchen_sink_kitchen_async_maybe_count(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_async* a = js_async_begin(env, JS_R_I64, "kitchen_sink_kitchen_async_maybe_count", &ret); + js_sync_begin(); + kitchen_sink_kitchen_async_maybe_count(done_kitchen_sink_kitchen_async_maybe_count, a); + js_sync_end(); + } + goto done; +done: + return ret; +} + +static void done_kitchen_sink_kitchen_async_samples(void* context, kitchen_sink_error* err, const float* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + a->slice_type = napi_float32_array; + a->slice_size = sizeof(float); + js_async_done(a, err); +} + +static napi_value nx_kitchen_sink_kitchen_async_samples(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + uint32_t a0 = 0; + void* token = NULL; + if (!js_arg_u32(env, argv[0], &a0)) goto done; + if (!js_arg_handle(env, argv[1], &token, true)) goto done; + { + js_async* a = js_async_begin(env, JS_R_SLICE, "kitchen_sink_kitchen_async_samples", &ret); + js_sync_begin(); + kitchen_sink_kitchen_async_samples(a0, (kitchen_sink_cancel_token*)token, done_kitchen_sink_kitchen_async_samples, a); + js_sync_end(); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_stream_maybe(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + js_sync_begin(); + kitchen_sink_kitchen_StreamMaybeIterator* r = kitchen_sink_kitchen_stream_maybe(&err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_StreamMaybeIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + bool out_has_item = false; + int32_t out_item = 0; + js_error err = {0}; + js_sync_begin(); + int32_t has = kitchen_sink_kitchen_StreamMaybeIterator_next((kitchen_sink_kitchen_StreamMaybeIterator*)h, &out_has_item, &out_item, &err); + js_sync_end(); + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return out_has_item ? js_new_i32(env, (int32_t)out_item) : js_null(env); +} + +static napi_value nx_kitchen_sink_kitchen_StreamMaybeIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + js_sync_begin(); + kitchen_sink_kitchen_StreamMaybeIterator_destroy((kitchen_sink_kitchen_StreamMaybeIterator*)h); + js_sync_end(); + return js_undefined(env); +} + +static napi_value nx_kitchen_sink_kitchen_stream_chunks(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + js_sync_begin(); + kitchen_sink_kitchen_StreamChunksIterator* r = kitchen_sink_kitchen_stream_chunks(&err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_StreamChunksIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + int32_t* out_item = NULL; + size_t out_len = 0; + js_error err = {0}; + js_sync_begin(); + int32_t has = kitchen_sink_kitchen_StreamChunksIterator_next((kitchen_sink_kitchen_StreamChunksIterator*)h, &out_item, &out_len, &err); + js_sync_end(); + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_take_slice(env, napi_int32_array, out_item, out_len, sizeof(int32_t)); +} + +static napi_value nx_kitchen_sink_kitchen_StreamChunksIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + js_sync_begin(); + kitchen_sink_kitchen_StreamChunksIterator_destroy((kitchen_sink_kitchen_StreamChunksIterator*)h); + js_sync_end(); + return js_undefined(env); +} + +static napi_value nx_kitchen_sink_kitchen_sample_with(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + js_cb* a0 = NULL; + bool a1_has = false; + uint32_t a1 = 0; + if (!js_arg_cb(env, argv[0], "kitchen_sink_kitchen_Sampler", dispatch_kitchen_sink_kitchen_Sampler, false, &a0)) goto done; + if (js_present(env, argv[1], &a1_has) && !js_arg_u32(env, argv[1], &a1)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + js_sync_begin(); + int16_t* r = kitchen_sink_kitchen_sample_with((void*)a0, &vtable_kitchen_sink_kitchen_Sampler, a1_has, a1, &out_len, &err); + js_sync_end(); + a0 = NULL; + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_slice(env, napi_int16_array, r, out_len, sizeof(int16_t)); + } + goto done; +done: + if (a0 != NULL) js_cb_release(env, a0); + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_take_from_pantry(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + if (!js_arg_str(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + js_sync_begin(); + int32_t r = kitchen_sink_kitchen_take_from_pantry(JS_STR_PTR(a0), a0.len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_parse_any(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + if (!js_arg_str(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + js_sync_begin(); + int64_t r = kitchen_sink_kitchen_parse_any(JS_STR_PTR(a0), a0.len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i64(env, r); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + static napi_value nx_kitchen_sink_kitchen_Gadget_new(napi_env env, napi_callback_info info) { JS_ARGS(1); napi_value ret = NULL; @@ -1698,6 +2415,31 @@ done: return ret; } +static napi_value nx_kitchen_sink_kitchen_Gadget_readings(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + void* self_h = NULL; + bool a0_has = false; + uint32_t a0 = 0; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + if (js_present(env, argv[1], &a0_has) && !js_arg_u32(env, argv[1], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + js_sync_begin(); + double* r = kitchen_sink_kitchen_Gadget_readings((const kitchen_sink_kitchen_Gadget*)self_h, a0_has, a0, &out_len, &err); + js_sync_end(); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_slice(env, napi_float64_array, r, out_len, sizeof(double)); + } + goto done; +done: + return ret; +} + static napi_value nx_kitchen_sink_kitchen_Gadget_version(napi_env env, napi_callback_info info) { JS_ARGS(0); napi_value ret = NULL; @@ -1772,9 +2514,29 @@ NAPI_MODULE_INIT() { {"kitchen_sink_kitchen_do_cancellable", NULL, nx_kitchen_sink_kitchen_do_cancellable, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_legacy_op", NULL, nx_kitchen_sink_kitchen_legacy_op, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_new_op", NULL, nx_kitchen_sink_kitchen_new_op, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_opt_i32_id", NULL, nx_kitchen_sink_kitchen_opt_i32_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_opt_bool_id", NULL, nx_kitchen_sink_kitchen_opt_bool_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_opt_priority", NULL, nx_kitchen_sink_kitchen_opt_priority, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_opt_mixed", NULL, nx_kitchen_sink_kitchen_opt_mixed, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_sum_f64", NULL, nx_kitchen_sink_kitchen_sum_f64, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_scale_i32", NULL, nx_kitchen_sink_kitchen_scale_i32, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_ids_u64", NULL, nx_kitchen_sink_kitchen_ids_u64, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_mixed_slices", NULL, nx_kitchen_sink_kitchen_mixed_slices, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_async_maybe_count", NULL, nx_kitchen_sink_kitchen_async_maybe_count, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_async_samples", NULL, nx_kitchen_sink_kitchen_async_samples, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_stream_maybe", NULL, nx_kitchen_sink_kitchen_stream_maybe, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamMaybeIterator_next", NULL, nx_kitchen_sink_kitchen_StreamMaybeIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamMaybeIterator_destroy", NULL, nx_kitchen_sink_kitchen_StreamMaybeIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_stream_chunks", NULL, nx_kitchen_sink_kitchen_stream_chunks, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamChunksIterator_next", NULL, nx_kitchen_sink_kitchen_StreamChunksIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamChunksIterator_destroy", NULL, nx_kitchen_sink_kitchen_StreamChunksIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_sample_with", NULL, nx_kitchen_sink_kitchen_sample_with, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_take_from_pantry", NULL, nx_kitchen_sink_kitchen_take_from_pantry, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_parse_any", NULL, nx_kitchen_sink_kitchen_parse_any, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_Gadget_new", NULL, nx_kitchen_sink_kitchen_Gadget_new, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_Gadget_describe", NULL, nx_kitchen_sink_kitchen_Gadget_describe, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_Gadget_poke", NULL, nx_kitchen_sink_kitchen_Gadget_poke, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_readings", NULL, nx_kitchen_sink_kitchen_Gadget_readings, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_Gadget_version", NULL, nx_kitchen_sink_kitchen_Gadget_version, NULL, NULL, NULL, napi_default, NULL}, {"kitchen_sink_kitchen_nested_hello", NULL, nx_kitchen_sink_kitchen_nested_hello, NULL, NULL, NULL, napi_default, NULL}, }; diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap deleted file mode 100644 index daa999c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate", - "name": "kitchen_sink", - "version": "0.1.0", - "description": "Native bindings for kitchen_sink", - "type": "module", - "main": "index.js", - "types": "index.d.ts", - "exports": { - ".": { - "types": "./index.d.ts", - "default": "./index.js" - } - }, - "files": [ - "index.js", - "index.d.ts", - "runtime.js", - "binding.gyp", - "kitchen_sink_node.c", - "kitchen_sink.h" - ], - "gypfile": true, - "scripts": { - "install": "node -e \"const p = process.platform + '-' + process.arch; try { require.resolve('kitchen_sink-' + p + '/kitchen_sink_node.node'); process.exit(0); } catch {} process.exit(require('fs').existsSync('prebuilds/' + p + '/kitchen_sink_node.node') ? 0 : 1)\" || node-gyp rebuild" - }, - "engines": { - "node": ">=18" - } -} diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__runtime_js.snap deleted file mode 100644 index 02cd16c0..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__runtime_js.snap +++ /dev/null @@ -1,636 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// The runtime shared by every binding in this package: the error classes, -// the value-buffer codec, the object and iterator wrappers, cancellation, and -// the load-time contract check. The generated `index.js` imports it; the -// native transport (an N-API addon or a WebAssembly instance) never sees it, -// except for `$Fault`, the shape every native failure arrives in. - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export class KitchenSinkError extends Error { - constructor(code, message) { - super(message === undefined ? '' : message); - this.name = new.target.name; - this.code = code; - } -} - -/** The error a cancelled async call rejects with (code -5). */ -export class CancelledError extends KitchenSinkError { - constructor(message = 'cancelled') { - super(-5, message); - } -} - -/** - * A failure as the native transport reports it, before the generated - * wrappers map it onto an error class. `payload` holds the matched error - * code's fields as a value buffer, or `null`. - */ -export class $Fault { - constructor(code, message, payload) { - this.code = code; - this.message = message; - this.payload = payload === undefined ? null : payload; - } -} - -/** - * Map an exception raised by a native call onto the public error classes: - * a cancelled call becomes `CancelledError`, any other fault the root error, - * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a - * code -2 error. Anything else (a `TypeError` from argument checking, say) - * passes through unchanged. - */ -export function $fault(e) { - if (e instanceof $Fault) { - return e.code === -5 - ? new CancelledError(e.message || undefined) - : new KitchenSinkError(e.code, e.message); - } - if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { - return new KitchenSinkError(-2, 'the native library trapped: ' + e.message); - } - return e; -} - -/** - * Map a fault onto an error domain: `codes` maps each declared code to its - * class, and `payloads` maps each code that carries fields to the reader - * that decodes them (the class's constructor then takes the fields first). - * Codes outside the domain fall back to `$fault`. - */ -export function $domain(e, codes, payloads) { - if (e instanceof $Fault) { - const cls = codes.get(e.code); - if (cls !== undefined) { - const message = e.message || undefined; - const read = payloads.get(e.code); - if (read === undefined) return new cls(message); - return new cls(e.payload === null ? {} : $decode(e.payload, read), message); - } - } - return $fault(e); -} - -/** - * The fault a callback implementation reports for exception `e` from a - * method that throws the error domain `domain`: a declared code of the - * domain travels with its fields (`fields` maps each code that has any to - * the writer that encodes them); anything else is rethrown unchanged and - * reported as a code -4 failure. - */ -export function $raise(e, domain, codes, fields) { - if (e instanceof domain && codes.has(e.code)) { - const write = fields.get(e.code); - return new $Fault(e.code, e.message, write === undefined ? null : $encode(e, write)); - } - return e; -} - -function $malformed(what) { - return new KitchenSinkError(-3, 'malformed value buffer: ' + what); -} - -function $expected(what, v) { - return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); -} - -function $bigint(v) { - if (typeof v === 'bigint') return v; - if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); - throw $expected('a bigint', v); -} - -const $utf8 = new TextEncoder(); -const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); - -/** - * Writes the value-buffer wire format: little-endian, packed, no alignment. - * Strings and bytes are a u32 length then the bytes, optionals a presence - * byte then the value, lists a u32 count then the elements, and maps a u32 - * count then alternating keys and values. - */ -export class $Writer { - constructor() { - this.buf = new Uint8Array(64); - this.view = new DataView(this.buf.buffer); - this.len = 0; - } - - reserve(n) { - if (this.len + n <= this.buf.length) return; - let cap = this.buf.length * 2; - while (cap < this.len + n) cap *= 2; - const grown = new Uint8Array(cap); - grown.set(this.buf.subarray(0, this.len)); - this.buf = grown; - this.view = new DataView(grown.buffer); - } - - num(v) { - if (typeof v !== 'number') throw $expected('a number', v); - return v; - } - - big(v, bits) { - const b = $bigint(v); - if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); - return b; - } - - writeBool(v) { - if (typeof v !== 'boolean') throw $expected('a boolean', v); - this.reserve(1); - this.buf[this.len++] = v ? 1 : 0; - } - - writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } - writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } - writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } - writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } - writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } - writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } - writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } - writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } - writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } - writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } - - writeString(v) { - if (typeof v !== 'string') throw $expected('a string', v); - // UTF-8 never needs more than three bytes per UTF-16 code unit. - this.reserve(4 + v.length * 3); - const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); - this.view.setUint32(this.len, written, true); - this.len += 4 + written; - } - - writeBytes(v) { - if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); - this.reserve(4 + v.length); - this.view.setUint32(this.len, v.length, true); - this.buf.set(v, this.len + 4); - this.len += 4 + v.length; - } - - writeOpt(v, write) { - if (v === null || v === undefined) { - this.writeBool(false); - } else { - this.writeBool(true); - write(this, v); - } - } - - writeList(v, write) { - if (!Array.isArray(v)) throw $expected('an array', v); - this.writeU32(v.length); - for (const e of v) write(this, e); - } - - writeMap(v, writeKey, writeValue) { - if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); - const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); - this.writeU32(entries.length); - for (const [k, e] of entries) { - writeKey(this, k); - writeValue(this, e); - } - } - - finish() { - return this.buf.subarray(0, this.len); - } -} - -/** - * Reads the value-buffer wire format. Every read is bounds-checked; a - * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or - * trailing bytes throw a code -3 error. - */ -export class $Reader { - constructor(bytes) { - this.buf = bytes; - this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - this.pos = 0; - } - - take(n) { - if (n > this.buf.length - this.pos) throw $malformed('truncated'); - const at = this.pos; - this.pos += n; - return at; - } - - readBool() { - const b = this.buf[this.take(1)]; - if (b > 1) throw $malformed('invalid bool byte ' + b); - return b === 1; - } - - readI8() { return this.view.getInt8(this.take(1)); } - readU8() { return this.view.getUint8(this.take(1)); } - readI16() { return this.view.getInt16(this.take(2), true); } - readU16() { return this.view.getUint16(this.take(2), true); } - readI32() { return this.view.getInt32(this.take(4), true); } - readU32() { return this.view.getUint32(this.take(4), true); } - readI64() { return this.view.getBigInt64(this.take(8), true); } - readU64() { return this.view.getBigUint64(this.take(8), true); } - readF32() { return this.view.getFloat32(this.take(4), true); } - readF64() { return this.view.getFloat64(this.take(8), true); } - - readString() { - const n = this.readU32(); - const at = this.take(n); - try { - return $utf8Strict.decode(this.buf.subarray(at, at + n)); - } catch { - throw $malformed('invalid UTF-8'); - } - } - - readBytes() { - const n = this.readU32(); - const at = this.take(n); - return this.buf.slice(at, at + n); - } - - readOpt(read) { - return this.readBool() ? read(this) : null; - } - - // A count may exceed the remaining bytes when elements encode to zero - // bytes (a record without fields), so it is not checked up front; a - // truncated element still fails on its first read. - readList(read) { - const n = this.readU32(); - const out = []; - for (let i = 0; i < n; i++) out.push(read(this)); - return out; - } - - readMap(readKey, readValue) { - const n = this.readU32(); - const out = {}; - for (let i = 0; i < n; i++) { - const k = readKey(this); - out[k] = readValue(this); - } - return out; - } - - end() { - if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); - } -} - -/** Encode `v` with `write` into a fresh value buffer. */ -export function $encode(v, write) { - const w = new $Writer(); - write(w, v); - return w.finish(); -} - -/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ -export function $decode(bytes, read) { - const r = new $Reader(bytes); - const v = read(r); - r.end(); - return v; -} - -/** Element writers for the fixed-width primitives, strings, and bytes. */ -export const $W = Object.freeze({ - Bool: (w, v) => w.writeBool(v), - I8: (w, v) => w.writeI8(v), - I16: (w, v) => w.writeI16(v), - I32: (w, v) => w.writeI32(v), - I64: (w, v) => w.writeI64(v), - U8: (w, v) => w.writeU8(v), - U16: (w, v) => w.writeU16(v), - U32: (w, v) => w.writeU32(v), - U64: (w, v) => w.writeU64(v), - F32: (w, v) => w.writeF32(v), - F64: (w, v) => w.writeF64(v), - String: (w, v) => w.writeString(v), - Bytes: (w, v) => w.writeBytes(v), -}); - -/** - * Map key writers. Plain-object keys are always strings, so numeric, - * `bigint`, and `bool` keys convert first (a `Map`'s typed keys pass - * through the same conversions unchanged). C-style enum keys use `I32`. - */ -export const $WK = Object.freeze({ - Bool: (w, k) => w.writeBool(k === true || k === 'true'), - I8: (w, k) => w.writeI8(Number(k)), - I16: (w, k) => w.writeI16(Number(k)), - I32: (w, k) => w.writeI32(Number(k)), - I64: (w, k) => w.writeI64(BigInt(k)), - U8: (w, k) => w.writeU8(Number(k)), - U16: (w, k) => w.writeU16(Number(k)), - U32: (w, k) => w.writeU32(Number(k)), - U64: (w, k) => w.writeU64(BigInt(k)), - F32: (w, k) => w.writeF32(Number(k)), - F64: (w, k) => w.writeF64(Number(k)), - String: (w, k) => w.writeString(k), - Bytes: (w, k) => w.writeBytes(k), -}); - -/** Element readers for the fixed-width primitives, strings, and bytes. */ -export const $R = Object.freeze({ - Bool: (r) => r.readBool(), - I8: (r) => r.readI8(), - I16: (r) => r.readI16(), - I32: (r) => r.readI32(), - I64: (r) => r.readI64(), - U8: (r) => r.readU8(), - U16: (r) => r.readU16(), - U32: (r) => r.readU32(), - U64: (r) => r.readU64(), - F32: (r) => r.readF32(), - F64: (r) => r.readF64(), - String: (r) => r.readString(), - Bytes: (r) => r.readBytes(), -}); - -/** The key `using` declarations call, or a stand-in where it is missing. */ -export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); - -// Releases the native reference of a wrapper collected without close(). -// The held value never references the wrapper, so it can't keep it alive. -const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); - -function $release(o) { - const handle = o.$h; - o.$h = null; - o.constructor.$destroy(handle); -} - -/** - * The base of every interface class. An instance owns one strong reference - * to a native object. `close()` (or a `using` declaration) releases it; - * a wrapper that is garbage collected unclosed is released by a - * `FinalizationRegistry`. A `close()` that races an in-flight call (from a - * callback, or while an async method is pending) is deferred until that - * call returns, so the native object is never freed mid-call. - */ -export class $Object { - close() { - if (this.$closed !== false) return; - this.$closed = true; - $objects.unregister(this); - if (this.$calls === 0) $release(this); - } - - [$dispose]() { - this.close(); - } -} - -/** Bind a native handle (one strong reference) to wrapper `o`. */ -export function $own(o, handle) { - o.$h = handle; - o.$calls = 0; - o.$closed = false; - $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); - return o; -} - -/** Wrap a handle the native side handed over in a new `cls` instance. */ -export function $adopt(cls, handle) { - return $own(Object.create(cls.prototype), handle); -} - -/** `$adopt`, with `null` for an absent object. */ -export function $adoptOpt(cls, handle) { - return handle === null ? null : $adopt(cls, handle); -} - -/** - * Lend wrapper `o` (an instance of `cls`) to one native call and return its - * handle. Every `$lend` is paired with an `$unlend` when the call returns. - */ -export function $lend(o, cls) { - if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); - if (o.$closed !== false) throw new KitchenSinkError(-3, `${cls.name} used after close()`); - o.$calls++; - return o.$h; -} - -/** `$lend`, with `null` for an absent object. */ -export function $lendOpt(o, cls) { - return o === null || o === undefined ? null : $lend(o, cls); -} - -/** End one loan of `o`, completing a `close()` deferred by the loan. */ -export function $unlend(o) { - if (o === null || o === undefined) return; - if (--o.$calls === 0 && o.$closed) $release(o); -} - -/** - * A second strong reference to `o`'s native object, for an object token - * written into a value buffer (the native side adopts it). - */ -export function $clone(o, cls) { - const handle = $lend(o, cls); - try { - return cls.$clone(handle); - } finally { - $unlend(o); - } -} - -/** `$clone`, with `null` for an absent object. */ -export function $cloneOpt(o, cls) { - return o === null || o === undefined ? null : $clone(o, cls); -} - -const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); - -/** - * A lazy iterator over a native `iter`: each `next()` makes exactly one - * native call. The native iterator is destroyed exactly once: when it is - * exhausted or fails, by `return()` (which `for...of` calls on `break`), by - * `close()` or a `using` declaration, or when the iterator is collected. - * `spec` supplies `next`, `destroy`, `map` (the error mapping), and - * `convert` (or `null` when elements need no conversion). - */ -export class $Iterator { - constructor(handle, spec) { - this.$h = handle; - this.$spec = spec; - $iterators.register(this, { handle, destroy: spec.destroy }, this); - } - - next() { - if (this.$h === null) return { done: true, value: undefined }; - const spec = this.$spec; - let v; - try { - v = spec.next(this.$h); - } catch (e) { - this.close(); - throw spec.map(e); - } - if (v === undefined) { - this.close(); - return { done: true, value: undefined }; - } - return { done: false, value: spec.convert === null ? v : spec.convert(v) }; - } - - return(value) { - this.close(); - return { done: true, value }; - } - - close() { - const handle = this.$h; - if (handle === null) return; - this.$h = null; - $iterators.unregister(this); - this.$spec.destroy(handle); - } - - [Symbol.iterator]() { - return this; - } - - [$dispose]() { - this.close(); - } -} - -/** - * Run a cancellable launch. Without a signal, `launch(null)` runs as is. - * With one, a native cancel token is created and passed to `launch`, - * aborting the signal cancels it (an already-aborted signal cancels it - * before the launch, so the call completes with `CancelledError` at once), - * and the token is destroyed once the call settles. `tokens` supplies the - * native `create`, `cancel`, and `destroy`. - */ -export function $cancellable(signal, tokens, launch) { - if (signal === undefined || signal === null) return launch(null); - if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); - const token = tokens.create(); - const abort = () => tokens.cancel(token); - const done = () => { - signal.removeEventListener('abort', abort); - tokens.destroy(token); - }; - if (signal.aborted) { - abort(); - } else { - signal.addEventListener('abort', abort, { once: true }); - } - let pending; - try { - pending = launch(token); - } catch (e) { - done(); - throw e; - } - return pending.finally(done); -} - -/** Check a callback interface implementation. */ -export function $impl(impl, what) { - if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { - throw $expected(`a ${what} implementation`, impl); - } - return impl; -} - -function $num(v, where) { - if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); - return v; -} - -function $retBigint(v, where) { - if (typeof v === 'bigint') return v; - if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); - throw new TypeError(`${where} must return a bigint`); -} - -/** - * Checks for a callback method's return value, by wire primitive (plus - * `String` and `Bytes`). A value of the wrong type throws, which reports - * the call as a code -4 failure. - */ -export const $ret = Object.freeze({ - Bool(v, where) { - if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); - return v; - }, - I8: $num, - I16: $num, - I32: $num, - U8: $num, - U16: $num, - U32: $num, - F32: $num, - F64: $num, - I64: $retBigint, - U64: $retBigint, - String(v, where) { - if (typeof v !== 'string') throw new TypeError(`${where} must return a string`); - return v; - }, - Bytes(v, where) { - if (!(v instanceof Uint8Array)) throw new TypeError(`${where} must return a Uint8Array`); - return v; - }, -}); - -/** - * The load-time checks: the native library must implement ABI revision - * `abi`, and, for every top-level module, its contract table must hold each - * entry these bindings were generated with. `contract` lists - * `[module, [[id, hash, path], ...]]` per module; the native side returns - * a module's table as a `BigUint64Array` of `id, hash` pairs. Entries the - * library has and the bindings don't are fine. - */ -export function $verify(raw, library, prefix, abi, contract) { - const version = raw[`${prefix}_abi_version`]; - if (typeof version !== 'function') { - throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); - } - const found = Number(version()); - if (found !== abi) { - throw new Error( - `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, - ); - } - for (const [module, expected] of contract) { - const table = raw[`${prefix}_${module}_contract`]; - if (typeof table !== 'function') { - throw new Error(`${library}: the native library does not export ${prefix}_${module}_contract`); - } - const pairs = table(); - const hashes = new Map(); - for (let i = 0; i + 1 < pairs.length; i += 2) hashes.set(pairs[i], pairs[i + 1]); - for (const [id, hash, path] of expected) { - const got = hashes.get(id); - if (got === undefined) { - throw new Error(`${library}: ${path} is missing from the library`); - } - if (got !== hash) { - throw new Error(`${library}: ${path} changed since these bindings were generated`); - } - } - } -} - -// END runtime.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_d_ts.snap deleted file mode 100644 index f957aa40..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_d_ts.snap +++ /dev/null @@ -1,84 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class NestedModulesError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends NestedModulesError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof NestedModulesError; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -export declare namespace graphics { - /** The errors of the `graphics` error domain. */ - export class GraphicsError extends $Error { - constructor(code: number, message?: string); - } - /** coordinate out of bounds */ - export class OutOfBoundsError extends graphics.GraphicsError { - static readonly CODE: 1; - constructor(message?: string); - } - /** A measurement unit shared with nested submodules. */ - export enum Unit { - pixels = 0, - points = 1, - } - /** Top-level struct that references a struct defined one level deeper. */ - export interface Canvas { - width: number; - /** Reference down into a nested module */ - root: graphics.shapes.Shape; - } - /** Construct a Canvas. */ - export function newCanvas(width: number): graphics.Canvas; - export namespace shapes { - /** A struct one level deep that references both an ancestor enum and a deeper struct. */ - export interface Shape { - sides: number; - /** Reference up to the parent module's enum */ - unit: graphics.Unit; - /** Reference down into a deeper module */ - detail: graphics.shapes.geometry.Detail; - } - /** Construct a Shape. */ - export function makeShape(sides: number): graphics.shapes.Shape; - /** Nested function returning a struct owned by an ancestor module. */ - export function describe(): graphics.Canvas; - export namespace geometry { - /** A struct two levels deep. */ - export interface Detail { - area: number; - } - /** - * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - * @throws {graphics.GraphicsError} - */ - export function compute(shape: graphics.shapes.Shape): graphics.shapes.geometry.Detail; - } - } -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap deleted file mode 100644 index 4d89074c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap +++ /dev/null @@ -1,198 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - NestedModulesError as $Error, -} from './runtime.js'; -export { NestedModulesError, CancelledError } from './runtime.js'; - -import { createRequire } from 'node:module'; - -// The native entry points: the N-API addon built from nested_modules_node.c. A prebuilt -// copy comes first (from the `nested_modules--` package `npm install` -// picks for this platform, or under prebuilds/-/), then the one -// node-gyp compiles at install time when none matches. -function $loadAddon(name) { - const require = createRequire(import.meta.url); - const platform = `${process.platform}-${process.arch}`; - const candidates = [ - `nested_modules-${platform}/${name}`, - `./prebuilds/${platform}/${name}`, - `./build/Release/${name}`, - `./build/Debug/${name}`, - ]; - for (const candidate of candidates) { - try { - return require(candidate); - } catch (e) { - if (e.code !== 'MODULE_NOT_FOUND') throw e; - } - } - throw new Error( - `the native addon ${name} was not found (tried ${candidates.join(', ')}); ` + - 'install the package for this platform, or build the addon with `npm install` ' + - 'or `node-gyp rebuild` in this package', - ); -} - -const $raw = $loadAddon('nested_modules_node.node'); -$raw.$setup($Fault); - -// Object tokens in value buffers are already bigint handles here. -const $token = (t) => t; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['graphics', [ - [0x2c29eef9327a6bd4n, 0x12983d5271651389n, 'graphics.Unit'], - [0x304088bbc28f0015n, 0x97e6e809913be6b6n, 'graphics.new_canvas'], - [0x49f8837532df9ac6n, 0x32e5b27e81ee0957n, 'graphics.Canvas'], - [0x55717fa027336b47n, 0xb2bd3a6bdcce48c8n, 'graphics.shapes.describe'], - [0x7626b91b8f1f53cdn, 0x867bbf7cf0f41c74n, 'graphics.shapes.geometry.compute'], - [0xb3efcb50d9a61ab7n, 0x3cb0207dfad2a149n, 'graphics.GraphicsError'], - [0xc62b16a9591b744bn, 0x7895610c8487eb1bn, 'graphics.shapes.Shape'], - [0xe13001f4983ccd35n, 0x16e7de070b4a4cfcn, 'graphics.shapes.geometry.Detail'], - [0xf3f658bcfc5dba62n, 0x2665ccb47356e746n, 'graphics.shapes.make_shape'], - ]], -]; -$verify($raw, 'nested_modules', 'nested_modules', 4, $contract); - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.nested_modules_debug_live(kind); -} - -const graphics$GraphicsError = class GraphicsError extends $Error {}; -const graphics$OutOfBoundsError = class OutOfBoundsError extends graphics$GraphicsError { - static CODE = 1; - constructor(message = 'coordinate out of bounds') { - super(1, message); - } -}; -const $codes$graphics$GraphicsError = new Map([ - [1, graphics$OutOfBoundsError], -]); -const $payloads$graphics$GraphicsError = new Map(); -function $from$graphics$GraphicsError(e) { - return $domain(e, $codes$graphics$GraphicsError, $payloads$graphics$GraphicsError); -} - -const graphics$Unit = Object.freeze({ - pixels: 0, - points: 1, - 0: 'pixels', - 1: 'points', -}); - -function $w$graphics$Canvas(w, v) { - w.writeI32(v.width); - $w$graphics$shapes$Shape(w, v.root); -} -function $r$graphics$Canvas(r) { - return { - width: r.readI32(), - root: $r$graphics$shapes$Shape(r), - }; -} -function $w$graphics$shapes$Shape(w, v) { - w.writeI32(v.sides); - w.writeI32(v.unit); - $w$graphics$shapes$geometry$Detail(w, v.detail); -} -function $r$graphics$shapes$Shape(r) { - return { - sides: r.readI32(), - unit: r.readI32(), - detail: $r$graphics$shapes$geometry$Detail(r), - }; -} -function $w$graphics$shapes$geometry$Detail(w, v) { - w.writeF64(v.area); -} -function $r$graphics$shapes$geometry$Detail(r) { - return { - area: r.readF64(), - }; -} - -const graphics$newCanvas = function newCanvas(width) { - try { - return $decode($raw.nested_modules_graphics_new_canvas(width), $r$graphics$Canvas); - } catch ($e) { - throw $fault($e); - } -}; - -const graphics$shapes$makeShape = function makeShape(sides) { - try { - return $decode($raw.nested_modules_graphics_shapes_make_shape(sides), $r$graphics$shapes$Shape); - } catch ($e) { - throw $fault($e); - } -}; - -const graphics$shapes$describe = function describe() { - try { - return $decode($raw.nested_modules_graphics_shapes_describe(), $r$graphics$Canvas); - } catch ($e) { - throw $fault($e); - } -}; - -const graphics$shapes$geometry$compute = function compute(shape) { - try { - return $decode($raw.nested_modules_graphics_shapes_geometry_compute($encode(shape, $w$graphics$shapes$Shape)), $r$graphics$shapes$geometry$Detail); - } catch ($e) { - throw $from$graphics$GraphicsError($e); - } -}; - -export const graphics = Object.freeze({ - GraphicsError: graphics$GraphicsError, - OutOfBoundsError: graphics$OutOfBoundsError, - Unit: graphics$Unit, - newCanvas: graphics$newCanvas, - shapes: Object.freeze({ - makeShape: graphics$shapes$makeShape, - describe: graphics$shapes$describe, - geometry: Object.freeze({ - compute: graphics$shapes$geometry$compute, - }), - }), -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_node_c.snap deleted file mode 100644 index b20b1f76..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_node_c.snap +++ /dev/null @@ -1,978 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/* libuv's Unix headers use POSIX types (pthread_rwlock_t, struct addrinfo) - * that glibc hides under a strict -std=c11; ask for them explicitly. */ -#if defined(__linux__) && !defined(_GNU_SOURCE) -#define _GNU_SOURCE -#endif -#ifndef NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include -#include -#include -#include -#include -#include - -/* The addon wraps deprecated functions too; the JavaScript API carries the - * deprecation instead. */ -#define NESTED_MODULES_DEPRECATED(msg) -#include "nested_modules.h" - -/* - * The N-API transport of the JavaScript bindings. Every C symbol of the - * library is exported to `index.js` under its own name and called with the - * raw convention the shared JavaScript layer documents: direct values as - * numbers, bigints, and booleans; strings as strings; bytes and value - * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint - * handles (null when absent); callback interfaces as adapter objects (null - * for an absent optional one). A failure throws an instance of the - * runtime's `$Fault` class. - */ - -typedef nested_modules_error js_error; - -/* Helpers an API may not need. */ -#if defined(__GNUC__) || defined(__clang__) -#define JS_HELPER static inline __attribute__((unused)) -#define JS_TLS __thread -#else -#define JS_HELPER static inline -#define JS_TLS __declspec(thread) -#endif - -/* --------------------------------------------------------------------------- - * JavaScript threads. One `js_thread` per thread that runs JavaScript and - * loaded the addon (the main thread and each worker). It counts the - * synchronous calls into the library in progress on that thread, which a - * callback made on another thread consults while it waits for the - * JavaScript thread (see js_cb_hop). Each environment and each callback - * registration holds a reference. - * ------------------------------------------------------------------------- */ - -typedef struct { - uv_mutex_t mu; - uv_cond_t cv; /* broadcast when a call begins or ends, or a hop finishes */ - unsigned depth; /* synchronous calls into the library in progress */ - uint64_t calls; /* outermost synchronous calls begun so far */ - unsigned waiters; /* producer threads waiting in js_cb_hop */ - unsigned refs; -} js_thread; - -/* The state of the JavaScript thread this is, or NULL on any other thread. */ -static JS_TLS js_thread* js_current = NULL; - -static js_thread* js_thread_new(void) { - js_thread* t = (js_thread*)calloc(1, sizeof *t); - uv_mutex_init(&t->mu); - uv_cond_init(&t->cv); - t->refs = 1; - return t; -} - -static void js_thread_retain(js_thread* t) { - uv_mutex_lock(&t->mu); - t->refs++; - uv_mutex_unlock(&t->mu); -} - -static void js_thread_release(js_thread* t) { - uv_mutex_lock(&t->mu); - bool last = --t->refs == 0; - uv_mutex_unlock(&t->mu); - if (!last) return; - uv_cond_destroy(&t->cv); - uv_mutex_destroy(&t->mu); - free(t); -} - -/* Bracket every synchronous call into the library. */ -JS_HELPER void js_sync_begin(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - if (t->depth++ == 0) t->calls++; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -JS_HELPER void js_sync_end(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - t->depth--; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* Per-environment state (one per main thread or worker): the `$Fault` - * constructor `index.js` registers through `$setup`, and the thread. */ -typedef struct { - napi_ref fault; - js_thread* thread; -} js_env; - -static void js_env_free(napi_env env, void* data, void* hint) { - (void)hint; - js_env* e = (js_env*)data; - if (e->fault != NULL) napi_delete_reference(env, e->fault); - if (js_current == e->thread) js_current = NULL; - js_thread_release(e->thread); - free(e); -} - -/* Set up the environment of the thread loading the addon. */ -static void js_env_init(napi_env env) { - js_env* e = (js_env*)calloc(1, sizeof *e); - e->thread = js_thread_new(); - js_current = e->thread; - napi_set_instance_data(env, e, js_env_free, NULL); -} - -static js_env* js_env_of(napi_env env) { - js_env* e = NULL; - napi_get_instance_data(env, (void**)&e); - return e; -} - -JS_HELPER napi_value js_undefined(napi_env env) { - napi_value v; - napi_get_undefined(env, &v); - return v; -} - -static napi_value js_setup(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value argv[1]; - napi_get_cb_info(env, info, &argc, argv, NULL, NULL); - js_env* e = js_env_of(env); - if (e->fault != NULL) napi_delete_reference(env, e->fault); - napi_create_reference(env, argv[0], 1, &e->fault); - return js_undefined(env); -} - -/* The `$Fault` constructor, or NULL before `$setup`. */ -static napi_value js_fault_class(napi_env env) { - js_env* e = js_env_of(env); - napi_value ctor = NULL; - if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok) { - return NULL; - } - return ctor; -} - -/* --------------------------------------------------------------------------- - * Value conversions - * ------------------------------------------------------------------------- */ - -JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len, &data, &ab); - if (len > 0) memcpy(data, ptr, len); - napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out; - napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); - return out; -} - -JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { - napi_value out; - napi_create_int32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { - napi_value out; - napi_create_uint32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { - napi_value out; - napi_create_bigint_int64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { - napi_value out; - napi_create_bigint_uint64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_f64(napi_env env, double v) { - napi_value out; - napi_create_double(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_bool(napi_env env, bool v) { - napi_value out; - napi_get_boolean(env, v, &out); - return out; -} - -/* An object, iterator, or cancel token handle: the pointer as a bigint, or - * null for NULL. */ -JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { - napi_value out; - if (p == NULL) { - napi_get_null(env, &out); - } else { - napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); - } - return out; -} - -/* Returned strings and buffers: convert, then release the producer's copy. */ -JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_str(env, ptr, len); - if (ptr != NULL) nested_modules_free_bytes((uint8_t*)ptr, len); - return out; -} - -JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_bytes(env, ptr, len); - if (ptr != NULL) nested_modules_free_bytes((uint8_t*)ptr, len); - return out; -} - -/* A module's contract table as a BigUint64Array of `id, hash` pairs. */ -JS_HELPER napi_value js_new_contract(napi_env env, const nested_modules_contract_entry* table, - size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len * 2 * sizeof(uint64_t), &data, &ab); - uint64_t* words = (uint64_t*)data; - for (size_t i = 0; i < len; i++) { - words[2 * i] = table[i].id; - words[2 * i + 1] = table[i].hash; - } - napi_create_typedarray(env, napi_biguint64_array, len * 2, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, - const uint8_t* payload, size_t payload_len) { - napi_value argv[3], ctor, out = NULL; - napi_create_int32(env, code, &argv[0]); - napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); - if (payload != NULL) { - argv[2] = js_new_bytes(env, payload, payload_len); - } else { - napi_get_null(env, &argv[2]); - } - ctor = js_fault_class(env); - if (ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { - napi_create_error(env, NULL, argv[1], &out); - napi_set_named_property(env, out, "code", argv[0]); - } - return out; -} - -/* Throw the error a call reported, then release it. */ -JS_HELPER napi_value js_throw(napi_env env, js_error* err) { - napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); - nested_modules_error_clear(err); - return NULL; -} - -JS_HELPER bool js_type_error(napi_env env, const char* expected) { - bool pending = false; - napi_is_exception_pending(env, &pending); - if (!pending) napi_throw_type_error(env, NULL, expected); - return false; -} - -JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { - return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { - return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int8_t)t; - return true; -} - -JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int16_t)t; - return true; -} - -JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint8_t)t; - return true; -} - -JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint16_t)t; - return true; -} - -/* 64-bit integers: a bigint that fits, or an integral number. */ -JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_int64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { - *out = (int64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_uint64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && - d == (double)(uint64_t)d) { - *out = (uint64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { - return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { - double d = 0; - if (!js_arg_f64(env, v, &d)) return false; - *out = (float)d; - return true; -} - -JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { - return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); -} - -/* A handle argument; null or undefined is NULL when `nullable`. */ -JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return nullable || js_type_error(env, "expected an object handle"); - } - uint64_t raw = 0; - bool lossless = false; - if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || - raw == 0) { - return js_type_error(env, "expected an object handle"); - } - *out = (void*)(uintptr_t)raw; - return true; -} - -/* A borrowed byte run: a Uint8Array (or Buffer). */ -JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { - bool is_typed = false; - napi_is_typedarray(env, v, &is_typed); - if (is_typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); - if (type == napi_uint8_array || type == napi_uint8_clamped_array) { - *ptr = (const uint8_t*)data; - return true; - } - } - return js_type_error(env, "expected a Uint8Array"); -} - -/* A string argument encoded as UTF-8: short strings live in the inline - * buffer, longer ones on the heap until js_str_free. */ -typedef struct { - char* ptr; - size_t len; - char inline_buf[256]; -} js_str; - -#define JS_STR_INIT {NULL, 0, {0}} - -JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { - size_t len = 0; - if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { - return js_type_error(env, "expected a string"); - } - s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); - napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); - return true; -} - -JS_HELPER void js_str_free(js_str* s) { - if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); - s->ptr = NULL; -} - -#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) - -/* Read up to `n` arguments; missing ones are undefined. */ -#define JS_ARGS(n) \ - size_t argc = (n); \ - napi_value argv[(n) + 1]; \ - napi_get_cb_info(env, info, &argc, argv, NULL, NULL) - -/* --------------------------------------------------------------------------- - * Async calls: the completion may run on any thread, so it only records the - * result and queues the settlement on the JavaScript thread through a - * thread-safe function (which also keeps the event loop alive meanwhile). - * ------------------------------------------------------------------------- */ - -typedef enum { - JS_R_VOID, - JS_R_I32, - JS_R_U32, - JS_R_I64, - JS_R_U64, - JS_R_F64, - JS_R_BOOL, - JS_R_STR, - JS_R_BYTES, - JS_R_HANDLE -} js_kind; - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - js_kind kind; - js_error* err; - union { - int64_t i; - uint64_t u; - double f; - bool b; - const void* p; - } v; - size_t len; -} js_async; - -JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { - (void)cb; - (void)context; - js_async* a = (js_async*)data; - bool failed = a->err != NULL && a->err->code != 0; - if (env != NULL && failed) { - napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, - a->err->payload_ptr, a->err->payload_len)); - } else if (env != NULL) { - napi_value v = NULL; - switch (a->kind) { - case JS_R_VOID: v = js_undefined(env); break; - case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; - case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; - case JS_R_I64: v = js_new_i64(env, a->v.i); break; - case JS_R_U64: v = js_new_u64(env, a->v.u); break; - case JS_R_F64: v = js_new_f64(env, a->v.f); break; - case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; - case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; - } - napi_resolve_deferred(env, a->deferred, v); - } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { - /* The environment is shutting down: just release the result. */ - nested_modules_free_bytes((uint8_t*)a->v.p, a->len); - } - nested_modules_error_free(a->err); - napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); - free(a); -} - -/* Create the promise and its settlement queue for one async call. */ -JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { - js_async* a = (js_async*)calloc(1, sizeof *a); - a->kind = kind; - napi_create_promise(env, &a->deferred, promise); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, - js_async_settle, &a->tsfn); - return a; -} - -/* Called by each completion once it has recorded its result. */ -JS_HELPER void js_async_done(js_async* a, js_error* err) { - a->err = err; - napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); -} - -/* --------------------------------------------------------------------------- - * Callback interfaces. One registration per implementation passed to the - * producer: a reference to the adapter object, the JavaScript thread it - * belongs to, and a thread-safe function that runs calls made from any other - * thread on that thread while the caller waits. - * - * Waiting is what makes a call from another thread safe, and also what could - * deadlock it: if the JavaScript thread is itself inside a synchronous call - * into the library that waits for the calling thread, neither can proceed. - * A waiting call therefore watches the JavaScript thread. While that thread - * stays inside one synchronous call for JS_CB_DEADLOCK_MS without having - * started the callback, the wait is abandoned and the callback fails with - * code -4 instead of hanging (a shorter synchronous call, or one that - * returns to the event loop, just delays the callback). - * ------------------------------------------------------------------------- */ - -#define JS_CB_DEADLOCK_MS 1000 - -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; - uv_thread_t js_thread; - js_thread* thread; -} js_cb; - -typedef enum { JS_REQ_QUEUED, JS_REQ_RUNNING, JS_REQ_DONE, JS_REQ_ABANDONED } js_req_state; - -/* A call queued from another thread: `method` is the vtable index, or -1 to - * release the registration; `frame` holds the method's slots. The request is - * heap-allocated and holds its own reference to the thread state, so a - * caller that abandons it can return (and the producer can then release the - * registration) while it is still queued: the dispatcher frees it without - * touching the frame or the registration. */ -typedef struct { - js_cb* cb; - js_thread* thread; - int method; - void* frame; - js_error* out_err; - js_req_state state; -} js_cb_req; - -JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &cb->js_thread) != 0; -} - -JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, - napi_threadsafe_function_call_js dispatch) { - js_cb* cb = (js_cb*)calloc(1, sizeof *cb); - cb->env = env; - cb->js_thread = uv_thread_self(); - cb->thread = js_env_of(env)->thread; - js_thread_retain(cb->thread); - napi_create_reference(env, adapter, 1, &cb->ref); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, - &cb->tsfn); - /* A live implementation must not keep the process alive by itself. */ - napi_unref_threadsafe_function(env, cb->tsfn); - return cb; -} - -JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { - if (env != NULL) napi_delete_reference(env, cb->ref); - napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); - js_thread_release(cb->thread); - free(cb); -} - -/* A callback-interface argument: the adapter object `index.js` built, or - * null for an absent optional one. */ -JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, - napi_threadsafe_function_call_js dispatch, bool nullable, js_cb** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (nullable && (t == napi_null || t == napi_undefined)) { - *out = NULL; - return true; - } - if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); - *out = js_cb_register(env, v, name, dispatch); - return true; -} - -/* The vtable's `free`: the producer is done with the implementation. The - * reference can only be deleted on its own thread. */ -JS_HELPER void js_cb_free(void* ctx) { - js_cb* cb = (js_cb*)ctx; - if (cb == NULL) return; - if (js_cb_on_js_thread(cb)) { - js_cb_release(cb->env, cb); - return; - } - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->method = -1; - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); -} - -/* Run method `method` of `cb` on its JavaScript thread and wait for it, - * failing it with code -4 rather than deadlocking (see above). `what` names - * the method in that failure. */ -JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, - const char* what) { - js_thread* t = cb->thread; - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->thread = t; - js_thread_retain(t); - req->method = method; - req->frame = frame; - req->out_err = out_err; - req->state = JS_REQ_QUEUED; - uv_mutex_lock(&t->mu); - t->waiters++; - uv_mutex_unlock(&t->mu); - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - uv_mutex_lock(&t->mu); - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); - nested_modules_error_set(out_err, -4, "the callback implementation is no longer reachable"); - return; - } - const uint64_t limit = (uint64_t)JS_CB_DEADLOCK_MS * 1000000u; - bool blocked = false; - uint64_t call = 0, since = 0; - uv_mutex_lock(&t->mu); - while (req->state != JS_REQ_DONE) { - if (req->state == JS_REQ_QUEUED && t->depth > 0) { - uint64_t now = uv_hrtime(); - if (!blocked || call != t->calls) { - blocked = true; - call = t->calls; - since = now; - } - if (now - since >= limit) { - req->state = JS_REQ_ABANDONED; /* the dispatcher frees it */ - t->waiters--; - uv_mutex_unlock(&t->mu); - char msg[512]; - snprintf(msg, sizeof msg, - "%s was called on another thread while the JavaScript thread was blocked in a " - "synchronous call into the library for %d ms; the call likely waits for this " - "callback, so waiting longer would deadlock (call it asynchronously, or call " - "back on the calling thread)", - what, JS_CB_DEADLOCK_MS); - nested_modules_error_set(out_err, -4, msg); - return; - } - uv_cond_timedwait(&t->cv, &t->mu, limit - (now - since)); - } else { - blocked = false; - uv_cond_wait(&t->cv, &t->mu); - } - } - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); -} - -/* The dispatcher's side of a hop. js_cb_take claims a queued request (false - * when its caller gave up, in which case the request is freed); - * js_cb_finish reports it done. */ -JS_HELPER bool js_cb_take(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - bool abandoned = req->state == JS_REQ_ABANDONED; - if (!abandoned) req->state = JS_REQ_RUNNING; - uv_mutex_unlock(&t->mu); - if (abandoned) { - free(req); - js_thread_release(t); - } - return !abandoned; -} - -JS_HELPER void js_cb_finish(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - req->state = JS_REQ_DONE; - uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* A JavaScript value's text as a heap-allocated C string (NULL on failure). */ -JS_HELPER char* js_cstring(napi_env env, napi_value v) { - napi_value str; - size_t len = 0; - if (napi_coerce_to_string(env, v, &str) != napi_ok || - napi_get_value_string_utf8(env, str, NULL, 0, &len) != napi_ok) { - return NULL; - } - char* out = (char*)malloc(len + 1); - napi_get_value_string_utf8(env, str, out, len + 1, NULL); - return out; -} - -/* Report the pending JavaScript exception (or `fallback`) through - * `out_err`; nothing unwinds through the C frame. A `$Fault` (a domain error - * a throwing method raised) reports its code, message, and payload; any - * other exception is a foreign failure, code -4, with its message. */ -JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { - int32_t code = -4; - char* msg = NULL; - const uint8_t* payload = NULL; - size_t payload_len = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc, text; - napi_get_and_clear_last_exception(env, &exc); - napi_value fault = js_fault_class(env); - bool is_fault = false; - if (fault != NULL) napi_instanceof(env, exc, fault, &is_fault); - napi_valuetype t; - napi_typeof(env, exc, &t); - text = exc; - if (t == napi_object) napi_get_named_property(env, exc, "message", &text); - msg = js_cstring(env, text); - if (is_fault) { - napi_value v; - napi_get_named_property(env, exc, "code", &v); - napi_get_value_int32(env, v, &code); - napi_get_named_property(env, exc, "payload", &v); - bool typed = false; - napi_is_typedarray(env, v, &typed); - if (typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, &payload_len, &data, NULL, NULL); - payload = (const uint8_t*)data; - } - } - napi_is_exception_pending(env, &pending); - if (pending) napi_get_and_clear_last_exception(env, &exc); - } - nested_modules_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); - if (payload != NULL) nested_modules_error_set_payload(out_err, payload, payload_len); - free(msg); -} - -/* Look up and call `method` on the adapter; false when it threw. */ -JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, - const napi_value* argv, napi_value* result) { - napi_value adapter, fn; - napi_get_reference_value(env, cb->ref, &adapter); - napi_get_named_property(env, adapter, method, &fn); - return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; -} - -/* Hand `len` bytes to the producer through a callback's out slots, as a run - * from {p}_alloc that the producer adopts. */ -JS_HELPER void js_give(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = nested_modules_alloc(len); - if (len > 0) memcpy(run, data, len); - *out_ptr = run; - *out_len = len; -} - -/* A callback's string return: false (with a pending exception) when the - * value isn't a string. */ -JS_HELPER bool js_ret_str(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - js_str s = JS_STR_INIT; - if (!js_arg_str(env, v, &s)) return false; - js_give(s.ptr, s.len, out_ptr, out_len); - js_str_free(&s); - return true; -} - -/* A callback's bytes or buffer return: a Uint8Array. */ -JS_HELPER bool js_ret_bytes(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - const uint8_t* ptr = NULL; - size_t len = 0; - if (!js_arg_bytes(env, v, &ptr, &len)) return false; - js_give(ptr, len, out_ptr, out_len); - return true; -} - -/* --------------------------------------------------------------------------- - * The runtime symbols every library exports. - * ------------------------------------------------------------------------- */ - -static napi_value js_abi_version(napi_env env, napi_callback_info info) { - (void)info; - return js_new_u32(env, nested_modules_abi_version()); -} - -static napi_value js_debug_live(napi_env env, napi_callback_info info) { - JS_ARGS(1); - int32_t kind = 0; - if (!js_arg_i32(env, argv[0], &kind)) return NULL; - return js_new_u64(env, nested_modules_debug_live(kind)); -} - -static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { - (void)info; - return js_new_handle(env, nested_modules_cancel_token_create()); -} - -static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - nested_modules_cancel_token_cancel((nested_modules_cancel_token*)token); - return js_undefined(env); -} - -static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - nested_modules_cancel_token_destroy((nested_modules_cancel_token*)token); - return js_undefined(env); -} - -#define JS_RUNTIME_EXPORTS \ - {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ - {"nested_modules_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ - {"nested_modules_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ - {"nested_modules_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"nested_modules_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"nested_modules_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ - napi_default, NULL} - -static napi_value nx_nested_modules_graphics_contract(napi_env env, napi_callback_info info) { - (void)info; - size_t len = 0; - const nested_modules_contract_entry* table = nested_modules_graphics_contract(&len); - return js_new_contract(env, table, len); -} - -static napi_value nx_nested_modules_graphics_new_canvas(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - int32_t a0 = 0; - if (!js_arg_i32(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = nested_modules_graphics_new_canvas(a0, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_nested_modules_graphics_shapes_make_shape(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - int32_t a0 = 0; - if (!js_arg_i32(env, argv[0], &a0)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = nested_modules_graphics_shapes_make_shape(a0, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_nested_modules_graphics_shapes_describe(napi_env env, napi_callback_info info) { - JS_ARGS(0); - napi_value ret = NULL; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = nested_modules_graphics_shapes_describe(&out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_nested_modules_graphics_shapes_geometry_compute(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = nested_modules_graphics_shapes_geometry_compute(a0, a0_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -NAPI_MODULE_INIT() { - js_env_init(env); - napi_property_descriptor props[] = { - JS_RUNTIME_EXPORTS, - {"nested_modules_graphics_contract", NULL, nx_nested_modules_graphics_contract, NULL, NULL, NULL, napi_default, NULL}, - {"nested_modules_graphics_new_canvas", NULL, nx_nested_modules_graphics_new_canvas, NULL, NULL, NULL, napi_default, NULL}, - {"nested_modules_graphics_shapes_make_shape", NULL, nx_nested_modules_graphics_shapes_make_shape, NULL, NULL, NULL, napi_default, NULL}, - {"nested_modules_graphics_shapes_describe", NULL, nx_nested_modules_graphics_shapes_describe, NULL, NULL, NULL, napi_default, NULL}, - {"nested_modules_graphics_shapes_geometry_compute", NULL, nx_nested_modules_graphics_shapes_geometry_compute, NULL, NULL, NULL, napi_default, NULL}, - }; - napi_define_properties(env, exports, sizeof props / sizeof props[0], props); - return exports; -} - -// END nested_modules_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__index_d_ts.snap deleted file mode 100644 index d6ab315f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_d_ts.snap +++ /dev/null @@ -1,77 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class ShapesError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends ShapesError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof ShapesError; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -/** Rich (algebraic) enums and the expanded numeric primitives. */ -export declare namespace shapes { - /** An algebraic shape (sum type with associated data) */ - export type Shape = - /** The empty shape (a unit variant) */ - | { tag: 'Empty' } - /** A circle with a radius */ - | { tag: 'Circle'; radius: number } - /** An axis-aligned rectangle */ - | { tag: 'Rectangle'; width: number; height: number } - /** An integer-bounded shape (exercises narrow + wide ints) */ - | { tag: 'Bounded'; min: number; max: number; weight: bigint } - /** A labeled shape with a small count */ - | { tag: 'Labeled'; label: string; count: number }; - /** A plain C-style enum (no payloads) */ - export enum Channel { - Red = 0, - Green = 1, - Blue = 2, - } - /** One field per supported numeric primitive */ - export interface Numerics { - a: number; - b: number; - c: number; - d: bigint; - e: number; - f: number; - g: number; - h: bigint; - i: number; - j: number; - } - /** Render a shape to a string */ - export function describe(shape: shapes.Shape): string; - /** Scale a shape by a factor, returning a new shape */ - export function scale(shape: shapes.Shape, factor: number): shapes.Shape; - /** Sum a list of bytes into a wide integer (numerics smoke) */ - export function sumBytes(values: Uint8Array): bigint; - /** Build a Numerics struct from every numeric primitive */ - export function makeNumerics(a: number, b: number, c: number, d: bigint, e: number, f: number, g: number, h: bigint, i: number, j: number): shapes.Numerics; -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap deleted file mode 100644 index cad4f052..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap +++ /dev/null @@ -1,214 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - ShapesError as $Error, -} from './runtime.js'; -export { ShapesError, CancelledError } from './runtime.js'; - -import { createRequire } from 'node:module'; - -// The native entry points: the N-API addon built from shapes_node.c. A prebuilt -// copy comes first (from the `shapes--` package `npm install` -// picks for this platform, or under prebuilds/-/), then the one -// node-gyp compiles at install time when none matches. -function $loadAddon(name) { - const require = createRequire(import.meta.url); - const platform = `${process.platform}-${process.arch}`; - const candidates = [ - `shapes-${platform}/${name}`, - `./prebuilds/${platform}/${name}`, - `./build/Release/${name}`, - `./build/Debug/${name}`, - ]; - for (const candidate of candidates) { - try { - return require(candidate); - } catch (e) { - if (e.code !== 'MODULE_NOT_FOUND') throw e; - } - } - throw new Error( - `the native addon ${name} was not found (tried ${candidates.join(', ')}); ` + - 'install the package for this platform, or build the addon with `npm install` ' + - 'or `node-gyp rebuild` in this package', - ); -} - -const $raw = $loadAddon('shapes_node.node'); -$raw.$setup($Fault); - -// Object tokens in value buffers are already bigint handles here. -const $token = (t) => t; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['shapes', [ - [0x0d7b3eec49243e7an, 0x5f232311808e1b1fn, 'shapes.Channel'], - [0x183fa8198100860en, 0xe40c6993cb31d359n, 'shapes.describe'], - [0x2d1395eb837b848en, 0x42202a0c12f3a16en, 'shapes.sum_bytes'], - [0x7f9a5b87ba09b86bn, 0xc5b32fb0efbbaa17n, 'shapes.scale'], - [0xc8edd779240fda17n, 0x68e1be78fddb20e4n, 'shapes.Numerics'], - [0xd3d3881b2fd18a62n, 0xf24d0eb76a99d918n, 'shapes.make_numerics'], - [0xf010844afff5ac3cn, 0x50de28ae9b41ff15n, 'shapes.Shape'], - ]], -]; -$verify($raw, 'shapes', 'shapes', 4, $contract); - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.shapes_debug_live(kind); -} - -const shapes$Channel = Object.freeze({ - Red: 0, - Green: 1, - Blue: 2, - 0: 'Red', - 1: 'Green', - 2: 'Blue', -}); - -function $w$shapes$Numerics(w, v) { - w.writeI8(v.a); - w.writeI16(v.b); - w.writeI32(v.c); - w.writeI64(v.d); - w.writeU8(v.e); - w.writeU16(v.f); - w.writeU32(v.g); - w.writeU64(v.h); - w.writeF32(v.i); - w.writeF64(v.j); -} -function $r$shapes$Numerics(r) { - return { - a: r.readI8(), - b: r.readI16(), - c: r.readI32(), - d: r.readI64(), - e: r.readU8(), - f: r.readU16(), - g: r.readU32(), - h: r.readU64(), - i: r.readF32(), - j: r.readF64(), - }; -} -function $w$shapes$Shape(w, v) { - switch (v.tag) { - case 'Empty': - w.writeI32(0); - return; - case 'Circle': - w.writeI32(1); - w.writeF64(v.radius); - return; - case 'Rectangle': - w.writeI32(2); - w.writeF32(v.width); - w.writeF32(v.height); - return; - case 'Bounded': - w.writeI32(3); - w.writeI16(v.min); - w.writeI16(v.max); - w.writeU64(v.weight); - return; - case 'Labeled': - w.writeI32(4); - w.writeString(v.label); - w.writeU8(v.count); - return; - default: - throw new TypeError('unknown Shape tag: ' + (v && v.tag)); - } -} -function $r$shapes$Shape(r) { - const tag = r.readI32(); - switch (tag) { - case 0: return { tag: 'Empty' }; - case 1: return { tag: 'Circle', radius: r.readF64() }; - case 2: return { tag: 'Rectangle', width: r.readF32(), height: r.readF32() }; - case 3: return { tag: 'Bounded', min: r.readI16(), max: r.readI16(), weight: r.readU64() }; - case 4: return { tag: 'Labeled', label: r.readString(), count: r.readU8() }; - default: - throw new $Error(-3, 'unknown Shape tag: ' + tag); - } -} - -const shapes$describe = function describe(shape) { - try { - return $raw.shapes_shapes_describe($encode(shape, $w$shapes$Shape)); - } catch ($e) { - throw $fault($e); - } -}; - -const shapes$scale = function scale(shape, factor) { - try { - return $decode($raw.shapes_shapes_scale($encode(shape, $w$shapes$Shape), factor), $r$shapes$Shape); - } catch ($e) { - throw $fault($e); - } -}; - -const shapes$sumBytes = function sumBytes(values) { - try { - return $raw.shapes_shapes_sum_bytes(values); - } catch ($e) { - throw $fault($e); - } -}; - -const shapes$makeNumerics = function makeNumerics(a, b, c, d, e, f, g, h, i, j) { - try { - return $decode($raw.shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j), $r$shapes$Numerics); - } catch ($e) { - throw $fault($e); - } -}; - -export const shapes = Object.freeze({ - Channel: shapes$Channel, - describe: shapes$describe, - scale: shapes$scale, - sumBytes: shapes$sumBytes, - makeNumerics: shapes$makeNumerics, -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_node_c.snap deleted file mode 100644 index 5ac8e146..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_node_c.snap +++ /dev/null @@ -1,1001 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/* libuv's Unix headers use POSIX types (pthread_rwlock_t, struct addrinfo) - * that glibc hides under a strict -std=c11; ask for them explicitly. */ -#if defined(__linux__) && !defined(_GNU_SOURCE) -#define _GNU_SOURCE -#endif -#ifndef NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include -#include -#include -#include -#include -#include - -/* The addon wraps deprecated functions too; the JavaScript API carries the - * deprecation instead. */ -#define SHAPES_DEPRECATED(msg) -#include "shapes.h" - -/* - * The N-API transport of the JavaScript bindings. Every C symbol of the - * library is exported to `index.js` under its own name and called with the - * raw convention the shared JavaScript layer documents: direct values as - * numbers, bigints, and booleans; strings as strings; bytes and value - * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint - * handles (null when absent); callback interfaces as adapter objects (null - * for an absent optional one). A failure throws an instance of the - * runtime's `$Fault` class. - */ - -typedef shapes_error js_error; - -/* Helpers an API may not need. */ -#if defined(__GNUC__) || defined(__clang__) -#define JS_HELPER static inline __attribute__((unused)) -#define JS_TLS __thread -#else -#define JS_HELPER static inline -#define JS_TLS __declspec(thread) -#endif - -/* --------------------------------------------------------------------------- - * JavaScript threads. One `js_thread` per thread that runs JavaScript and - * loaded the addon (the main thread and each worker). It counts the - * synchronous calls into the library in progress on that thread, which a - * callback made on another thread consults while it waits for the - * JavaScript thread (see js_cb_hop). Each environment and each callback - * registration holds a reference. - * ------------------------------------------------------------------------- */ - -typedef struct { - uv_mutex_t mu; - uv_cond_t cv; /* broadcast when a call begins or ends, or a hop finishes */ - unsigned depth; /* synchronous calls into the library in progress */ - uint64_t calls; /* outermost synchronous calls begun so far */ - unsigned waiters; /* producer threads waiting in js_cb_hop */ - unsigned refs; -} js_thread; - -/* The state of the JavaScript thread this is, or NULL on any other thread. */ -static JS_TLS js_thread* js_current = NULL; - -static js_thread* js_thread_new(void) { - js_thread* t = (js_thread*)calloc(1, sizeof *t); - uv_mutex_init(&t->mu); - uv_cond_init(&t->cv); - t->refs = 1; - return t; -} - -static void js_thread_retain(js_thread* t) { - uv_mutex_lock(&t->mu); - t->refs++; - uv_mutex_unlock(&t->mu); -} - -static void js_thread_release(js_thread* t) { - uv_mutex_lock(&t->mu); - bool last = --t->refs == 0; - uv_mutex_unlock(&t->mu); - if (!last) return; - uv_cond_destroy(&t->cv); - uv_mutex_destroy(&t->mu); - free(t); -} - -/* Bracket every synchronous call into the library. */ -JS_HELPER void js_sync_begin(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - if (t->depth++ == 0) t->calls++; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -JS_HELPER void js_sync_end(void) { - js_thread* t = js_current; - if (t == NULL) return; - uv_mutex_lock(&t->mu); - t->depth--; - if (t->waiters > 0) uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* Per-environment state (one per main thread or worker): the `$Fault` - * constructor `index.js` registers through `$setup`, and the thread. */ -typedef struct { - napi_ref fault; - js_thread* thread; -} js_env; - -static void js_env_free(napi_env env, void* data, void* hint) { - (void)hint; - js_env* e = (js_env*)data; - if (e->fault != NULL) napi_delete_reference(env, e->fault); - if (js_current == e->thread) js_current = NULL; - js_thread_release(e->thread); - free(e); -} - -/* Set up the environment of the thread loading the addon. */ -static void js_env_init(napi_env env) { - js_env* e = (js_env*)calloc(1, sizeof *e); - e->thread = js_thread_new(); - js_current = e->thread; - napi_set_instance_data(env, e, js_env_free, NULL); -} - -static js_env* js_env_of(napi_env env) { - js_env* e = NULL; - napi_get_instance_data(env, (void**)&e); - return e; -} - -JS_HELPER napi_value js_undefined(napi_env env) { - napi_value v; - napi_get_undefined(env, &v); - return v; -} - -static napi_value js_setup(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value argv[1]; - napi_get_cb_info(env, info, &argc, argv, NULL, NULL); - js_env* e = js_env_of(env); - if (e->fault != NULL) napi_delete_reference(env, e->fault); - napi_create_reference(env, argv[0], 1, &e->fault); - return js_undefined(env); -} - -/* The `$Fault` constructor, or NULL before `$setup`. */ -static napi_value js_fault_class(napi_env env) { - js_env* e = js_env_of(env); - napi_value ctor = NULL; - if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok) { - return NULL; - } - return ctor; -} - -/* --------------------------------------------------------------------------- - * Value conversions - * ------------------------------------------------------------------------- */ - -JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len, &data, &ab); - if (len > 0) memcpy(data, ptr, len); - napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out; - napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); - return out; -} - -JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { - napi_value out; - napi_create_int32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { - napi_value out; - napi_create_uint32(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { - napi_value out; - napi_create_bigint_int64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { - napi_value out; - napi_create_bigint_uint64(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_f64(napi_env env, double v) { - napi_value out; - napi_create_double(env, v, &out); - return out; -} - -JS_HELPER napi_value js_new_bool(napi_env env, bool v) { - napi_value out; - napi_get_boolean(env, v, &out); - return out; -} - -/* An object, iterator, or cancel token handle: the pointer as a bigint, or - * null for NULL. */ -JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { - napi_value out; - if (p == NULL) { - napi_get_null(env, &out); - } else { - napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); - } - return out; -} - -/* Returned strings and buffers: convert, then release the producer's copy. */ -JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_str(env, ptr, len); - if (ptr != NULL) shapes_free_bytes((uint8_t*)ptr, len); - return out; -} - -JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { - napi_value out = js_new_bytes(env, ptr, len); - if (ptr != NULL) shapes_free_bytes((uint8_t*)ptr, len); - return out; -} - -/* A module's contract table as a BigUint64Array of `id, hash` pairs. */ -JS_HELPER napi_value js_new_contract(napi_env env, const shapes_contract_entry* table, - size_t len) { - napi_value ab, out; - void* data = NULL; - napi_create_arraybuffer(env, len * 2 * sizeof(uint64_t), &data, &ab); - uint64_t* words = (uint64_t*)data; - for (size_t i = 0; i < len; i++) { - words[2 * i] = table[i].id; - words[2 * i + 1] = table[i].hash; - } - napi_create_typedarray(env, napi_biguint64_array, len * 2, ab, 0, &out); - return out; -} - -JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, - const uint8_t* payload, size_t payload_len) { - napi_value argv[3], ctor, out = NULL; - napi_create_int32(env, code, &argv[0]); - napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); - if (payload != NULL) { - argv[2] = js_new_bytes(env, payload, payload_len); - } else { - napi_get_null(env, &argv[2]); - } - ctor = js_fault_class(env); - if (ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { - napi_create_error(env, NULL, argv[1], &out); - napi_set_named_property(env, out, "code", argv[0]); - } - return out; -} - -/* Throw the error a call reported, then release it. */ -JS_HELPER napi_value js_throw(napi_env env, js_error* err) { - napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); - shapes_error_clear(err); - return NULL; -} - -JS_HELPER bool js_type_error(napi_env env, const char* expected) { - bool pending = false; - napi_is_exception_pending(env, &pending); - if (!pending) napi_throw_type_error(env, NULL, expected); - return false; -} - -JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { - return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { - return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int8_t)t; - return true; -} - -JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { - int32_t t = 0; - if (!js_arg_i32(env, v, &t)) return false; - *out = (int16_t)t; - return true; -} - -JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint8_t)t; - return true; -} - -JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { - uint32_t t = 0; - if (!js_arg_u32(env, v, &t)) return false; - *out = (uint16_t)t; - return true; -} - -/* 64-bit integers: a bigint that fits, or an integral number. */ -JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_int64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { - *out = (int64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_get_value_bigint_uint64(env, v, out, &lossless); - if (lossless) return true; - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return false; - } - double d = 0; - if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && - d == (double)(uint64_t)d) { - *out = (uint64_t)d; - return true; - } - return js_type_error(env, "expected a bigint"); -} - -JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { - return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); -} - -JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { - double d = 0; - if (!js_arg_f64(env, v, &d)) return false; - *out = (float)d; - return true; -} - -JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { - return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); -} - -/* A handle argument; null or undefined is NULL when `nullable`. */ -JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return nullable || js_type_error(env, "expected an object handle"); - } - uint64_t raw = 0; - bool lossless = false; - if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || - raw == 0) { - return js_type_error(env, "expected an object handle"); - } - *out = (void*)(uintptr_t)raw; - return true; -} - -/* A borrowed byte run: a Uint8Array (or Buffer). */ -JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { - bool is_typed = false; - napi_is_typedarray(env, v, &is_typed); - if (is_typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); - if (type == napi_uint8_array || type == napi_uint8_clamped_array) { - *ptr = (const uint8_t*)data; - return true; - } - } - return js_type_error(env, "expected a Uint8Array"); -} - -/* A string argument encoded as UTF-8: short strings live in the inline - * buffer, longer ones on the heap until js_str_free. */ -typedef struct { - char* ptr; - size_t len; - char inline_buf[256]; -} js_str; - -#define JS_STR_INIT {NULL, 0, {0}} - -JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { - size_t len = 0; - if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { - return js_type_error(env, "expected a string"); - } - s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); - napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); - return true; -} - -JS_HELPER void js_str_free(js_str* s) { - if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); - s->ptr = NULL; -} - -#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) - -/* Read up to `n` arguments; missing ones are undefined. */ -#define JS_ARGS(n) \ - size_t argc = (n); \ - napi_value argv[(n) + 1]; \ - napi_get_cb_info(env, info, &argc, argv, NULL, NULL) - -/* --------------------------------------------------------------------------- - * Async calls: the completion may run on any thread, so it only records the - * result and queues the settlement on the JavaScript thread through a - * thread-safe function (which also keeps the event loop alive meanwhile). - * ------------------------------------------------------------------------- */ - -typedef enum { - JS_R_VOID, - JS_R_I32, - JS_R_U32, - JS_R_I64, - JS_R_U64, - JS_R_F64, - JS_R_BOOL, - JS_R_STR, - JS_R_BYTES, - JS_R_HANDLE -} js_kind; - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - js_kind kind; - js_error* err; - union { - int64_t i; - uint64_t u; - double f; - bool b; - const void* p; - } v; - size_t len; -} js_async; - -JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { - (void)cb; - (void)context; - js_async* a = (js_async*)data; - bool failed = a->err != NULL && a->err->code != 0; - if (env != NULL && failed) { - napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, - a->err->payload_ptr, a->err->payload_len)); - } else if (env != NULL) { - napi_value v = NULL; - switch (a->kind) { - case JS_R_VOID: v = js_undefined(env); break; - case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; - case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; - case JS_R_I64: v = js_new_i64(env, a->v.i); break; - case JS_R_U64: v = js_new_u64(env, a->v.u); break; - case JS_R_F64: v = js_new_f64(env, a->v.f); break; - case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; - case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; - case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; - } - napi_resolve_deferred(env, a->deferred, v); - } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { - /* The environment is shutting down: just release the result. */ - shapes_free_bytes((uint8_t*)a->v.p, a->len); - } - shapes_error_free(a->err); - napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); - free(a); -} - -/* Create the promise and its settlement queue for one async call. */ -JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { - js_async* a = (js_async*)calloc(1, sizeof *a); - a->kind = kind; - napi_create_promise(env, &a->deferred, promise); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, - js_async_settle, &a->tsfn); - return a; -} - -/* Called by each completion once it has recorded its result. */ -JS_HELPER void js_async_done(js_async* a, js_error* err) { - a->err = err; - napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); -} - -/* --------------------------------------------------------------------------- - * Callback interfaces. One registration per implementation passed to the - * producer: a reference to the adapter object, the JavaScript thread it - * belongs to, and a thread-safe function that runs calls made from any other - * thread on that thread while the caller waits. - * - * Waiting is what makes a call from another thread safe, and also what could - * deadlock it: if the JavaScript thread is itself inside a synchronous call - * into the library that waits for the calling thread, neither can proceed. - * A waiting call therefore watches the JavaScript thread. While that thread - * stays inside one synchronous call for JS_CB_DEADLOCK_MS without having - * started the callback, the wait is abandoned and the callback fails with - * code -4 instead of hanging (a shorter synchronous call, or one that - * returns to the event loop, just delays the callback). - * ------------------------------------------------------------------------- */ - -#define JS_CB_DEADLOCK_MS 1000 - -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; - uv_thread_t js_thread; - js_thread* thread; -} js_cb; - -typedef enum { JS_REQ_QUEUED, JS_REQ_RUNNING, JS_REQ_DONE, JS_REQ_ABANDONED } js_req_state; - -/* A call queued from another thread: `method` is the vtable index, or -1 to - * release the registration; `frame` holds the method's slots. The request is - * heap-allocated and holds its own reference to the thread state, so a - * caller that abandons it can return (and the producer can then release the - * registration) while it is still queued: the dispatcher frees it without - * touching the frame or the registration. */ -typedef struct { - js_cb* cb; - js_thread* thread; - int method; - void* frame; - js_error* out_err; - js_req_state state; -} js_cb_req; - -JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &cb->js_thread) != 0; -} - -JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, - napi_threadsafe_function_call_js dispatch) { - js_cb* cb = (js_cb*)calloc(1, sizeof *cb); - cb->env = env; - cb->js_thread = uv_thread_self(); - cb->thread = js_env_of(env)->thread; - js_thread_retain(cb->thread); - napi_create_reference(env, adapter, 1, &cb->ref); - napi_value resource; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); - napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, - &cb->tsfn); - /* A live implementation must not keep the process alive by itself. */ - napi_unref_threadsafe_function(env, cb->tsfn); - return cb; -} - -JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { - if (env != NULL) napi_delete_reference(env, cb->ref); - napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); - js_thread_release(cb->thread); - free(cb); -} - -/* A callback-interface argument: the adapter object `index.js` built, or - * null for an absent optional one. */ -JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, - napi_threadsafe_function_call_js dispatch, bool nullable, js_cb** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (nullable && (t == napi_null || t == napi_undefined)) { - *out = NULL; - return true; - } - if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); - *out = js_cb_register(env, v, name, dispatch); - return true; -} - -/* The vtable's `free`: the producer is done with the implementation. The - * reference can only be deleted on its own thread. */ -JS_HELPER void js_cb_free(void* ctx) { - js_cb* cb = (js_cb*)ctx; - if (cb == NULL) return; - if (js_cb_on_js_thread(cb)) { - js_cb_release(cb->env, cb); - return; - } - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->method = -1; - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); -} - -/* Run method `method` of `cb` on its JavaScript thread and wait for it, - * failing it with code -4 rather than deadlocking (see above). `what` names - * the method in that failure. */ -JS_HELPER void js_cb_hop(js_cb* cb, int method, void* frame, js_error* out_err, - const char* what) { - js_thread* t = cb->thread; - js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); - req->cb = cb; - req->thread = t; - js_thread_retain(t); - req->method = method; - req->frame = frame; - req->out_err = out_err; - req->state = JS_REQ_QUEUED; - uv_mutex_lock(&t->mu); - t->waiters++; - uv_mutex_unlock(&t->mu); - if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - uv_mutex_lock(&t->mu); - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); - shapes_error_set(out_err, -4, "the callback implementation is no longer reachable"); - return; - } - const uint64_t limit = (uint64_t)JS_CB_DEADLOCK_MS * 1000000u; - bool blocked = false; - uint64_t call = 0, since = 0; - uv_mutex_lock(&t->mu); - while (req->state != JS_REQ_DONE) { - if (req->state == JS_REQ_QUEUED && t->depth > 0) { - uint64_t now = uv_hrtime(); - if (!blocked || call != t->calls) { - blocked = true; - call = t->calls; - since = now; - } - if (now - since >= limit) { - req->state = JS_REQ_ABANDONED; /* the dispatcher frees it */ - t->waiters--; - uv_mutex_unlock(&t->mu); - char msg[512]; - snprintf(msg, sizeof msg, - "%s was called on another thread while the JavaScript thread was blocked in a " - "synchronous call into the library for %d ms; the call likely waits for this " - "callback, so waiting longer would deadlock (call it asynchronously, or call " - "back on the calling thread)", - what, JS_CB_DEADLOCK_MS); - shapes_error_set(out_err, -4, msg); - return; - } - uv_cond_timedwait(&t->cv, &t->mu, limit - (now - since)); - } else { - blocked = false; - uv_cond_wait(&t->cv, &t->mu); - } - } - t->waiters--; - uv_mutex_unlock(&t->mu); - free(req); - js_thread_release(t); -} - -/* The dispatcher's side of a hop. js_cb_take claims a queued request (false - * when its caller gave up, in which case the request is freed); - * js_cb_finish reports it done. */ -JS_HELPER bool js_cb_take(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - bool abandoned = req->state == JS_REQ_ABANDONED; - if (!abandoned) req->state = JS_REQ_RUNNING; - uv_mutex_unlock(&t->mu); - if (abandoned) { - free(req); - js_thread_release(t); - } - return !abandoned; -} - -JS_HELPER void js_cb_finish(js_cb_req* req) { - js_thread* t = req->thread; - uv_mutex_lock(&t->mu); - req->state = JS_REQ_DONE; - uv_cond_broadcast(&t->cv); - uv_mutex_unlock(&t->mu); -} - -/* A JavaScript value's text as a heap-allocated C string (NULL on failure). */ -JS_HELPER char* js_cstring(napi_env env, napi_value v) { - napi_value str; - size_t len = 0; - if (napi_coerce_to_string(env, v, &str) != napi_ok || - napi_get_value_string_utf8(env, str, NULL, 0, &len) != napi_ok) { - return NULL; - } - char* out = (char*)malloc(len + 1); - napi_get_value_string_utf8(env, str, out, len + 1, NULL); - return out; -} - -/* Report the pending JavaScript exception (or `fallback`) through - * `out_err`; nothing unwinds through the C frame. A `$Fault` (a domain error - * a throwing method raised) reports its code, message, and payload; any - * other exception is a foreign failure, code -4, with its message. */ -JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { - int32_t code = -4; - char* msg = NULL; - const uint8_t* payload = NULL; - size_t payload_len = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc, text; - napi_get_and_clear_last_exception(env, &exc); - napi_value fault = js_fault_class(env); - bool is_fault = false; - if (fault != NULL) napi_instanceof(env, exc, fault, &is_fault); - napi_valuetype t; - napi_typeof(env, exc, &t); - text = exc; - if (t == napi_object) napi_get_named_property(env, exc, "message", &text); - msg = js_cstring(env, text); - if (is_fault) { - napi_value v; - napi_get_named_property(env, exc, "code", &v); - napi_get_value_int32(env, v, &code); - napi_get_named_property(env, exc, "payload", &v); - bool typed = false; - napi_is_typedarray(env, v, &typed); - if (typed) { - napi_typedarray_type type; - void* data = NULL; - napi_get_typedarray_info(env, v, &type, &payload_len, &data, NULL, NULL); - payload = (const uint8_t*)data; - } - } - napi_is_exception_pending(env, &pending); - if (pending) napi_get_and_clear_last_exception(env, &exc); - } - shapes_error_set(out_err, code, msg != NULL && msg[0] != 0 ? msg : fallback); - if (payload != NULL) shapes_error_set_payload(out_err, payload, payload_len); - free(msg); -} - -/* Look up and call `method` on the adapter; false when it threw. */ -JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, - const napi_value* argv, napi_value* result) { - napi_value adapter, fn; - napi_get_reference_value(env, cb->ref, &adapter); - napi_get_named_property(env, adapter, method, &fn); - return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; -} - -/* Hand `len` bytes to the producer through a callback's out slots, as a run - * from {p}_alloc that the producer adopts. */ -JS_HELPER void js_give(const void* data, size_t len, uint8_t** out_ptr, size_t* out_len) { - uint8_t* run = shapes_alloc(len); - if (len > 0) memcpy(run, data, len); - *out_ptr = run; - *out_len = len; -} - -/* A callback's string return: false (with a pending exception) when the - * value isn't a string. */ -JS_HELPER bool js_ret_str(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - js_str s = JS_STR_INIT; - if (!js_arg_str(env, v, &s)) return false; - js_give(s.ptr, s.len, out_ptr, out_len); - js_str_free(&s); - return true; -} - -/* A callback's bytes or buffer return: a Uint8Array. */ -JS_HELPER bool js_ret_bytes(napi_env env, napi_value v, uint8_t** out_ptr, size_t* out_len) { - const uint8_t* ptr = NULL; - size_t len = 0; - if (!js_arg_bytes(env, v, &ptr, &len)) return false; - js_give(ptr, len, out_ptr, out_len); - return true; -} - -/* --------------------------------------------------------------------------- - * The runtime symbols every library exports. - * ------------------------------------------------------------------------- */ - -static napi_value js_abi_version(napi_env env, napi_callback_info info) { - (void)info; - return js_new_u32(env, shapes_abi_version()); -} - -static napi_value js_debug_live(napi_env env, napi_callback_info info) { - JS_ARGS(1); - int32_t kind = 0; - if (!js_arg_i32(env, argv[0], &kind)) return NULL; - return js_new_u64(env, shapes_debug_live(kind)); -} - -static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { - (void)info; - return js_new_handle(env, shapes_cancel_token_create()); -} - -static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - shapes_cancel_token_cancel((shapes_cancel_token*)token); - return js_undefined(env); -} - -static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { - JS_ARGS(1); - void* token = NULL; - if (!js_arg_handle(env, argv[0], &token, false)) return NULL; - shapes_cancel_token_destroy((shapes_cancel_token*)token); - return js_undefined(env); -} - -#define JS_RUNTIME_EXPORTS \ - {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ - {"shapes_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ - {"shapes_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ - {"shapes_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"shapes_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ - napi_default, NULL}, \ - {"shapes_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ - napi_default, NULL} - -static napi_value nx_shapes_shapes_contract(napi_env env, napi_callback_info info) { - (void)info; - size_t len = 0; - const shapes_contract_entry* table = shapes_shapes_contract(&len); - return js_new_contract(env, table, len); -} - -static napi_value nx_shapes_shapes_describe(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = shapes_shapes_describe(a0, a0_len, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_str(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_shapes_shapes_scale(napi_env env, napi_callback_info info) { - JS_ARGS(2); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - double a1 = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - if (!js_arg_f64(env, argv[1], &a1)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = shapes_shapes_scale(a0, a0_len, a1, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -static napi_value nx_shapes_shapes_sum_bytes(napi_env env, napi_callback_info info) { - JS_ARGS(1); - napi_value ret = NULL; - const uint8_t* a0 = NULL; - size_t a0_len = 0; - if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; - { - js_error err = {0}; - js_sync_begin(); - uint64_t r = shapes_shapes_sum_bytes(a0, a0_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_new_u64(env, r); - } - goto done; -done: - return ret; -} - -static napi_value nx_shapes_shapes_make_numerics(napi_env env, napi_callback_info info) { - JS_ARGS(10); - napi_value ret = NULL; - int8_t a0 = 0; - int16_t a1 = 0; - int32_t a2 = 0; - int64_t a3 = 0; - uint8_t a4 = 0; - uint16_t a5 = 0; - uint32_t a6 = 0; - uint64_t a7 = 0; - float a8 = 0; - double a9 = 0; - if (!js_arg_i8(env, argv[0], &a0)) goto done; - if (!js_arg_i16(env, argv[1], &a1)) goto done; - if (!js_arg_i32(env, argv[2], &a2)) goto done; - if (!js_arg_i64(env, argv[3], &a3)) goto done; - if (!js_arg_u8(env, argv[4], &a4)) goto done; - if (!js_arg_u16(env, argv[5], &a5)) goto done; - if (!js_arg_u32(env, argv[6], &a6)) goto done; - if (!js_arg_u64(env, argv[7], &a7)) goto done; - if (!js_arg_f32(env, argv[8], &a8)) goto done; - if (!js_arg_f64(env, argv[9], &a9)) goto done; - { - js_error err = {0}; - size_t out_len = 0; - js_sync_begin(); - const uint8_t* r = shapes_shapes_make_numerics(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, &out_len, &err); - js_sync_end(); - if (err.code != 0) { - js_throw(env, &err); - goto done; - } - ret = js_take_bytes(env, r, out_len); - } - goto done; -done: - return ret; -} - -NAPI_MODULE_INIT() { - js_env_init(env); - napi_property_descriptor props[] = { - JS_RUNTIME_EXPORTS, - {"shapes_shapes_contract", NULL, nx_shapes_shapes_contract, NULL, NULL, NULL, napi_default, NULL}, - {"shapes_shapes_describe", NULL, nx_shapes_shapes_describe, NULL, NULL, NULL, napi_default, NULL}, - {"shapes_shapes_scale", NULL, nx_shapes_shapes_scale, NULL, NULL, NULL, napi_default, NULL}, - {"shapes_shapes_sum_bytes", NULL, nx_shapes_shapes_sum_bytes, NULL, NULL, NULL, napi_default, NULL}, - {"shapes_shapes_make_numerics", NULL, nx_shapes_shapes_make_numerics, NULL, NULL, NULL, napi_default, NULL}, - }; - napi_define_properties(env, exports, sizeof props / sizeof props[0], props); - return exports; -} - -// END shapes_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_py.snap deleted file mode 100644 index 6d85d09b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_py.snap +++ /dev/null @@ -1,1142 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -"""ctypes bindings for the docs_everywhere native library.""" -from __future__ import annotations - -import abc -import asyncio -import builtins -import ctypes -import os -import platform -import struct -import threading -import warnings -from dataclasses import dataclass -from enum import IntEnum -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar - - -class Error(Exception): - """An error a fallible call reports. - - `code` is positive for one of the API's declared error codes, raised as - the declaring module's typed subclasses, and negative for a runtime - failure of a call that declares errors: a generic failure (-1), a - producer panic (-2), a marshalling failure (-3), or an exception raised - by a callback-interface implementation (-4). A cancelled async call (-5) - raises `asyncio.CancelledError` instead. A call that declares no errors - never raises this; its failures are bugs and raise `InternalError`. - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - CANCELLED_ERROR_CODE = -5 - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - def _payload(self) -> bytes: - """The code's fields as a value buffer (a code with fields overrides - this).""" - return b"" - - -class InternalError(RuntimeError): - """A failure of a call that declares no errors: a producer panic (-2), a - marshalling failure (-3), a callback-interface failure the producer let - through (-4), or another runtime failure (-1). These are bugs, so they - are unchecked; `code` and `message` say what went wrong. - """ - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - -class _ErrorStruct(ctypes.Structure): - """The C `docs_everywhere_error` out-parameter every call reports through.""" - - _fields_ = [ - ("code", ctypes.c_int32), - ("message", ctypes.c_char_p), - ("payload_ptr", ctypes.c_void_p), - ("payload_len", ctypes.c_size_t), - ] - - -def _load_library() -> ctypes.CDLL: - # An explicit path wins; then a library bundled inside this package (a - # platform wheel); then the system loader's search path. - override = os.environ.get("DOCS_EVERYWHERE_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libdocs_everywhere.dylib" - elif system == "Windows": - name = "docs_everywhere.dll" - else: - name = "libdocs_everywhere.so" - bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) - if os.path.exists(bundled): - return ctypes.CDLL(bundled) - try: - return ctypes.CDLL(name) - except OSError as exc: - raise ImportError( - f"cannot load the native library {name!r} ({exc}); " - "set DOCS_EVERYWHERE_LIBRARY to its path" - ) from exc - - -_lib = _load_library() - - -def _bind(name: str, restype: Any, *argtypes: Any) -> Any: - """Look up one exported symbol and fix its C signature. Every prototype - is bound once, at import time.""" - fn = getattr(_lib, name) - fn.restype = restype - fn.argtypes = argtypes - return fn - - -class _ContractEntry(ctypes.Structure): - """The C `docs_everywhere_contract_entry`: one declaration's id and hash.""" - - _fields_ = [("id", ctypes.c_uint64), ("hash", ctypes.c_uint64)] - - -# The C ABI revision these bindings were generated against and, per -# top-level module's contract symbol, the declarations they rely on: -# (id, hash, dotted path). -_ABI_VERSION = 4 -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { - "docs_everywhere_docs_contract": [ - (0x00b2187fd75634ec, 0x2263c6f787bf6081, "docs.publish_async"), - (0x12fc242b9dc6444c, 0x96405f484a642b31, "docs.Notebook.insert"), - (0x2213822ed05cd5bd, 0x26c5833392aa63ce, "docs.Notebook.new"), - (0x2bd762bcdaa207ff, 0x1b0ebab2d04c49b0, "docs.SavedListener"), - (0x2e086c45e9d617de, 0x4d43bf32358c01ff, "docs.Notebook.find"), - (0x4817dc98197ad960, 0xf8017b5a7d58b2c7, "docs.Notebook.size"), - (0x614de47452d5a55c, 0xd9aae1472494ffef, "docs.get_document"), - (0x67c5cb9fc119bcd5, 0x08d5b25a1a22a7fa, "docs.Document"), - (0x8d484bf2d78c9b70, 0xe9093cf96912194c, "docs.Notebook.max_documents"), - (0xaea61d6451691c67, 0xe4704128b3524b3d, "docs.watch"), - (0xd68d6ad1540cdec5, 0x01afeb40b0a8bfe1, "docs.Notebook"), - (0xdb0601444a4760cc, 0xe56c69629b444797, "docs.create_document"), - (0xe12d6f4838347b3e, 0xa9bab86646d84f51, "docs.Status"), - (0xee2f8d25b3313b50, 0x9691232f35439a2b, "docs.DocsErrors"), - ], -} - - -def _check_contract() -> None: - """Refuse to load a library built for another ABI revision, or one that - lacks or changed a declaration these bindings use, before any other - symbol is bound.""" - try: - abi_version = _bind("docs_everywhere_abi_version", ctypes.c_uint32) - except AttributeError: - raise ImportError( - f"{_lib._name} does not export docs_everywhere_abi_version; " - "it is not the library these bindings were generated for" - ) from None - found = abi_version() - if found != _ABI_VERSION: - raise ImportError( - f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " - f"but {_lib._name} reports revision {found}" - ) - for symbol, expected in _CONTRACTS.items(): - try: - contract = _bind( - symbol, ctypes.POINTER(_ContractEntry), ctypes.POINTER(ctypes.c_size_t) - ) - except AttributeError: - raise ImportError(f"{_lib._name} does not export {symbol}") from None - length = ctypes.c_size_t() - table = contract(ctypes.byref(length)) - hashes = {table[i].id: table[i].hash for i in range(length.value)} if table else {} - for entry_id, entry_hash, path in expected: - actual = hashes.get(entry_id) - if actual is None: - raise ImportError(f"{path} is missing from the library ({_lib._name})") - if actual != entry_hash: - raise ImportError( - f"{path} changed since these bindings were generated ({_lib._name})" - ) - - -_check_contract() - -_error_clear = _bind("docs_everywhere_error_clear", None, ctypes.POINTER(_ErrorStruct)) -_free_bytes = _bind("docs_everywhere_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) -_debug_live_fn = _bind("docs_everywhere_debug_live", ctypes.c_uint64, ctypes.c_int32) - - -def _debug_live(kind: int) -> int: - """The producer's live-resource counter `kind` (0 objects, 1 callbacks, - 2 iterators, 3 cancel tokens, 4 byte runs; -1 is 1 when the producer - counts at all). Always 0 unless the producer was built with its leak - counters enabled.""" - live: int = _debug_live_fn(kind) - return live - - -def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: - """Copy a filled out-err slot's code, message, and payload, then release - them.""" - code = err.code - message = err.message.decode("utf-8", "replace") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _error_clear(ctypes.byref(err)) - return code, message, payload - - -def _trap_from(code: int, message: str, payload: bytes = b"") -> InternalError: - """The exception for a failed call that declares no errors.""" - return InternalError(code, message) - - -def _take_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a producer-owned byte run (a string, bytes, or value buffer) and - release it with docs_everywhere_free_bytes.""" - if not ptr: - return b"" - try: - return ctypes.string_at(ptr, length) - finally: - _free_bytes(ptr, length) - - -def _take_str(ptr: Optional[int], length: int) -> str: - """Copy and release a producer-owned UTF-8 string.""" - return _take_bytes(ptr, length).decode("utf-8") - - -def _peek_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a byte run the producer only lends for the current call.""" - return ctypes.string_at(ptr, length) if ptr and length else b"" - - -_H = TypeVar("_H", bound="_Handle") - - -class _Handle: - """Owns one native reference and releases it exactly once. - - Calls lend the pointer through `_acquire`/`_release`, so `close()` from - another thread while a call is in flight defers the release until the - last such call returns instead of freeing the object mid-call. - """ - - # The native destroy (or iterator release) function, set per subclass. - _destroy: Callable[[int], None] - - def _init_handle(self, ptr: int) -> None: - self._ptr: Optional[int] = ptr - self._key = ptr - self._calls = 0 - self._closing = False - self._lock = threading.Lock() - - @classmethod - def _adopt(cls: Type[_H], ptr: int) -> _H: - """Wrap one strong reference the producer handed over.""" - obj = cls.__new__(cls) - obj._init_handle(ptr) - return obj - - def _acquire(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise ValueError(f"{type(self).__name__} is closed") - self._calls += 1 - return ptr - - def _release(self) -> None: - with self._lock: - self._calls -= 1 - if self._calls or not self._closing: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def close(self) -> None: - """Release the native reference. Idempotent; the wrapper is unusable - afterwards.""" - lock = getattr(self, "_lock", None) - if lock is None: - return - with lock: - if self._closing: - return - self._closing = True - if self._calls: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def __enter__(self: _H) -> _H: - return self - - def __exit__(self, *exc: object) -> None: - self.close() - - def __del__(self) -> None: - # An unreachable wrapper has no call in flight: release directly. - ptr = getattr(self, "_ptr", None) - if ptr is not None: - self._ptr = None - type(self)._destroy(ptr) - - -class _Object(_Handle): - """Base of every interface wrapper: a reference-counted native object. - Two wrappers are equal when they hold the same native object.""" - - # The native clone function, set per subclass. - _clone: Callable[[int], int] - - def _clone_ref(self) -> int: - """A new strong reference to the same object, for a receiver that - adopts it (an object token, or a callback method's return).""" - ptr = self._acquire() - try: - return type(self)._clone(ptr) - finally: - self._release() - - def __eq__(self, other: object) -> bool: - if not isinstance(other, _Object): - return NotImplemented - return self is other or (self._ptr is not None and self._ptr == other._ptr) - - def __hash__(self) -> int: - return hash(self._key) - - -class _Iterator(_Handle): - """Base of every lazy iterator over a producer stream: one native `next` - call per step; the handle is released on exhaustion, `close()`, or - garbage collection.""" - - def __iter__(self: _H) -> _H: - return self - - def _step(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise StopIteration - self._calls += 1 - return ptr - - -def _required(ptr: Optional[int]) -> int: - """A non-null object pointer the producer returned.""" - if not ptr: - raise InternalError(-3, "the producer returned a null object") - return ptr - - -def _lend(obj: Any, cls: Type[_Object]) -> int: - """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - return obj._acquire() - - -def _lend_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: - """`_lend` for an optional object: None lends a null pointer.""" - return None if obj is None else _lend(obj, cls) - - -def _release_opt(obj: Optional[_Object]) -> None: - if obj is not None: - obj._release() - - -_I8 = struct.Struct(" InternalError: - return InternalError(-3, f"malformed value buffer: {what}") - - -class _Writer: - """Encodes values in the value-buffer wire format: little-endian, - packed, no alignment.""" - - __slots__ = ("_buf", "_objects") - - def __init__(self) -> None: - self._buf = bytearray() - self._objects: List[Tuple[int, _Object]] = [] - - def finish(self) -> bytes: - # Object tokens are minted last, so an encoding that fails part-way - # never leaks a strong reference. - minted: List[Tuple[_Object, int]] = [] - try: - for pos, obj in self._objects: - ptr = obj._clone_ref() - minted.append((obj, ptr)) - _U64.pack_into(self._buf, pos, ptr) - except BaseException: - for obj, ptr in minted: - type(obj)._destroy(ptr) - raise - self._objects = [] - return bytes(self._buf) - - def write_bool(self, v: bool) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += _I8.pack(v) - - def write_u8(self, v: int) -> None: - self._buf += _U8.pack(v) - - def write_i16(self, v: int) -> None: - self._buf += _I16.pack(v) - - def write_u16(self, v: int) -> None: - self._buf += _U16.pack(v) - - def write_i32(self, v: int) -> None: - self._buf += _I32.pack(v) - - def write_u32(self, v: int) -> None: - self._buf += _U32.pack(v) - - def write_i64(self, v: int) -> None: - self._buf += _I64.pack(v) - - def write_u64(self, v: int) -> None: - self._buf += _U64.pack(v) - - def write_f32(self, v: float) -> None: - self._buf += _F32.pack(v) - - def write_f64(self, v: float) -> None: - self._buf += _F64.pack(v) - - def write_count(self, n: int) -> None: - self._buf += _U32.pack(n) - - def write_flag(self, present: bool) -> None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - data = v.encode("utf-8") - self._buf += _U32.pack(len(data)) - self._buf += data - - def write_bytes(self, v: bytes) -> None: - self._buf += _U32.pack(len(v)) - self._buf += v - - def write_numbers(self, code: str, values: List[Any]) -> None: - """A list of one fixed-width numeric primitive (`code` is its struct - format character), packed in one call.""" - self._buf += _U32.pack(len(values)) - self._buf += struct.pack(f"<{len(values)}{code}", *values) - - def write_object(self, obj: Any, cls: Type[_Object]) -> None: - """Reserve an object token: one strong reference to `obj` the reader - adopts, cloned in `finish()`. A closed wrapper is rejected now.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - obj._acquire() - obj._release() - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -class _Reader: - """Decodes values from the value-buffer wire format, rejecting truncated - buffers, invalid flag bytes, invalid UTF-8, undeclared enum values, and - trailing data.""" - - __slots__ = ("_data", "_pos") - - def __init__(self, data: bytes) -> None: - self._data = data - self._pos = 0 - - def _take(self, n: int, what: str) -> int: - pos = self._pos - if len(self._data) - pos < n: - raise _malformed(f"truncated {what}") - self._pos = pos + n - return pos - - def read_bool(self) -> bool: - b = self._data[self._take(1, "bool")] - if b > 1: - raise _malformed("invalid bool byte") - return b == 1 - - def read_i8(self) -> int: - return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) - - def read_u8(self) -> int: - return self._data[self._take(1, "u8")] - - def read_i16(self) -> int: - return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) - - def read_u16(self) -> int: - return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) - - def read_i32(self) -> int: - return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) - - def read_u32(self) -> int: - return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) - - def read_i64(self) -> int: - return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) - - def read_u64(self) -> int: - return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) - - def read_f32(self) -> float: - return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) - - def read_f64(self) -> float: - return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) - - def read_count(self) -> int: - # An element count, not a byte length: elements can encode to zero - # bytes, so it is not bounded by the remaining data. - return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) - - def read_flag(self) -> bool: - b = self._data[self._take(1, "option flag")] - if b > 1: - raise _malformed("invalid option flag") - return b == 1 - - def read_bytes(self) -> bytes: - n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) - pos = self._take(n, "bytes") - return self._data[pos:pos + n] - - def read_string(self) -> str: - try: - return self.read_bytes().decode("utf-8") - except UnicodeDecodeError: - raise _malformed("string is not valid UTF-8") from None - - def read_enum(self, cls: Type[_E]) -> _E: - value = self.read_i32() - try: - return cls(value) - except ValueError: - raise _malformed(f"undeclared {cls.__name__} value {value}") from None - - def read_numbers(self, code: str, size: int) -> List[Any]: - """A list of one fixed-width numeric primitive, unpacked in one call.""" - n = self.read_count() - pos = self._take(n * size, "list") - return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) - - def read_object(self) -> int: - """An object token: one strong reference the caller adopts.""" - ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) - if ptr == 0: - raise _malformed("null object token") - return ptr - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise _malformed("trailing bytes") - - -def _decode(data: bytes, read: Callable[[_Reader], _T]) -> _T: - """Decode exactly one value from `data`.""" - r = _Reader(data) - value = read(r) - r.expect_end() - return value - - -# Async completions. A call registers its future under an integer key, passes -# the key as the launcher's `context`, and the function's static completion -# trampoline settles it from whichever producer thread completes the call. -# The trampolines live at module scope, so nothing a pending call needs can -# be garbage collected before the producer fires the completion, even when -# the awaiting coroutine is gone. -_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, asyncio.Future[Any]]] = {} -_pending_lock = threading.Lock() -_pending_next = 0 - -_error_free = _bind("docs_everywhere_error_free", None, ctypes.POINTER(_ErrorStruct)) - - -def _async_begin() -> Tuple[int, asyncio.Future[Any]]: - """Register a new pending call on the running event loop.""" - global _pending_next - loop = asyncio.get_running_loop() - future = loop.create_future() - with _pending_lock: - _pending_next += 1 - key = _pending_next - _pending[key] = (loop, future) - return key, future - - -def _async_abandon(key: int) -> None: - """Forget a call whose launcher never ran.""" - with _pending_lock: - _pending.pop(key, None) - - -def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) -> BaseException: - """Copy and release a heap-boxed completion error, then build the - exception it reports. The cancelled code maps to CancelledError.""" - e = err.contents - code = e.code - message = e.message.decode("utf-8", "replace") if e.message else "" - payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" - _error_free(err) - if code == Error.CANCELLED_ERROR_CODE: - return asyncio.CancelledError(message) - return factory(code, message, payload) - - -def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: - """Hand a completion back to the event loop that awaits it.""" - with _pending_lock: - entry = _pending.pop(key or 0, None) - if entry is None: - return - loop, future = entry - try: - loop.call_soon_threadsafe(_async_resolve, future, exc, value) - except RuntimeError: - # The loop is closed, so nothing awaits the result any more. - pass - - -def _async_resolve(future: asyncio.Future[Any], exc: Optional[BaseException], value: Any) -> None: - if future.done(): - return - if isinstance(exc, asyncio.CancelledError): - future.cancel() - elif exc is not None: - future.set_exception(exc) - else: - future.set_result(value) - - -# Callback-interface implementations the producer holds, keyed by the integer -# it receives as `ctx`. An entry is added when an implementation is passed to -# a call and removed when the producer calls the vtable's `free(ctx)`, so the -# implementation lives exactly as long as the producer may call it and the -# producer never holds a raw reference to a Python object. ctypes acquires -# the GIL on entry to a trampoline, so the producer may call (and free) from -# any thread. -_callbacks: Dict[int, Any] = {} -_callbacks_lock = threading.Lock() -_callbacks_next = 0 - -_alloc = _bind("docs_everywhere_alloc", ctypes.c_void_p, ctypes.c_size_t) -_error_set = _bind( - "docs_everywhere_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p -) -_error_set_payload = _bind( - "docs_everywhere_error_set_payload", - None, - ctypes.POINTER(_ErrorStruct), - ctypes.c_char_p, - ctypes.c_size_t, -) - - -def _callback_register(impl: Any, cls: type) -> int: - """Register an implementation for one call; returns its `ctx` key.""" - global _callbacks_next - if not isinstance(impl, cls): - raise TypeError(f"expected {cls.__name__}, got {type(impl).__name__}") - with _callbacks_lock: - _callbacks_next += 1 - ctx = _callbacks_next - _callbacks[ctx] = impl - return ctx - - -def _callback_register_opt(impl: Any, cls: type) -> Optional[int]: - """`_callback_register` for an optional callback: None passes none.""" - return None if impl is None else _callback_register(impl, cls) - - -def _callback_get(ctx: Optional[int]) -> Any: - return _callbacks[ctx or 0] - - -def _callback_free(ctx: Optional[int]) -> None: - # The producer's last reference is gone; it never passes `ctx` again. - with _callbacks_lock: - _callbacks.pop(ctx or 0, None) - - -# Every vtable's `free` entry: one function object for the process lifetime. -_CallbackFree = ctypes.CFUNCTYPE(None, ctypes.c_void_p) -_callback_free_fn = _CallbackFree(_callback_free) - - -def _callback_return_bytes(out_ptr: Any, out_len: Any, data: bytes) -> None: - """Hand a string, bytes, or value-buffer return to the producer: a run - allocated with docs_everywhere_alloc, which the producer adopts and frees.""" - n = len(data) - ptr = _alloc(n) if n else None - if n: - if not ptr: - raise MemoryError(f"docs_everywhere_alloc({n}) failed") - ctypes.memmove(ptr, data, n) - out_ptr[0] = ptr - out_len[0] = n - - -def _callback_return_object(obj: Any, cls: Type[_Object]) -> int: - """Hand an object return to the producer: one fresh strong reference it - adopts.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - return obj._clone_ref() - - -def _callback_return_object_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: - """`_callback_return_object` for an optional object: None returns null.""" - return None if obj is None else _callback_return_object(obj, cls) - - -def _callback_fail(out_err: Any, exc: BaseException, domain: Optional[Type[Error]] = None) -> None: - """Report an implementation's exception to the producer. A method that - declares errors passes its module's error domain, and an exception of - that domain carrying a declared code travels as that code with its - fields; every other exception reaches the producer as the foreign error - code (-4). Nothing unwinds through C.""" - if domain is not None and isinstance(exc, domain) and exc.code > 0: - _error_set(out_err, exc.code, exc.message.encode("utf-8", "replace")) - payload = exc._payload() - if payload: - _error_set_payload(out_err, payload, len(payload)) - return - if isinstance(exc, (Error, InternalError)): - message = exc.message - else: - message = str(exc) or type(exc).__name__ - _error_set(out_err, Error.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) - - -# === Module: docs === - - -class DocsErrorsError(Error): - """Base exception for the `docs` module's error domain.""" - - NotFound: Type[NotFound] - InvalidStatus: Type[InvalidStatus] - - -class NotFound(DocsErrorsError): - """Returned when no document matches the requested id""" - - CODE = 1 - - def __init__(self, message: str = "Document not found") -> None: - super().__init__(1, message) - - -class InvalidStatus(DocsErrorsError): - """Raised when attempting to move into a forbidden state""" - - CODE = 2 - - def __init__(self, message: str = "Invalid status transition") -> None: - super().__init__(2, message) - - -DocsErrorsError.NotFound = NotFound -DocsErrorsError.InvalidStatus = InvalidStatus - - -def _docs_errors_error_from(code: int, message: str, payload: bytes = b"") -> Error: - """The DocsErrorsError subclass for `code` with its payload fields, or the - plain Error for a runtime code (a panic, a marshalling failure). - """ - if code == 1: - return NotFound(message) - if code == 2: - return InvalidStatus(message) - return Error(code, message) - - -class Status(IntEnum): - """Lifecycle status of a document. - - Used to drive UI badges and filters. - """ - - # Document is being edited - Draft = 0 - # Document is publicly visible - Published = 1 - # Document is hidden from listings - Archived = 2 - - -@dataclass -class Document: - """Represents a single document tracked by the system. - - Documents are persisted to disk and exposed via the public API. - """ - - # Stable opaque identifier - id: int - # Human-readable title shown in lists - title: str - # Current lifecycle status - status: Status - # Free-form tags for grouping - tags: List[str] - - -def _write_Document(_w: _Writer, value: Document) -> None: - _w.write_i64(value.id) - _w.write_string(value.title) - _w.write_i32(value.status) - _write_list_string(_w, value.tags) - - -def _read_Document(_r: _Reader) -> Document: - return Document( - id=_r.read_i64(), - title=_r.read_string(), - status=_r.read_enum(Status), - tags=_read_list_string(_r), - ) - - -class SavedListener(abc.ABC): - """Consumer-implemented observer of document-saved notifications. - - Implementations must be safe to call from any thread. - - Subclass it and implement every method to pass an instance where the - API takes a `SavedListener`. The producer may call the methods from any - thread until it releases the instance. An exception a method raises - reaches the producer as Error.FOREIGN_ERROR_CODE (-4). - """ - - @abc.abstractmethod - def on_saved(self, doc_id: int, revision: int) -> None: - """Fires after a document has been persisted successfully - - Parameters - ---------- - doc_id : int - Identifier of the saved document - revision : int - Monotonically increasing revision - """ - - -_SavedListener_on_saved_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int64, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) - - -class _SavedListenerVtable(ctypes.Structure): - """The C vtable `docs_everywhere_docs_SavedListener_vtable`.""" - - _fields_ = [ - ("size", ctypes.c_uint32), - ("flags", ctypes.c_uint32), - ("free", _CallbackFree), - ("on_saved", _SavedListener_on_saved_t), - ] - - -def _SavedListener_on_saved(ctx: Optional[int], doc_id: int, revision: int, out_err: Any) -> None: - try: - _callback_get(ctx).on_saved(doc_id, revision) - except BaseException as exc: - _callback_fail(out_err, exc) - - -_SavedListener_vtable = _SavedListenerVtable( - ctypes.sizeof(_SavedListenerVtable), - 0, - _callback_free_fn, - _SavedListener_on_saved_t(_SavedListener_on_saved), -) -_SavedListener_vtable_ptr = ctypes.addressof(_SavedListener_vtable) - - -_c_docs_Notebook_new = _bind("docs_everywhere_docs_Notebook_new", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_docs_Notebook_insert = _bind("docs_everywhere_docs_Notebook_insert", ctypes.c_int32, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) -_c_docs_Notebook_find = _bind("docs_everywhere_docs_Notebook_find", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -_c_docs_Notebook_size = _bind("docs_everywhere_docs_Notebook_size", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_docs_Notebook_max_documents = _bind("docs_everywhere_docs_Notebook_max_documents", ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) -_c_docs_Notebook_clone = _bind("docs_everywhere_docs_Notebook_clone", ctypes.c_void_p, ctypes.c_void_p) -_c_docs_Notebook_destroy = _bind("docs_everywhere_docs_Notebook_destroy", None, ctypes.c_void_p) - - -class Notebook(_Object): - """An ordered collection of documents. - - Notebooks own their documents and release them when destroyed. - """ - - _destroy = staticmethod(_c_docs_Notebook_destroy) - _clone = staticmethod(_c_docs_Notebook_clone) - - def __init__(self) -> None: - """Create an empty notebook""" - _err = _ErrorStruct() - _ret = _c_docs_Notebook_new(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - self._init_handle(_required(_ret)) - - def insert(self, document: Document) -> int: - """Add a document, returning its position in the notebook - - Parameters - ---------- - document : Document - The document to add - """ - _document_w = _Writer() - _write_Document(_document_w, document) - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _document_b = _document_w.finish() - _ret: int = _c_docs_Notebook_insert(_self_p, _document_b, len(_document_b), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - def find(self, id: int) -> Document: - """Look up a document by identifier, failing when none matches - - Parameters - ---------- - id : int - Identifier of the document to find - - Raises - ------ - DocsErrorsError - If the call reports one of the domain's error codes. - """ - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _self_p = self._acquire() - try: - _ret = _c_docs_Notebook_find(_self_p, id, ctypes.byref(_out_len), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _docs_errors_error_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Document) - - def size(self) -> int: - """The number of documents currently held""" - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _ret: int = _c_docs_Notebook_size(_self_p, ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - @staticmethod - def max_documents() -> int: - """The largest number of documents one notebook will hold""" - _err = _ErrorStruct() - _ret: int = _c_docs_Notebook_max_documents(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - -_c_docs_create_document = _bind("docs_everywhere_docs_create_document", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def create_document(title: str, tags: List[str]) -> Document: - """Create a brand new document. - - The document starts in `Draft` status; callers must publish - it explicitly to make it visible. - - Parameters - ---------- - title : str - Human-readable title for the new document - tags : List[str] - Initial set of tags (may be empty) - """ - _title_b = title.encode("utf-8") - _tags_w = _Writer() - _write_list_string(_tags_w, tags) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _tags_b = _tags_w.finish() - _ret = _c_docs_create_document(_title_b, len(_title_b), _tags_b, len(_tags_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Document) - - -_c_docs_get_document = _bind("docs_everywhere_docs_get_document", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def get_document(id: int) -> Optional[Document]: - """Look up a document by identifier - - Parameters - ---------- - id : int - Identifier returned from create_document - """ - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_docs_get_document(id, ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_opt_Document) - - -_c_docs_publish_async_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_bool) -_c_docs_publish_async = _bind("docs_everywhere_docs_publish_async", None, ctypes.c_int64, _c_docs_publish_async_callback, ctypes.c_void_p) - - -def _docs_publish_async_complete(context: Optional[int], err: Any, result: bool) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _trap_from), None) - else: - _async_settle(context, None, result) - except BaseException as exc: - _async_settle(context, exc, None) - - -_docs_publish_async_completion = _c_docs_publish_async_callback(_docs_publish_async_complete) - - -async def publish_async(id: int) -> bool: - """Publish a document in the background - - Parameters - ---------- - id : int - Identifier of the document to publish - """ - _call, _future = _async_begin() - try: - _c_docs_publish_async(id, _docs_publish_async_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: bool = await _future - return _result - - -_c_docs_watch = _bind("docs_everywhere_docs_watch", None, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) - - -def watch(listener: SavedListener) -> None: - """Register a listener for saved notifications - - Parameters - ---------- - listener : SavedListener - Receiver of saved notifications - """ - _err = _ErrorStruct() - _listener_ctx = _callback_register(listener, SavedListener) - _c_docs_watch(_listener_ctx, _SavedListener_vtable_ptr, ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - - -# === Value-buffer codecs of composite types === - - -def _write_list_string(_w: _Writer, value: List[str]) -> None: - _w.write_count(len(value)) - for item in value: - _w.write_string(item) - - -def _read_list_string(_r: _Reader) -> List[str]: - return [_r.read_string() for _ in range(_r.read_count())] - - -def _write_opt_Document(_w: _Writer, value: Optional[Document]) -> None: - _w.write_flag(value is not None) - if value is not None: - _write_Document(_w, value) - - -def _read_opt_Document(_r: _Reader) -> Optional[Document]: - return _read_Document(_r) if _r.read_flag() else None - - -__all__ = [ - "Error", - "InternalError", - "DocsErrorsError", - "NotFound", - "InvalidStatus", - "Status", - "Document", - "SavedListener", - "Notebook", - "create_document", - "get_document", - "publish_async", - "watch", -] - -# END docs_everywhere.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_py.snap deleted file mode 100644 index a25d8179..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_py.snap +++ /dev/null @@ -1,2477 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -"""ctypes bindings for the edge_cases native library.""" -from __future__ import annotations - -import abc -import asyncio -import builtins -import ctypes -import os -import platform -import struct -import threading -import warnings -from dataclasses import dataclass -from enum import IntEnum -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar - - -class Error(Exception): - """An error a fallible call reports. - - `code` is positive for one of the API's declared error codes, raised as - the declaring module's typed subclasses, and negative for a runtime - failure of a call that declares errors: a generic failure (-1), a - producer panic (-2), a marshalling failure (-3), or an exception raised - by a callback-interface implementation (-4). A cancelled async call (-5) - raises `asyncio.CancelledError` instead. A call that declares no errors - never raises this; its failures are bugs and raise `InternalError`. - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - CANCELLED_ERROR_CODE = -5 - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - def _payload(self) -> bytes: - """The code's fields as a value buffer (a code with fields overrides - this).""" - return b"" - - -class InternalError(RuntimeError): - """A failure of a call that declares no errors: a producer panic (-2), a - marshalling failure (-3), a callback-interface failure the producer let - through (-4), or another runtime failure (-1). These are bugs, so they - are unchecked; `code` and `message` say what went wrong. - """ - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - -class _ErrorStruct(ctypes.Structure): - """The C `edge_cases_error` out-parameter every call reports through.""" - - _fields_ = [ - ("code", ctypes.c_int32), - ("message", ctypes.c_char_p), - ("payload_ptr", ctypes.c_void_p), - ("payload_len", ctypes.c_size_t), - ] - - -def _load_library() -> ctypes.CDLL: - # An explicit path wins; then a library bundled inside this package (a - # platform wheel); then the system loader's search path. - override = os.environ.get("EDGE_CASES_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libedge_cases.dylib" - elif system == "Windows": - name = "edge_cases.dll" - else: - name = "libedge_cases.so" - bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) - if os.path.exists(bundled): - return ctypes.CDLL(bundled) - try: - return ctypes.CDLL(name) - except OSError as exc: - raise ImportError( - f"cannot load the native library {name!r} ({exc}); " - "set EDGE_CASES_LIBRARY to its path" - ) from exc - - -_lib = _load_library() - - -def _bind(name: str, restype: Any, *argtypes: Any) -> Any: - """Look up one exported symbol and fix its C signature. Every prototype - is bound once, at import time.""" - fn = getattr(_lib, name) - fn.restype = restype - fn.argtypes = argtypes - return fn - - -class _ContractEntry(ctypes.Structure): - """The C `edge_cases_contract_entry`: one declaration's id and hash.""" - - _fields_ = [("id", ctypes.c_uint64), ("hash", ctypes.c_uint64)] - - -# The C ABI revision these bindings were generated against and, per -# top-level module's contract symbol, the declarations they rely on: -# (id, hash, dotted path). -_ABI_VERSION = 4 -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { - "edge_cases_edge_cases_contract": [ - (0x056a86511785ba2b, 0x5cba3f16d57c6cdb, "edge_cases.make_widget"), - (0x07bfa9ecb2b720d2, 0x3813b3f57c64bf35, "edge_cases.EdgeErrors"), - (0x0aaa11867e37f991, 0xf090dd52146ebdf0, "edge_cases.Widget.refresh"), - (0x0f9701493898ae4c, 0x6944f47a275c96e8, "edge_cases.Inner"), - (0x1226b1adac179f7d, 0x0e95a00ab0bef637, "edge_cases.async_keywords"), - (0x16d36bef1541136c, 0x5ea7021ec324298f, "edge_cases.find_widget"), - (0x1b7324c55e980feb, 0xabf87fe372358f42, "edge_cases.Widget.old_name"), - (0x1cbd731467c2bab8, 0x4915b9989c2c9074, "edge_cases.Vault"), - (0x2c2a6de67d894b0a, 0x50a4496b1e7191ca, "edge_cases.Vault.open"), - (0x322b4cfcd2d6861c, 0xca7527a379ae8465, "edge_cases.Keywords"), - (0x3f8e23a5f32906f4, 0x8d4c908d796f130d, "edge_cases.Leaf"), - (0x42ab8a685457b97c, 0xe5ce30500cadb32d, "edge_cases.Vault.handle"), - (0x4a68540e2836d3d0, 0xa20e44c71e420ed2, "edge_cases.mode_stream"), - (0x4e030580ac0b111d, 0xb68b7a18d910e175, "edge_cases.legacy_value"), - (0x4f5e4e5291864ef3, 0x55a1a88b21a08f8b, "edge_cases.Legacy"), - (0x4f84b06171b4213d, 0xbaedcfdebb6a2820, "edge_cases.scalar_stream"), - (0x5067a5ff2d9ef910, 0xbfc79b18fc4714fc, "edge_cases.Widget"), - (0x59aff159112fbf5b, 0x18e4def57a8cfa49, "edge_cases.optional_params"), - (0x5daf65b98fa77c48, 0xc817a8cb202e59c2, "edge_cases.keywords_roundtrip"), - (0x5de8a5c9726c2c5a, 0x7c50b5b65f7a896d, "edge_cases.register_batch"), - (0x618a620309994c3c, 0x826ad0b1d208d723, "edge_cases.async_bytes"), - (0x648285cdba0a7b8e, 0xcf6e80e783a4d8be, "edge_cases.WidgetHolder"), - (0x649c78079824b10d, 0x5b12dd26e0634b69, "edge_cases.async_widget"), - (0x7da7a7357d122a37, 0xf544e025ffaa3d20, "edge_cases.async_void"), - (0x7e4015bc3a472e2d, 0xfe22291d90b82e4a, "edge_cases.widget_stream"), - (0x805276e5d81fdba7, 0xd9b64e360bd03b2b, "edge_cases.Outer"), - (0x80e1183326542df4, 0xe8a57e12cc6ed276, "edge_cases.select"), - (0x923ce5e9bcd769a1, 0x0b973f5dc07dda8b, "edge_cases.Vault.to_string"), - (0x98ca8c01f5239404, 0xfe11c93eb6b303ef, "edge_cases.Widget.rename"), - (0xa13e22e98ea278d4, 0xf1fae215d7387800, "edge_cases.widgets_everywhere"), - (0xa84a8c9bf2a881e1, 0xbe1dc840bf221996, "edge_cases.async_optional"), - (0xad958c478fbf205d, 0xe386b0716d24f19b, "edge_cases.Retired"), - (0xb2fc3964ef56ab39, 0x73c325c8bc351236, "edge_cases.async_list"), - (0xb3403af521cf0006, 0x6abc30d48206a3ba, "edge_cases.Widget.close"), - (0xb794304a22a406b8, 0xf6fb98711c516105, "edge_cases.async_struct"), - (0xba3936106a49c47d, 0xb7025f2fadd6288e, "edge_cases.Vault.peek"), - (0xc155b0c3cfb164f4, 0xc777ae57d92ebc47, "edge_cases.noop"), - (0xcdc18607777019dd, 0xb81a31e5c20aa7a8, "edge_cases.inspect_widget"), - (0xd164a82aeb64b8cc, 0xded02c68005c4737, "edge_cases.nested_optional"), - (0xd978ccad2468038f, 0x30c39c65d6235153, "edge_cases.Mode"), - (0xdbe9d947431c9124, 0x39537fb848fdf980, "edge_cases.BatchListener"), - (0xeae8b7862c08bb07, 0xfe12eeaa830452ea, "edge_cases.Keyword"), - (0xf1e155a05ae262b8, 0xb575eb818a423d42, "edge_cases.async_enum"), - (0xf22e210bdbce7343, 0x2a5e0f801bc8c768, "edge_cases.Widget.dispose"), - (0xf3c7cf675563ee6c, 0x0ecffeef399facc8, "edge_cases.Vault.probe"), - (0xfd34bc67acac379e, 0x576caa14f3c0901a, "edge_cases.Widget.fetch"), - (0xff2e9d2bff941786, 0x7e96764ef1f5fbaa, "edge_cases.string_stream"), - ], -} - - -def _check_contract() -> None: - """Refuse to load a library built for another ABI revision, or one that - lacks or changed a declaration these bindings use, before any other - symbol is bound.""" - try: - abi_version = _bind("edge_cases_abi_version", ctypes.c_uint32) - except AttributeError: - raise ImportError( - f"{_lib._name} does not export edge_cases_abi_version; " - "it is not the library these bindings were generated for" - ) from None - found = abi_version() - if found != _ABI_VERSION: - raise ImportError( - f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " - f"but {_lib._name} reports revision {found}" - ) - for symbol, expected in _CONTRACTS.items(): - try: - contract = _bind( - symbol, ctypes.POINTER(_ContractEntry), ctypes.POINTER(ctypes.c_size_t) - ) - except AttributeError: - raise ImportError(f"{_lib._name} does not export {symbol}") from None - length = ctypes.c_size_t() - table = contract(ctypes.byref(length)) - hashes = {table[i].id: table[i].hash for i in range(length.value)} if table else {} - for entry_id, entry_hash, path in expected: - actual = hashes.get(entry_id) - if actual is None: - raise ImportError(f"{path} is missing from the library ({_lib._name})") - if actual != entry_hash: - raise ImportError( - f"{path} changed since these bindings were generated ({_lib._name})" - ) - - -_check_contract() - -_error_clear = _bind("edge_cases_error_clear", None, ctypes.POINTER(_ErrorStruct)) -_free_bytes = _bind("edge_cases_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) -_debug_live_fn = _bind("edge_cases_debug_live", ctypes.c_uint64, ctypes.c_int32) - - -def _debug_live(kind: int) -> int: - """The producer's live-resource counter `kind` (0 objects, 1 callbacks, - 2 iterators, 3 cancel tokens, 4 byte runs; -1 is 1 when the producer - counts at all). Always 0 unless the producer was built with its leak - counters enabled.""" - live: int = _debug_live_fn(kind) - return live - - -def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: - """Copy a filled out-err slot's code, message, and payload, then release - them.""" - code = err.code - message = err.message.decode("utf-8", "replace") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _error_clear(ctypes.byref(err)) - return code, message, payload - - -def _trap_from(code: int, message: str, payload: bytes = b"") -> InternalError: - """The exception for a failed call that declares no errors.""" - return InternalError(code, message) - - -def _take_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a producer-owned byte run (a string, bytes, or value buffer) and - release it with edge_cases_free_bytes.""" - if not ptr: - return b"" - try: - return ctypes.string_at(ptr, length) - finally: - _free_bytes(ptr, length) - - -def _take_str(ptr: Optional[int], length: int) -> str: - """Copy and release a producer-owned UTF-8 string.""" - return _take_bytes(ptr, length).decode("utf-8") - - -def _peek_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a byte run the producer only lends for the current call.""" - return ctypes.string_at(ptr, length) if ptr and length else b"" - - -_H = TypeVar("_H", bound="_Handle") - - -class _Handle: - """Owns one native reference and releases it exactly once. - - Calls lend the pointer through `_acquire`/`_release`, so `close()` from - another thread while a call is in flight defers the release until the - last such call returns instead of freeing the object mid-call. - """ - - # The native destroy (or iterator release) function, set per subclass. - _destroy: Callable[[int], None] - - def _init_handle(self, ptr: int) -> None: - self._ptr: Optional[int] = ptr - self._key = ptr - self._calls = 0 - self._closing = False - self._lock = threading.Lock() - - @classmethod - def _adopt(cls: Type[_H], ptr: int) -> _H: - """Wrap one strong reference the producer handed over.""" - obj = cls.__new__(cls) - obj._init_handle(ptr) - return obj - - def _acquire(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise ValueError(f"{type(self).__name__} is closed") - self._calls += 1 - return ptr - - def _release(self) -> None: - with self._lock: - self._calls -= 1 - if self._calls or not self._closing: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def close(self) -> None: - """Release the native reference. Idempotent; the wrapper is unusable - afterwards.""" - lock = getattr(self, "_lock", None) - if lock is None: - return - with lock: - if self._closing: - return - self._closing = True - if self._calls: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def __enter__(self: _H) -> _H: - return self - - def __exit__(self, *exc: object) -> None: - self.close() - - def __del__(self) -> None: - # An unreachable wrapper has no call in flight: release directly. - ptr = getattr(self, "_ptr", None) - if ptr is not None: - self._ptr = None - type(self)._destroy(ptr) - - -class _Object(_Handle): - """Base of every interface wrapper: a reference-counted native object. - Two wrappers are equal when they hold the same native object.""" - - # The native clone function, set per subclass. - _clone: Callable[[int], int] - - def _clone_ref(self) -> int: - """A new strong reference to the same object, for a receiver that - adopts it (an object token, or a callback method's return).""" - ptr = self._acquire() - try: - return type(self)._clone(ptr) - finally: - self._release() - - def __eq__(self, other: object) -> bool: - if not isinstance(other, _Object): - return NotImplemented - return self is other or (self._ptr is not None and self._ptr == other._ptr) - - def __hash__(self) -> int: - return hash(self._key) - - -class _Iterator(_Handle): - """Base of every lazy iterator over a producer stream: one native `next` - call per step; the handle is released on exhaustion, `close()`, or - garbage collection.""" - - def __iter__(self: _H) -> _H: - return self - - def _step(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise StopIteration - self._calls += 1 - return ptr - - -def _required(ptr: Optional[int]) -> int: - """A non-null object pointer the producer returned.""" - if not ptr: - raise InternalError(-3, "the producer returned a null object") - return ptr - - -def _lend(obj: Any, cls: Type[_Object]) -> int: - """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - return obj._acquire() - - -def _lend_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: - """`_lend` for an optional object: None lends a null pointer.""" - return None if obj is None else _lend(obj, cls) - - -def _release_opt(obj: Optional[_Object]) -> None: - if obj is not None: - obj._release() - - -_I8 = struct.Struct(" InternalError: - return InternalError(-3, f"malformed value buffer: {what}") - - -class _Writer: - """Encodes values in the value-buffer wire format: little-endian, - packed, no alignment.""" - - __slots__ = ("_buf", "_objects") - - def __init__(self) -> None: - self._buf = bytearray() - self._objects: List[Tuple[int, _Object]] = [] - - def finish(self) -> bytes: - # Object tokens are minted last, so an encoding that fails part-way - # never leaks a strong reference. - minted: List[Tuple[_Object, int]] = [] - try: - for pos, obj in self._objects: - ptr = obj._clone_ref() - minted.append((obj, ptr)) - _U64.pack_into(self._buf, pos, ptr) - except BaseException: - for obj, ptr in minted: - type(obj)._destroy(ptr) - raise - self._objects = [] - return bytes(self._buf) - - def write_bool(self, v: bool) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += _I8.pack(v) - - def write_u8(self, v: int) -> None: - self._buf += _U8.pack(v) - - def write_i16(self, v: int) -> None: - self._buf += _I16.pack(v) - - def write_u16(self, v: int) -> None: - self._buf += _U16.pack(v) - - def write_i32(self, v: int) -> None: - self._buf += _I32.pack(v) - - def write_u32(self, v: int) -> None: - self._buf += _U32.pack(v) - - def write_i64(self, v: int) -> None: - self._buf += _I64.pack(v) - - def write_u64(self, v: int) -> None: - self._buf += _U64.pack(v) - - def write_f32(self, v: float) -> None: - self._buf += _F32.pack(v) - - def write_f64(self, v: float) -> None: - self._buf += _F64.pack(v) - - def write_count(self, n: int) -> None: - self._buf += _U32.pack(n) - - def write_flag(self, present: bool) -> None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - data = v.encode("utf-8") - self._buf += _U32.pack(len(data)) - self._buf += data - - def write_bytes(self, v: bytes) -> None: - self._buf += _U32.pack(len(v)) - self._buf += v - - def write_numbers(self, code: str, values: List[Any]) -> None: - """A list of one fixed-width numeric primitive (`code` is its struct - format character), packed in one call.""" - self._buf += _U32.pack(len(values)) - self._buf += struct.pack(f"<{len(values)}{code}", *values) - - def write_object(self, obj: Any, cls: Type[_Object]) -> None: - """Reserve an object token: one strong reference to `obj` the reader - adopts, cloned in `finish()`. A closed wrapper is rejected now.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - obj._acquire() - obj._release() - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -class _Reader: - """Decodes values from the value-buffer wire format, rejecting truncated - buffers, invalid flag bytes, invalid UTF-8, undeclared enum values, and - trailing data.""" - - __slots__ = ("_data", "_pos") - - def __init__(self, data: bytes) -> None: - self._data = data - self._pos = 0 - - def _take(self, n: int, what: str) -> int: - pos = self._pos - if len(self._data) - pos < n: - raise _malformed(f"truncated {what}") - self._pos = pos + n - return pos - - def read_bool(self) -> bool: - b = self._data[self._take(1, "bool")] - if b > 1: - raise _malformed("invalid bool byte") - return b == 1 - - def read_i8(self) -> int: - return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) - - def read_u8(self) -> int: - return self._data[self._take(1, "u8")] - - def read_i16(self) -> int: - return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) - - def read_u16(self) -> int: - return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) - - def read_i32(self) -> int: - return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) - - def read_u32(self) -> int: - return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) - - def read_i64(self) -> int: - return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) - - def read_u64(self) -> int: - return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) - - def read_f32(self) -> float: - return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) - - def read_f64(self) -> float: - return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) - - def read_count(self) -> int: - # An element count, not a byte length: elements can encode to zero - # bytes, so it is not bounded by the remaining data. - return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) - - def read_flag(self) -> bool: - b = self._data[self._take(1, "option flag")] - if b > 1: - raise _malformed("invalid option flag") - return b == 1 - - def read_bytes(self) -> bytes: - n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) - pos = self._take(n, "bytes") - return self._data[pos:pos + n] - - def read_string(self) -> str: - try: - return self.read_bytes().decode("utf-8") - except UnicodeDecodeError: - raise _malformed("string is not valid UTF-8") from None - - def read_enum(self, cls: Type[_E]) -> _E: - value = self.read_i32() - try: - return cls(value) - except ValueError: - raise _malformed(f"undeclared {cls.__name__} value {value}") from None - - def read_numbers(self, code: str, size: int) -> List[Any]: - """A list of one fixed-width numeric primitive, unpacked in one call.""" - n = self.read_count() - pos = self._take(n * size, "list") - return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) - - def read_object(self) -> int: - """An object token: one strong reference the caller adopts.""" - ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) - if ptr == 0: - raise _malformed("null object token") - return ptr - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise _malformed("trailing bytes") - - -def _decode(data: bytes, read: Callable[[_Reader], _T]) -> _T: - """Decode exactly one value from `data`.""" - r = _Reader(data) - value = read(r) - r.expect_end() - return value - - -# Async completions. A call registers its future under an integer key, passes -# the key as the launcher's `context`, and the function's static completion -# trampoline settles it from whichever producer thread completes the call. -# The trampolines live at module scope, so nothing a pending call needs can -# be garbage collected before the producer fires the completion, even when -# the awaiting coroutine is gone. -_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, asyncio.Future[Any]]] = {} -_pending_lock = threading.Lock() -_pending_next = 0 - -_error_free = _bind("edge_cases_error_free", None, ctypes.POINTER(_ErrorStruct)) - - -def _async_begin() -> Tuple[int, asyncio.Future[Any]]: - """Register a new pending call on the running event loop.""" - global _pending_next - loop = asyncio.get_running_loop() - future = loop.create_future() - with _pending_lock: - _pending_next += 1 - key = _pending_next - _pending[key] = (loop, future) - return key, future - - -def _async_abandon(key: int) -> None: - """Forget a call whose launcher never ran.""" - with _pending_lock: - _pending.pop(key, None) - - -def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) -> BaseException: - """Copy and release a heap-boxed completion error, then build the - exception it reports. The cancelled code maps to CancelledError.""" - e = err.contents - code = e.code - message = e.message.decode("utf-8", "replace") if e.message else "" - payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" - _error_free(err) - if code == Error.CANCELLED_ERROR_CODE: - return asyncio.CancelledError(message) - return factory(code, message, payload) - - -def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: - """Hand a completion back to the event loop that awaits it.""" - with _pending_lock: - entry = _pending.pop(key or 0, None) - if entry is None: - return - loop, future = entry - try: - loop.call_soon_threadsafe(_async_resolve, future, exc, value) - except RuntimeError: - # The loop is closed, so nothing awaits the result any more. - pass - - -def _async_resolve(future: asyncio.Future[Any], exc: Optional[BaseException], value: Any) -> None: - if future.done(): - return - if isinstance(exc, asyncio.CancelledError): - future.cancel() - elif exc is not None: - future.set_exception(exc) - else: - future.set_result(value) - - -# Cancellation for `cancellable` async functions. Each call creates a token, -# the producer takes its own reference at launch, and cancelling the awaiting -# task cancels the token; the producer then completes the call with the -# cancelled code. -_cancel_token_create = _bind("edge_cases_cancel_token_create", ctypes.c_void_p) -_cancel_token_cancel = _bind("edge_cases_cancel_token_cancel", None, ctypes.c_void_p) -_cancel_token_destroy = _bind("edge_cases_cancel_token_destroy", None, ctypes.c_void_p) - - -async def _async_wait_cancellable(future: asyncio.Future[Any], token: int) -> Any: - """Await a cancellable call. Cancelling the awaiting task cancels the - native call and waits for its (cancelled) completion before raising, so - nothing outlives the call.""" - try: - return await asyncio.shield(future) - except asyncio.CancelledError: - _cancel_token_cancel(token) - try: - await future - except BaseException: - pass - raise - finally: - _cancel_token_destroy(token) - - -# Callback-interface implementations the producer holds, keyed by the integer -# it receives as `ctx`. An entry is added when an implementation is passed to -# a call and removed when the producer calls the vtable's `free(ctx)`, so the -# implementation lives exactly as long as the producer may call it and the -# producer never holds a raw reference to a Python object. ctypes acquires -# the GIL on entry to a trampoline, so the producer may call (and free) from -# any thread. -_callbacks: Dict[int, Any] = {} -_callbacks_lock = threading.Lock() -_callbacks_next = 0 - -_alloc = _bind("edge_cases_alloc", ctypes.c_void_p, ctypes.c_size_t) -_error_set = _bind( - "edge_cases_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p -) -_error_set_payload = _bind( - "edge_cases_error_set_payload", - None, - ctypes.POINTER(_ErrorStruct), - ctypes.c_char_p, - ctypes.c_size_t, -) - - -def _callback_register(impl: Any, cls: type) -> int: - """Register an implementation for one call; returns its `ctx` key.""" - global _callbacks_next - if not isinstance(impl, cls): - raise TypeError(f"expected {cls.__name__}, got {type(impl).__name__}") - with _callbacks_lock: - _callbacks_next += 1 - ctx = _callbacks_next - _callbacks[ctx] = impl - return ctx - - -def _callback_register_opt(impl: Any, cls: type) -> Optional[int]: - """`_callback_register` for an optional callback: None passes none.""" - return None if impl is None else _callback_register(impl, cls) - - -def _callback_get(ctx: Optional[int]) -> Any: - return _callbacks[ctx or 0] - - -def _callback_free(ctx: Optional[int]) -> None: - # The producer's last reference is gone; it never passes `ctx` again. - with _callbacks_lock: - _callbacks.pop(ctx or 0, None) - - -# Every vtable's `free` entry: one function object for the process lifetime. -_CallbackFree = ctypes.CFUNCTYPE(None, ctypes.c_void_p) -_callback_free_fn = _CallbackFree(_callback_free) - - -def _callback_return_bytes(out_ptr: Any, out_len: Any, data: bytes) -> None: - """Hand a string, bytes, or value-buffer return to the producer: a run - allocated with edge_cases_alloc, which the producer adopts and frees.""" - n = len(data) - ptr = _alloc(n) if n else None - if n: - if not ptr: - raise MemoryError(f"edge_cases_alloc({n}) failed") - ctypes.memmove(ptr, data, n) - out_ptr[0] = ptr - out_len[0] = n - - -def _callback_return_object(obj: Any, cls: Type[_Object]) -> int: - """Hand an object return to the producer: one fresh strong reference it - adopts.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - return obj._clone_ref() - - -def _callback_return_object_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: - """`_callback_return_object` for an optional object: None returns null.""" - return None if obj is None else _callback_return_object(obj, cls) - - -def _callback_fail(out_err: Any, exc: BaseException, domain: Optional[Type[Error]] = None) -> None: - """Report an implementation's exception to the producer. A method that - declares errors passes its module's error domain, and an exception of - that domain carrying a declared code travels as that code with its - fields; every other exception reaches the producer as the foreign error - code (-4). Nothing unwinds through C.""" - if domain is not None and isinstance(exc, domain) and exc.code > 0: - _error_set(out_err, exc.code, exc.message.encode("utf-8", "replace")) - payload = exc._payload() - if payload: - _error_set_payload(out_err, payload, len(payload)) - return - if isinstance(exc, (Error, InternalError)): - message = exc.message - else: - message = str(exc) or type(exc).__name__ - _error_set(out_err, Error.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) - - -# === Module: edge_cases === - - -class EdgeErrorsError(Error): - """Base exception for the `edge_cases` module's error domain.""" - - Busy: Type[Busy] - - -class Busy(EdgeErrorsError): - """Resource is busy""" - - CODE = 7 - - def __init__(self, message: str = "Resource is busy") -> None: - super().__init__(7, message) - - -EdgeErrorsError.Busy = Busy - - -def _edge_errors_error_from(code: int, message: str, payload: bytes = b"") -> Error: - """The EdgeErrorsError subclass for `code` with its payload fields, or the - plain Error for a runtime code (a panic, a marshalling failure). - """ - if code == 7: - return Busy(message) - return Error(code, message) - - -class Legacy(IntEnum): - """Deprecated enum with non-contiguous values and keyword-like variants - - Deprecated: Use Mode instead - """ - - None_ = 0 - Default = 5 - Class = 10 - - -class Mode(IntEnum): - Fast = 1 - Safe = 2 - - -@dataclass -class Leaf: - weight: float - - -def _write_Leaf(_w: _Writer, value: Leaf) -> None: - _w.write_f32(value.weight) - - -def _read_Leaf(_r: _Reader) -> Leaf: - return Leaf( - weight=_r.read_f32(), - ) - - -@dataclass -class Inner: - """Middle layer of a three-deep struct nesting""" - - leaf: Leaf - leaves: List[Leaf] - - -def _write_Inner(_w: _Writer, value: Inner) -> None: - _write_Leaf(_w, value.leaf) - _write_list_Leaf(_w, value.leaves) - - -def _read_Inner(_r: _Reader) -> Inner: - return Inner( - leaf=_read_Leaf(_r), - leaves=_read_list_Leaf(_r), - ) - - -@dataclass -class Outer: - inner: Inner - maybe_inner: Optional[Inner] - by_name: Dict[str, Inner] - modes: List[Mode] - matrix: List[List[int]] - sparse: List[Optional[int]] - blob: Optional[bytes] - note: Optional[str] - by_id: Dict[int, str] - _hidden: bool - v2: int - - -def _write_Outer(_w: _Writer, value: Outer) -> None: - _write_Inner(_w, value.inner) - _write_opt_Inner(_w, value.maybe_inner) - _write_map_string_Inner(_w, value.by_name) - _write_list_Mode(_w, value.modes) - _write_list_list_i32(_w, value.matrix) - _write_list_opt_i64(_w, value.sparse) - _write_opt_bytes(_w, value.blob) - _write_opt_string(_w, value.note) - _write_map_i64_string(_w, value.by_id) - _w.write_bool(value._hidden) - _w.write_u8(value.v2) - - -def _read_Outer(_r: _Reader) -> Outer: - return Outer( - inner=_read_Inner(_r), - maybe_inner=_read_opt_Inner(_r), - by_name=_read_map_string_Inner(_r), - modes=_read_list_Mode(_r), - matrix=_read_list_list_i32(_r), - sparse=_read_list_opt_i64(_r), - blob=_read_opt_bytes(_r), - note=_read_opt_string(_r), - by_id=_read_map_i64_string(_r), - _hidden=_r.read_bool(), - v2=_r.read_u8(), - ) - - -@dataclass -class Keywords: - """Every field is a reserved word somewhere""" - - class_: builtins.int - def_: builtins.int - import_: builtins.int - from_: builtins.int - func: builtins.int - range: builtins.int - register: builtins.int - volatile: builtins.int - new: builtins.int - default: builtins.int - end: builtins.int - in_: builtins.int - is_: builtins.int - int: builtins.int - var: builtins.int - val: builtins.int - self: builtins.int - this: builtins.int - object: builtins.int - string: builtins.int - case: builtins.int - select: builtins.int - switch: builtins.int - void: builtins.int - union: builtins.int - namespace: builtins.int - operator: builtins.int - template: builtins.int - package: builtins.int - protocol: builtins.int - guard: builtins.int - defer: builtins.int - inout: builtins.int - internal: builtins.int - when: builtins.int - fun: builtins.int - data: builtins.int - late: builtins.int - part: builtins.int - out: builtins.int - ref: builtins.int - event: builtins.int - base: builtins.int - lambda_: builtins.int - yield_: builtins.int - nil: builtins.int - null: builtins.int - begin: builtins.int - rescue: builtins.int - module: builtins.int - chan: builtins.int - go: builtins.int - let: builtins.int - export: builtins.int - delete: builtins.int - typedef: builtins.int - sizeof: builtins.int - static: builtins.int - super: builtins.int - - -def _write_Keywords(_w: _Writer, value: Keywords) -> None: - _w.write_i32(value.class_) - _w.write_i32(value.def_) - _w.write_i32(value.import_) - _w.write_i32(value.from_) - _w.write_i32(value.func) - _w.write_i32(value.range) - _w.write_i32(value.register) - _w.write_i32(value.volatile) - _w.write_i32(value.new) - _w.write_i32(value.default) - _w.write_i32(value.end) - _w.write_i32(value.in_) - _w.write_i32(value.is_) - _w.write_i32(value.int) - _w.write_i32(value.var) - _w.write_i32(value.val) - _w.write_i32(value.self) - _w.write_i32(value.this) - _w.write_i32(value.object) - _w.write_i32(value.string) - _w.write_i32(value.case) - _w.write_i32(value.select) - _w.write_i32(value.switch) - _w.write_i32(value.void) - _w.write_i32(value.union) - _w.write_i32(value.namespace) - _w.write_i32(value.operator) - _w.write_i32(value.template) - _w.write_i32(value.package) - _w.write_i32(value.protocol) - _w.write_i32(value.guard) - _w.write_i32(value.defer) - _w.write_i32(value.inout) - _w.write_i32(value.internal) - _w.write_i32(value.when) - _w.write_i32(value.fun) - _w.write_i32(value.data) - _w.write_i32(value.late) - _w.write_i32(value.part) - _w.write_i32(value.out) - _w.write_i32(value.ref) - _w.write_i32(value.event) - _w.write_i32(value.base) - _w.write_i32(value.lambda_) - _w.write_i32(value.yield_) - _w.write_i32(value.nil) - _w.write_i32(value.null) - _w.write_i32(value.begin) - _w.write_i32(value.rescue) - _w.write_i32(value.module) - _w.write_i32(value.chan) - _w.write_i32(value.go) - _w.write_i32(value.let) - _w.write_i32(value.export) - _w.write_i32(value.delete) - _w.write_i32(value.typedef) - _w.write_i32(value.sizeof) - _w.write_i32(value.static) - _w.write_i32(value.super) - - -def _read_Keywords(_r: _Reader) -> Keywords: - return Keywords( - class_=_r.read_i32(), - def_=_r.read_i32(), - import_=_r.read_i32(), - from_=_r.read_i32(), - func=_r.read_i32(), - range=_r.read_i32(), - register=_r.read_i32(), - volatile=_r.read_i32(), - new=_r.read_i32(), - default=_r.read_i32(), - end=_r.read_i32(), - in_=_r.read_i32(), - is_=_r.read_i32(), - int=_r.read_i32(), - var=_r.read_i32(), - val=_r.read_i32(), - self=_r.read_i32(), - this=_r.read_i32(), - object=_r.read_i32(), - string=_r.read_i32(), - case=_r.read_i32(), - select=_r.read_i32(), - switch=_r.read_i32(), - void=_r.read_i32(), - union=_r.read_i32(), - namespace=_r.read_i32(), - operator=_r.read_i32(), - template=_r.read_i32(), - package=_r.read_i32(), - protocol=_r.read_i32(), - guard=_r.read_i32(), - defer=_r.read_i32(), - inout=_r.read_i32(), - internal=_r.read_i32(), - when=_r.read_i32(), - fun=_r.read_i32(), - data=_r.read_i32(), - late=_r.read_i32(), - part=_r.read_i32(), - out=_r.read_i32(), - ref=_r.read_i32(), - event=_r.read_i32(), - base=_r.read_i32(), - lambda_=_r.read_i32(), - yield_=_r.read_i32(), - nil=_r.read_i32(), - null=_r.read_i32(), - begin=_r.read_i32(), - rescue=_r.read_i32(), - module=_r.read_i32(), - chan=_r.read_i32(), - go=_r.read_i32(), - let=_r.read_i32(), - export=_r.read_i32(), - delete=_r.read_i32(), - typedef=_r.read_i32(), - sizeof=_r.read_i32(), - static=_r.read_i32(), - super=_r.read_i32(), - ) - - -@dataclass -class Retired: - """Deprecated struct - - Deprecated: Use Outer instead - """ - - id: int - - -def _write_Retired(_w: _Writer, value: Retired) -> None: - _w.write_i64(value.id) - - -def _read_Retired(_r: _Reader) -> Retired: - return Retired( - id=_r.read_i64(), - ) - - -@dataclass -class WidgetHolder: - """Record carrying objects in every buffered position""" - - primary: Widget - fallback: Optional[Widget] - extras: List[Widget] - by_name: Dict[str, Widget] - - -def _write_WidgetHolder(_w: _Writer, value: WidgetHolder) -> None: - _w.write_object(value.primary, Widget) - _write_opt_Widget(_w, value.fallback) - _write_list_Widget(_w, value.extras) - _write_map_string_Widget(_w, value.by_name) - - -def _read_WidgetHolder(_r: _Reader) -> WidgetHolder: - return WidgetHolder( - primary=Widget._adopt(_r.read_object()), - fallback=_read_opt_Widget(_r), - extras=_read_list_Widget(_r), - by_name=_read_map_string_Widget(_r), - ) - - -class BatchListener(abc.ABC): - """Callback interface carrying composite payloads and objects - - Subclass it and implement every method to pass an instance where the - API takes a `BatchListener`. The producer may call the methods from any - thread until it releases the instance. An exception a method raises - reaches the producer as Error.FOREIGN_ERROR_CODE (-4). - """ - - @abc.abstractmethod - def on_batch(self, items: List[Outer], raw: bytes, first: Optional[Outer], mode: Mode) -> None: - """Composite payloads cross as value buffers""" - - @abc.abstractmethod - def on_widget(self, widget: Widget, fallback: Optional[Widget], children: List[Widget]) -> None: - """Objects cross as strong references the consumer must release""" - - @abc.abstractmethod - def on_tick(self) -> None: - ... - - @abc.abstractmethod - def should_continue(self, class_: int) -> bool: - """Scalar return travels back through the vtable""" - - @abc.abstractmethod - def pick_mode(self) -> Mode: - """Enum return travels back as its integer discriminant""" - - -_BatchListener_on_batch_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) -_BatchListener_on_widget_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) -_BatchListener_on_tick_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_BatchListener_should_continue_t = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) -_BatchListener_pick_mode_t = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) - - -class _BatchListenerVtable(ctypes.Structure): - """The C vtable `edge_cases_edge_cases_BatchListener_vtable`.""" - - _fields_ = [ - ("size", ctypes.c_uint32), - ("flags", ctypes.c_uint32), - ("free", _CallbackFree), - ("on_batch", _BatchListener_on_batch_t), - ("on_widget", _BatchListener_on_widget_t), - ("on_tick", _BatchListener_on_tick_t), - ("should_continue", _BatchListener_should_continue_t), - ("pick_mode", _BatchListener_pick_mode_t), - ] - - -def _BatchListener_on_batch(ctx: Optional[int], items_ptr: Optional[int], items_len: int, raw_ptr: Optional[int], raw_len: int, first_ptr: Optional[int], first_len: int, mode: int, out_err: Any) -> None: - try: - _callback_get(ctx).on_batch(_decode(_peek_bytes(items_ptr, items_len), _read_list_Outer), _peek_bytes(raw_ptr, raw_len), _decode(_peek_bytes(first_ptr, first_len), _read_opt_Outer), Mode(mode)) - except BaseException as exc: - _callback_fail(out_err, exc) - - -def _BatchListener_on_widget(ctx: Optional[int], widget: Optional[int], fallback: Optional[int], children_ptr: Optional[int], children_len: int, out_err: Any) -> None: - try: - _widget = Widget._adopt(_required(widget)) - _fallback = Widget._adopt(fallback) if fallback else None - _children = _decode(_peek_bytes(children_ptr, children_len), _read_list_Widget) - _callback_get(ctx).on_widget(_widget, _fallback, _children) - except BaseException as exc: - _callback_fail(out_err, exc) - - -def _BatchListener_on_tick(ctx: Optional[int], out_err: Any) -> None: - try: - _callback_get(ctx).on_tick() - except BaseException as exc: - _callback_fail(out_err, exc) - - -def _BatchListener_should_continue(ctx: Optional[int], class_: int, out_err: Any) -> bool: - try: - return bool(_callback_get(ctx).should_continue(class_)) - except BaseException as exc: - _callback_fail(out_err, exc) - return False - - -def _BatchListener_pick_mode(ctx: Optional[int], out_err: Any) -> int: - try: - return int(_callback_get(ctx).pick_mode()) - except BaseException as exc: - _callback_fail(out_err, exc) - return 0 - - -_BatchListener_vtable = _BatchListenerVtable( - ctypes.sizeof(_BatchListenerVtable), - 0, - _callback_free_fn, - _BatchListener_on_batch_t(_BatchListener_on_batch), - _BatchListener_on_widget_t(_BatchListener_on_widget), - _BatchListener_on_tick_t(_BatchListener_on_tick), - _BatchListener_should_continue_t(_BatchListener_should_continue), - _BatchListener_pick_mode_t(_BatchListener_pick_mode), -) -_BatchListener_vtable_ptr = ctypes.addressof(_BatchListener_vtable) - - -class Keyword(abc.ABC): - """Callback interface whose name and methods are keywords - - Subclass it and implement every method to pass an instance where the - API takes a `Keyword`. The producer may call the methods from any - thread until it releases the instance. An exception a method raises - reaches the producer as Error.FOREIGN_ERROR_CODE (-4). - """ - - @abc.abstractmethod - def delete(self, self_: int) -> None: - ... - - @abc.abstractmethod - def import_(self) -> int: - ... - - -_Keyword_delete_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) -_Keyword_import_t = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) - - -class _KeywordVtable(ctypes.Structure): - """The C vtable `edge_cases_edge_cases_Keyword_vtable`.""" - - _fields_ = [ - ("size", ctypes.c_uint32), - ("flags", ctypes.c_uint32), - ("free", _CallbackFree), - ("delete", _Keyword_delete_t), - ("import", _Keyword_import_t), - ] - - -def _Keyword_delete(ctx: Optional[int], self_: int, out_err: Any) -> None: - try: - _callback_get(ctx).delete(self_) - except BaseException as exc: - _callback_fail(out_err, exc) - - -def _Keyword_import(ctx: Optional[int], out_err: Any) -> int: - try: - return int(_callback_get(ctx).import_()) - except BaseException as exc: - _callback_fail(out_err, exc) - return 0 - - -_Keyword_vtable = _KeywordVtable( - ctypes.sizeof(_KeywordVtable), - 0, - _callback_free_fn, - _Keyword_delete_t(_Keyword_delete), - _Keyword_import_t(_Keyword_import), -) -_Keyword_vtable_ptr = ctypes.addressof(_Keyword_vtable) - - -_c_edge_cases_Widget_refresh = _bind("edge_cases_edge_cases_Widget_refresh", None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Widget_rename = _bind("edge_cases_edge_cases_Widget_rename", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -_c_edge_cases_Widget_fetch_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) -_c_edge_cases_Widget_fetch = _bind("edge_cases_edge_cases_Widget_fetch", None, ctypes.c_void_p, ctypes.c_int64, _c_edge_cases_Widget_fetch_callback, ctypes.c_void_p) - - -def _edge_cases_Widget_fetch_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _edge_errors_error_from), None) - else: - _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), _read_Outer)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_Widget_fetch_completion = _c_edge_cases_Widget_fetch_callback(_edge_cases_Widget_fetch_complete) - - -_c_edge_cases_Widget_old_name = _bind("edge_cases_edge_cases_Widget_old_name", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Widget_close = _bind("edge_cases_edge_cases_Widget_close", ctypes.c_bool, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Widget_dispose = _bind("edge_cases_edge_cases_Widget_dispose", None, ctypes.c_void_p, ctypes.c_bool, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Widget_clone = _bind("edge_cases_edge_cases_Widget_clone", ctypes.c_void_p, ctypes.c_void_p) -_c_edge_cases_Widget_destroy = _bind("edge_cases_edge_cases_Widget_destroy", None, ctypes.c_void_p) - - -class Widget(_Object): - """Interface with no constructor; instances come from make_widget""" - - _destroy = staticmethod(_c_edge_cases_Widget_destroy) - _clone = staticmethod(_c_edge_cases_Widget_clone) - - def __init__(self) -> None: - raise TypeError("Widget cannot be instantiated directly") - - def refresh(self) -> None: - """Void method that can fail - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _c_edge_cases_Widget_refresh(_self_p, ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _edge_errors_error_from(*_read_error(_err)) - - def rename(self, class_: str, import_: Optional[str]) -> str: - _class__b = class_.encode("utf-8") - _import__w = _Writer() - _write_opt_string(_import__w, import_) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _self_p = self._acquire() - try: - _import__b = _import__w.finish() - _ret = _c_edge_cases_Widget_rename(_self_p, _class__b, len(_class__b), _import__b, len(_import__b), ctypes.byref(_out_len), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _take_str(_ret, _out_len.value) - - async def fetch(self, id: int) -> Outer: - """Async method with a struct result - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _call, _future = _async_begin() - try: - _self_p = self._acquire() - try: - _c_edge_cases_Widget_fetch(_self_p, id, _edge_cases_Widget_fetch_completion, _call) - finally: - self._release() - except BaseException: - _async_abandon(_call) - raise - _result: Outer = await _future - return _result - - def old_name(self) -> str: - """Deprecated: Use rename instead""" - warnings.warn("Use rename instead", DeprecationWarning, stacklevel=2) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _self_p = self._acquire() - try: - _ret = _c_edge_cases_Widget_old_name(_self_p, ctypes.byref(_out_len), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _take_str(_ret, _out_len.value) - - def close_(self) -> bool: - """Spelled like the release method of several wrappers""" - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _ret: bool = _c_edge_cases_Widget_close(_self_p, ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - def dispose(self, force: bool) -> None: - """Spelled like the release method of the Dart and .NET wrappers""" - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _c_edge_cases_Widget_dispose(_self_p, force, ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - - -_c_edge_cases_Vault_open = _bind("edge_cases_edge_cases_Vault_open", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Vault_peek = _bind("edge_cases_edge_cases_Vault_peek", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Vault_handle = _bind("edge_cases_edge_cases_Vault_handle", ctypes.c_int64, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Vault_probe = _bind("edge_cases_edge_cases_Vault_probe", ctypes.c_bool, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Vault_to_string = _bind("edge_cases_edge_cases_Vault_to_string", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_Vault_clone = _bind("edge_cases_edge_cases_Vault_clone", ctypes.c_void_p, ctypes.c_void_p) -_c_edge_cases_Vault_destroy = _bind("edge_cases_edge_cases_Vault_destroy", None, ctypes.c_void_p) - - -class Vault(_Object): - """Deprecated interface with a throwing constructor - - Deprecated: Use Widget instead - """ - - _destroy = staticmethod(_c_edge_cases_Vault_destroy) - _clone = staticmethod(_c_edge_cases_Vault_clone) - - def __init__(self) -> None: - raise TypeError("Vault cannot be instantiated directly") - - @classmethod - def open(cls, path: str) -> Vault: - """Fails when the vault is busy - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _path_b = path.encode("utf-8") - _err = _ErrorStruct() - _ret = _c_edge_cases_Vault_open(_path_b, len(_path_b), ctypes.byref(_err)) - if _err.code: - raise _edge_errors_error_from(*_read_error(_err)) - return cls._adopt(_required(_ret)) - - def peek(self) -> Optional[Widget]: - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _ret = _c_edge_cases_Vault_peek(_self_p, ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return Widget._adopt(_ret) if _ret else None - - def handle(self) -> int: - """Spelled like the handle accessor of several wrappers""" - _err = _ErrorStruct() - _self_p = self._acquire() - try: - _ret: int = _c_edge_cases_Vault_handle(_self_p, ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - @staticmethod - def probe() -> bool: - """Throwing static - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _err = _ErrorStruct() - _ret: bool = _c_edge_cases_Vault_probe(ctypes.byref(_err)) - if _err.code: - raise _edge_errors_error_from(*_read_error(_err)) - return _ret - - @staticmethod - def to_string() -> str: - """Static spelled like an inherited object member""" - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_edge_cases_Vault_to_string(ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _take_str(_ret, _out_len.value) - - -_c_edge_cases_noop = _bind("edge_cases_edge_cases_noop", None, ctypes.POINTER(_ErrorStruct)) - - -def noop() -> None: - """No parameters and no result""" - _err = _ErrorStruct() - _c_edge_cases_noop(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - - -_c_edge_cases_select = _bind("edge_cases_edge_cases_select", ctypes.c_int32, ctypes.c_int32, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool, ctypes.POINTER(_ErrorStruct)) - - -def select(class_: int, from_: str, func: List[str], range_: Optional[int], type: bool) -> int: - """Function named after a keyword whose parameters are all keywords""" - _from__b = from_.encode("utf-8") - _func_w = _Writer() - _write_list_string(_func_w, func) - _range__w = _Writer() - _write_opt_i32(_range__w, range_) - _err = _ErrorStruct() - _func_b = _func_w.finish() - _range__b = _range__w.finish() - _ret: int = _c_edge_cases_select(class_, _from__b, len(_from__b), _func_b, len(_func_b), _range__b, len(_range__b), type, ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - -_c_edge_cases_nested_optional = _bind("edge_cases_edge_cases_nested_optional", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def nested_optional(items: Optional[List[Optional[Outer]]]) -> Optional[List[Optional[Outer]]]: - """Optional list of optional structs, the deepest legal nesting""" - _items_w = _Writer() - _write_opt_list_opt_Outer(_items_w, items) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _items_b = _items_w.finish() - _ret = _c_edge_cases_nested_optional(_items_b, len(_items_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_opt_list_opt_Outer) - - -_c_edge_cases_optional_params = _bind("edge_cases_edge_cases_optional_params", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def optional_params(text: Optional[str], count: Optional[int], outer: Optional[Outer], tags: Optional[List[str]], blob: Optional[bytes], mode: Optional[Mode]) -> Optional[Mode]: - _text_w = _Writer() - _write_opt_string(_text_w, text) - _count_w = _Writer() - _write_opt_i32(_count_w, count) - _outer_w = _Writer() - _write_opt_Outer(_outer_w, outer) - _tags_w = _Writer() - _write_opt_list_string(_tags_w, tags) - _blob_w = _Writer() - _write_opt_bytes(_blob_w, blob) - _mode_w = _Writer() - _write_opt_Mode(_mode_w, mode) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _text_b = _text_w.finish() - _count_b = _count_w.finish() - _outer_b = _outer_w.finish() - _tags_b = _tags_w.finish() - _blob_b = _blob_w.finish() - _mode_b = _mode_w.finish() - _ret = _c_edge_cases_optional_params(_text_b, len(_text_b), _count_b, len(_count_b), _outer_b, len(_outer_b), _tags_b, len(_tags_b), _blob_b, len(_blob_b), _mode_b, len(_mode_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_opt_Mode) - - -_c_edge_cases_register_batch = _bind("edge_cases_edge_cases_register_batch", None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) - - -def register_batch(listener: BatchListener, keyword: Keyword) -> None: - """Two callback interface parameters on one function""" - _err = _ErrorStruct() - _listener_ctx = _callback_register(listener, BatchListener) - _keyword_ctx = _callback_register(keyword, Keyword) - _c_edge_cases_register_batch(_listener_ctx, _BatchListener_vtable_ptr, _keyword_ctx, _Keyword_vtable_ptr, ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - - -_c_edge_cases_widgets_everywhere = _bind("edge_cases_edge_cases_widgets_everywhere", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def widgets_everywhere(list: List[Widget], by_name: Dict[str, Widget], sparse: List[Optional[Widget]], holder: WidgetHolder) -> WidgetHolder: - """Objects in every buffered position""" - _list_w = _Writer() - _write_list_Widget(_list_w, list) - _by_name_w = _Writer() - _write_map_string_Widget(_by_name_w, by_name) - _sparse_w = _Writer() - _write_list_opt_Widget(_sparse_w, sparse) - _holder_w = _Writer() - _write_WidgetHolder(_holder_w, holder) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _list_b = _list_w.finish() - _by_name_b = _by_name_w.finish() - _sparse_b = _sparse_w.finish() - _holder_b = _holder_w.finish() - _ret = _c_edge_cases_widgets_everywhere(_list_b, len(_list_b), _by_name_b, len(_by_name_b), _sparse_b, len(_sparse_b), _holder_b, len(_holder_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_WidgetHolder) - - -_c_edge_cases_widget_stream = _bind("edge_cases_edge_cases_widget_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_WidgetStreamIterator_next = _bind("edge_cases_edge_cases_WidgetStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_WidgetStreamIterator_destroy = _bind("edge_cases_edge_cases_WidgetStreamIterator_destroy", None, ctypes.c_void_p) - - -class _edge_cases_WidgetStreamIterator(_Iterator): - """The lazy iterator `widget_stream` returns: each step pulls one element - from the producer.""" - - _destroy = staticmethod(_c_edge_cases_WidgetStreamIterator_destroy) - - def __next__(self) -> Widget: - _item = ctypes.c_void_p() - _err = _ErrorStruct() - _p = self._step() - try: - _more = _c_edge_cases_WidgetStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - if not _more: - self.close() - raise StopIteration - return Widget._adopt(_required(_item.value)) - - -def widget_stream() -> Iterator[Widget]: - """Iterator of objects - - Returns a lazy iterator: each step pulls one element from the - producer. Exhaust or close() the iterator to release its native - handle (garbage collection also releases it). - """ - _err = _ErrorStruct() - _ret = _c_edge_cases_widget_stream(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _edge_cases_WidgetStreamIterator._adopt(_ret) - - -_c_edge_cases_async_widget_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p) -_c_edge_cases_async_widget = _bind("edge_cases_edge_cases_async_widget", None, _c_edge_cases_async_widget_callback, ctypes.c_void_p) - - -def _edge_cases_async_widget_complete(context: Optional[int], err: Any, result: Optional[int]) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _edge_errors_error_from), None) - else: - _async_settle(context, None, Widget._adopt(_required(result))) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_widget_completion = _c_edge_cases_async_widget_callback(_edge_cases_async_widget_complete) - - -async def async_widget() -> Widget: - """Async returning an object - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _call, _future = _async_begin() - try: - _c_edge_cases_async_widget(_edge_cases_async_widget_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: Widget = await _future - return _result - - -_c_edge_cases_make_widget = _bind("edge_cases_edge_cases_make_widget", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) - - -def make_widget(label: str) -> Widget: - """Factory for an interface with no constructor""" - _label_b = label.encode("utf-8") - _err = _ErrorStruct() - _ret = _c_edge_cases_make_widget(_label_b, len(_label_b), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return Widget._adopt(_required(_ret)) - - -_c_edge_cases_find_widget = _bind("edge_cases_edge_cases_find_widget", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(_ErrorStruct)) - - -def find_widget(id: int) -> Optional[Widget]: - """Optional interface return - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _err = _ErrorStruct() - _ret = _c_edge_cases_find_widget(id, ctypes.byref(_err)) - if _err.code: - raise _edge_errors_error_from(*_read_error(_err)) - return Widget._adopt(_ret) if _ret else None - - -_c_edge_cases_inspect_widget = _bind("edge_cases_edge_cases_inspect_widget", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def inspect_widget(widget: Widget, fallback: Optional[Widget]) -> str: - """Interface parameter, including an optional one""" - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _widget_p = _lend(widget, Widget) - try: - _fallback_p = _lend_opt(fallback, Widget) - try: - _ret = _c_edge_cases_inspect_widget(_widget_p, _fallback_p, ctypes.byref(_out_len), ctypes.byref(_err)) - finally: - _release_opt(fallback) - finally: - widget._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - return _take_str(_ret, _out_len.value) - - -_c_edge_cases_string_stream = _bind("edge_cases_edge_cases_string_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_StringStreamIterator_next = _bind("edge_cases_edge_cases_StringStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_StringStreamIterator_destroy = _bind("edge_cases_edge_cases_StringStreamIterator_destroy", None, ctypes.c_void_p) - - -class _edge_cases_StringStreamIterator(_Iterator): - """The lazy iterator `string_stream` returns: each step pulls one element - from the producer.""" - - _destroy = staticmethod(_c_edge_cases_StringStreamIterator_destroy) - - def __next__(self) -> str: - _item = ctypes.c_void_p() - _len = ctypes.c_size_t() - _err = _ErrorStruct() - _p = self._step() - try: - _more = _c_edge_cases_StringStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_len), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - if not _more: - self.close() - raise StopIteration - return _take_str(_item.value, _len.value) - - -def string_stream() -> Iterator[str]: - """Iterator of strings - - Returns a lazy iterator: each step pulls one element from the - producer. Exhaust or close() the iterator to release its native - handle (garbage collection also releases it). - """ - _err = _ErrorStruct() - _ret = _c_edge_cases_string_stream(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _edge_cases_StringStreamIterator._adopt(_ret) - - -_c_edge_cases_scalar_stream = _bind("edge_cases_edge_cases_scalar_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_ScalarStreamIterator_next = _bind("edge_cases_edge_cases_ScalarStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_ScalarStreamIterator_destroy = _bind("edge_cases_edge_cases_ScalarStreamIterator_destroy", None, ctypes.c_void_p) - - -class _edge_cases_ScalarStreamIterator(_Iterator): - """The lazy iterator `scalar_stream` returns: each step pulls one element - from the producer.""" - - _destroy = staticmethod(_c_edge_cases_ScalarStreamIterator_destroy) - - def __next__(self) -> int: - _item = ctypes.c_int32() - _err = _ErrorStruct() - _p = self._step() - try: - _more = _c_edge_cases_ScalarStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - if not _more: - self.close() - raise StopIteration - return _item.value - - -def scalar_stream() -> Iterator[int]: - """Iterator of scalars - - Returns a lazy iterator: each step pulls one element from the - producer. Exhaust or close() the iterator to release its native - handle (garbage collection also releases it). - """ - _err = _ErrorStruct() - _ret = _c_edge_cases_scalar_stream(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _edge_cases_ScalarStreamIterator._adopt(_ret) - - -_c_edge_cases_mode_stream = _bind("edge_cases_edge_cases_mode_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_ModeStreamIterator_next = _bind("edge_cases_edge_cases_ModeStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_ModeStreamIterator_destroy = _bind("edge_cases_edge_cases_ModeStreamIterator_destroy", None, ctypes.c_void_p) - - -class _edge_cases_ModeStreamIterator(_Iterator): - """The lazy iterator `mode_stream` returns: each step pulls one element - from the producer.""" - - _destroy = staticmethod(_c_edge_cases_ModeStreamIterator_destroy) - - def __next__(self) -> Mode: - _item = ctypes.c_int32() - _err = _ErrorStruct() - _p = self._step() - try: - _more = _c_edge_cases_ModeStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - if not _more: - self.close() - raise StopIteration - return Mode(_item.value) - - -def mode_stream() -> Iterator[Mode]: - """Iterator of enums - - Returns a lazy iterator: each step pulls one element from the - producer. Exhaust or close() the iterator to release its native - handle (garbage collection also releases it). - """ - _err = _ErrorStruct() - _ret = _c_edge_cases_mode_stream(ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _edge_cases_ModeStreamIterator._adopt(_ret) - - -_c_edge_cases_async_void_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -_c_edge_cases_async_void = _bind("edge_cases_edge_cases_async_void", None, _c_edge_cases_async_void_callback, ctypes.c_void_p) - - -def _edge_cases_async_void_complete(context: Optional[int], err: Any) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _trap_from), None) - else: - _async_settle(context, None, None) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_void_completion = _c_edge_cases_async_void_callback(_edge_cases_async_void_complete) - - -async def async_void() -> None: - """Async with no result""" - _call, _future = _async_begin() - try: - _c_edge_cases_async_void(_edge_cases_async_void_completion, _call) - except BaseException: - _async_abandon(_call) - raise - await _future - - -_c_edge_cases_async_bytes_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) -_c_edge_cases_async_bytes = _bind("edge_cases_edge_cases_async_bytes", None, ctypes.c_uint32, _c_edge_cases_async_bytes_callback, ctypes.c_void_p) - - -def _edge_cases_async_bytes_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _trap_from), None) - else: - _async_settle(context, None, _take_bytes(result_ptr, result_len)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_bytes_completion = _c_edge_cases_async_bytes_callback(_edge_cases_async_bytes_complete) - - -async def async_bytes(size: int) -> bytes: - _call, _future = _async_begin() - try: - _c_edge_cases_async_bytes(size, _edge_cases_async_bytes_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: bytes = await _future - return _result - - -_c_edge_cases_async_struct_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) -_c_edge_cases_async_struct = _bind("edge_cases_edge_cases_async_struct", None, _c_edge_cases_async_struct_callback, ctypes.c_void_p) - - -def _edge_cases_async_struct_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _edge_errors_error_from), None) - else: - _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), _read_Outer)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_struct_completion = _c_edge_cases_async_struct_callback(_edge_cases_async_struct_complete) - - -async def async_struct() -> Outer: - """ - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _call, _future = _async_begin() - try: - _c_edge_cases_async_struct(_edge_cases_async_struct_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: Outer = await _future - return _result - - -_c_edge_cases_async_list_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) -_c_edge_cases_async_list = _bind("edge_cases_edge_cases_async_list", None, ctypes.c_void_p, _c_edge_cases_async_list_callback, ctypes.c_void_p) - - -def _edge_cases_async_list_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _edge_errors_error_from), None) - else: - _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), _read_list_Outer)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_list_completion = _c_edge_cases_async_list_callback(_edge_cases_async_list_complete) - - -async def async_list() -> List[Outer]: - """ - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _call, _future = _async_begin() - _token = _cancel_token_create() - try: - _c_edge_cases_async_list(_token, _edge_cases_async_list_completion, _call) - except BaseException: - _async_abandon(_call) - _cancel_token_destroy(_token) - raise - _result: List[Outer] = await _async_wait_cancellable(_future, _token) - return _result - - -_c_edge_cases_async_optional_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) -_c_edge_cases_async_optional = _bind("edge_cases_edge_cases_async_optional", None, _c_edge_cases_async_optional_callback, ctypes.c_void_p) - - -def _edge_cases_async_optional_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _trap_from), None) - else: - _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), _read_opt_i64)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_optional_completion = _c_edge_cases_async_optional_callback(_edge_cases_async_optional_complete) - - -async def async_optional() -> Optional[int]: - _call, _future = _async_begin() - try: - _c_edge_cases_async_optional(_edge_cases_async_optional_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: Optional[int] = await _future - return _result - - -_c_edge_cases_async_enum_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_int32) -_c_edge_cases_async_enum = _bind("edge_cases_edge_cases_async_enum", None, _c_edge_cases_async_enum_callback, ctypes.c_void_p) - - -def _edge_cases_async_enum_complete(context: Optional[int], err: Any, result: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _trap_from), None) - else: - _async_settle(context, None, Mode(result)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_enum_completion = _c_edge_cases_async_enum_callback(_edge_cases_async_enum_complete) - - -async def async_enum() -> Mode: - _call, _future = _async_begin() - try: - _c_edge_cases_async_enum(_edge_cases_async_enum_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: Mode = await _future - return _result - - -_c_edge_cases_async_keywords_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) -_c_edge_cases_async_keywords = _bind("edge_cases_edge_cases_async_keywords", None, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int32, _c_edge_cases_async_keywords_callback, ctypes.c_void_p) - - -def _edge_cases_async_keywords_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: - # Runs once, on a producer thread: take ownership of the result - # here, then hand it to the awaiting event loop. - try: - if err: - _async_settle(context, _async_error(err, _trap_from), None) - else: - _async_settle(context, None, _take_str(result_ptr, result_len)) - except BaseException as exc: - _async_settle(context, exc, None) - - -_edge_cases_async_keywords_completion = _c_edge_cases_async_keywords_callback(_edge_cases_async_keywords_complete) - - -async def async_keywords(class_: str, import_: int) -> str: - _class__b = class_.encode("utf-8") - _call, _future = _async_begin() - try: - _c_edge_cases_async_keywords(_class__b, len(_class__b), import_, _edge_cases_async_keywords_completion, _call) - except BaseException: - _async_abandon(_call) - raise - _result: str = await _future - return _result - - -_c_edge_cases_keywords_roundtrip = _bind("edge_cases_edge_cases_keywords_roundtrip", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def keywords_roundtrip(value: Keywords) -> Keywords: - _value_w = _Writer() - _write_Keywords(_value_w, value) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _value_b = _value_w.finish() - _ret = _c_edge_cases_keywords_roundtrip(_value_b, len(_value_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Keywords) - - -_c_edge_cases_legacy_value = _bind("edge_cases_edge_cases_legacy_value", ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def legacy_value(value: Legacy) -> Retired: - """Deprecated: Use Mode-based APIs""" - warnings.warn("Use Mode-based APIs", DeprecationWarning, stacklevel=2) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_edge_cases_legacy_value(value, ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Retired) - - -# === Value-buffer codecs of composite types === - - -def _write_list_Leaf(_w: _Writer, value: List[Leaf]) -> None: - _w.write_count(len(value)) - for item in value: - _write_Leaf(_w, item) - - -def _read_list_Leaf(_r: _Reader) -> List[Leaf]: - return [_read_Leaf(_r) for _ in range(_r.read_count())] - - -def _write_opt_Inner(_w: _Writer, value: Optional[Inner]) -> None: - _w.write_flag(value is not None) - if value is not None: - _write_Inner(_w, value) - - -def _read_opt_Inner(_r: _Reader) -> Optional[Inner]: - return _read_Inner(_r) if _r.read_flag() else None - - -def _write_map_string_Inner(_w: _Writer, value: Dict[str, Inner]) -> None: - _w.write_count(len(value)) - for key, item in value.items(): - _w.write_string(key) - _write_Inner(_w, item) - - -def _read_map_string_Inner(_r: _Reader) -> Dict[str, Inner]: - n = _r.read_count() - value = {_r.read_string(): _read_Inner(_r) for _ in range(n)} - if len(value) != n: - raise _malformed("repeated map key") - return value - - -def _write_list_Mode(_w: _Writer, value: List[Mode]) -> None: - _w.write_count(len(value)) - for item in value: - _w.write_i32(item) - - -def _read_list_Mode(_r: _Reader) -> List[Mode]: - return [_r.read_enum(Mode) for _ in range(_r.read_count())] - - -def _write_list_i32(_w: _Writer, value: List[int]) -> None: - _w.write_numbers("i", value) - - -def _read_list_i32(_r: _Reader) -> List[int]: - return _r.read_numbers("i", 4) - - -def _write_list_list_i32(_w: _Writer, value: List[List[int]]) -> None: - _w.write_count(len(value)) - for item in value: - _write_list_i32(_w, item) - - -def _read_list_list_i32(_r: _Reader) -> List[List[int]]: - return [_read_list_i32(_r) for _ in range(_r.read_count())] - - -def _write_opt_i64(_w: _Writer, value: Optional[int]) -> None: - _w.write_flag(value is not None) - if value is not None: - _w.write_i64(value) - - -def _read_opt_i64(_r: _Reader) -> Optional[int]: - return _r.read_i64() if _r.read_flag() else None - - -def _write_list_opt_i64(_w: _Writer, value: List[Optional[int]]) -> None: - _w.write_count(len(value)) - for item in value: - _write_opt_i64(_w, item) - - -def _read_list_opt_i64(_r: _Reader) -> List[Optional[int]]: - return [_read_opt_i64(_r) for _ in range(_r.read_count())] - - -def _write_opt_bytes(_w: _Writer, value: Optional[bytes]) -> None: - _w.write_flag(value is not None) - if value is not None: - _w.write_bytes(value) - - -def _read_opt_bytes(_r: _Reader) -> Optional[bytes]: - return _r.read_bytes() if _r.read_flag() else None - - -def _write_opt_string(_w: _Writer, value: Optional[str]) -> None: - _w.write_flag(value is not None) - if value is not None: - _w.write_string(value) - - -def _read_opt_string(_r: _Reader) -> Optional[str]: - return _r.read_string() if _r.read_flag() else None - - -def _write_map_i64_string(_w: _Writer, value: Dict[int, str]) -> None: - _w.write_count(len(value)) - for key, item in value.items(): - _w.write_i64(key) - _w.write_string(item) - - -def _read_map_i64_string(_r: _Reader) -> Dict[int, str]: - n = _r.read_count() - value = {_r.read_i64(): _r.read_string() for _ in range(n)} - if len(value) != n: - raise _malformed("repeated map key") - return value - - -def _write_opt_Widget(_w: _Writer, value: Optional[Widget]) -> None: - _w.write_flag(value is not None) - if value is not None: - _w.write_object(value, Widget) - - -def _read_opt_Widget(_r: _Reader) -> Optional[Widget]: - return Widget._adopt(_r.read_object()) if _r.read_flag() else None - - -def _write_list_Widget(_w: _Writer, value: List[Widget]) -> None: - _w.write_count(len(value)) - for item in value: - _w.write_object(item, Widget) - - -def _read_list_Widget(_r: _Reader) -> List[Widget]: - return [Widget._adopt(_r.read_object()) for _ in range(_r.read_count())] - - -def _write_map_string_Widget(_w: _Writer, value: Dict[str, Widget]) -> None: - _w.write_count(len(value)) - for key, item in value.items(): - _w.write_string(key) - _w.write_object(item, Widget) - - -def _read_map_string_Widget(_r: _Reader) -> Dict[str, Widget]: - n = _r.read_count() - value = {_r.read_string(): Widget._adopt(_r.read_object()) for _ in range(n)} - if len(value) != n: - raise _malformed("repeated map key") - return value - - -def _write_list_string(_w: _Writer, value: List[str]) -> None: - _w.write_count(len(value)) - for item in value: - _w.write_string(item) - - -def _read_list_string(_r: _Reader) -> List[str]: - return [_r.read_string() for _ in range(_r.read_count())] - - -def _write_opt_i32(_w: _Writer, value: Optional[int]) -> None: - _w.write_flag(value is not None) - if value is not None: - _w.write_i32(value) - - -def _read_opt_i32(_r: _Reader) -> Optional[int]: - return _r.read_i32() if _r.read_flag() else None - - -def _write_opt_Outer(_w: _Writer, value: Optional[Outer]) -> None: - _w.write_flag(value is not None) - if value is not None: - _write_Outer(_w, value) - - -def _read_opt_Outer(_r: _Reader) -> Optional[Outer]: - return _read_Outer(_r) if _r.read_flag() else None - - -def _write_list_opt_Outer(_w: _Writer, value: List[Optional[Outer]]) -> None: - _w.write_count(len(value)) - for item in value: - _write_opt_Outer(_w, item) - - -def _read_list_opt_Outer(_r: _Reader) -> List[Optional[Outer]]: - return [_read_opt_Outer(_r) for _ in range(_r.read_count())] - - -def _write_opt_list_opt_Outer(_w: _Writer, value: Optional[List[Optional[Outer]]]) -> None: - _w.write_flag(value is not None) - if value is not None: - _write_list_opt_Outer(_w, value) - - -def _read_opt_list_opt_Outer(_r: _Reader) -> Optional[List[Optional[Outer]]]: - return _read_list_opt_Outer(_r) if _r.read_flag() else None - - -def _write_opt_list_string(_w: _Writer, value: Optional[List[str]]) -> None: - _w.write_flag(value is not None) - if value is not None: - _write_list_string(_w, value) - - -def _read_opt_list_string(_r: _Reader) -> Optional[List[str]]: - return _read_list_string(_r) if _r.read_flag() else None - - -def _write_opt_Mode(_w: _Writer, value: Optional[Mode]) -> None: - _w.write_flag(value is not None) - if value is not None: - _w.write_i32(value) - - -def _read_opt_Mode(_r: _Reader) -> Optional[Mode]: - return _r.read_enum(Mode) if _r.read_flag() else None - - -def _write_list_opt_Widget(_w: _Writer, value: List[Optional[Widget]]) -> None: - _w.write_count(len(value)) - for item in value: - _write_opt_Widget(_w, item) - - -def _read_list_opt_Widget(_r: _Reader) -> List[Optional[Widget]]: - return [_read_opt_Widget(_r) for _ in range(_r.read_count())] - - -def _write_list_Outer(_w: _Writer, value: List[Outer]) -> None: - _w.write_count(len(value)) - for item in value: - _write_Outer(_w, item) - - -def _read_list_Outer(_r: _Reader) -> List[Outer]: - return [_read_Outer(_r) for _ in range(_r.read_count())] - - -__all__ = [ - "Error", - "InternalError", - "EdgeErrorsError", - "Busy", - "Legacy", - "Mode", - "Leaf", - "Inner", - "Outer", - "Keywords", - "Retired", - "WidgetHolder", - "BatchListener", - "Keyword", - "Widget", - "Vault", - "noop", - "select", - "nested_optional", - "optional_params", - "register_batch", - "widgets_everywhere", - "widget_stream", - "async_widget", - "make_widget", - "find_widget", - "inspect_widget", - "string_stream", - "scalar_stream", - "mode_stream", - "async_void", - "async_bytes", - "async_struct", - "async_list", - "async_optional", - "async_enum", - "async_keywords", - "keywords_roundtrip", - "legacy_value", -] - -# END edge_cases.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__README_md.snap deleted file mode 100644 index eabc5553..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__README_md.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink (Python) - -Python bindings for the `kitchen_sink` native library, built on `ctypes`. - -## Requirements - -- Python 3.9 or later -- The native library (`libkitchen_sink.so`, `libkitchen_sink.dylib`, or `kitchen_sink.dll`) - -## Install - -```bash -pip install . -``` - -## Loading the native library - -The bindings load the library from `KITCHEN_SINK_LIBRARY` when it is set (a full path), -then from inside the installed package, then from the system loader's search -path. - -## Usage - -```python -import kitchen_sink -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink___init___py.snap deleted file mode 100644 index e399eb9d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink___init___py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -from .kitchen_sink import * # noqa: F401,F403 - -# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap index 2c24145b..f4e465d1 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap @@ -10,28 +10,32 @@ from __future__ import annotations import abc import asyncio +import array import builtins import ctypes +import operator import os import platform import struct import threading import warnings +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass from enum import IntEnum -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar +from typing import Any, ClassVar, Generic, TypeVar class Error(Exception): """An error a fallible call reports. `code` is positive for one of the API's declared error codes, raised as - the declaring module's typed subclasses, and negative for a runtime - failure of a call that declares errors: a generic failure (-1), a - producer panic (-2), a marshalling failure (-3), or an exception raised - by a callback-interface implementation (-4). A cancelled async call (-5) - raises `asyncio.CancelledError` instead. A call that declares no errors - never raises this; its failures are bugs and raise `InternalError`. + the domain's typed subclasses, and negative for a runtime failure of a + call that throws: a generic failure (-1, also every failure of a call + declared `throws: any`), a producer panic (-2), a marshalling failure + (-3), or a failed callback-interface implementation (-4). A cancelled + async call (-5) raises `asyncio.CancelledError` instead. A call that + declares no errors never raises this; its failures are bugs and raise + `InternalError`. """ GENERIC_ERROR_CODE = -1 @@ -64,23 +68,41 @@ class InternalError(RuntimeError): self.message = message +class LibraryLoadError(ImportError): + """The native library couldn't be loaded, or it isn't the library these + bindings were generated for (another C ABI revision, or a declaration + that's missing or changed). Raised while the package is imported, so + catch it as `ImportError` around the import; `path` names the library. + """ + + class _ErrorStruct(ctypes.Structure): """The C `kitchen_sink_error` out-parameter every call reports through.""" _fields_ = [ ("code", ctypes.c_int32), - ("message", ctypes.c_char_p), + ("message_ptr", ctypes.c_void_p), + ("message_len", ctypes.c_size_t), ("payload_ptr", ctypes.c_void_p), ("payload_len", ctypes.c_size_t), ] +def _open(path: str, what: str) -> ctypes.CDLL: + try: + return ctypes.CDLL(path) + except OSError as exc: + raise LibraryLoadError( + f"cannot load {what}: {exc}", name=__name__, path=path + ) from exc + + def _load_library() -> ctypes.CDLL: # An explicit path wins; then a library bundled inside this package (a # platform wheel); then the system loader's search path. override = os.environ.get("KITCHEN_SINK_LIBRARY") if override: - return ctypes.CDLL(override) + return _open(override, f"the kitchen_sink native library from KITCHEN_SINK_LIBRARY={override!r}") system = platform.system() if system == "Darwin": name = "libkitchen_sink.dylib" @@ -90,14 +112,12 @@ def _load_library() -> ctypes.CDLL: name = "libkitchen_sink.so" bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) if os.path.exists(bundled): - return ctypes.CDLL(bundled) - try: - return ctypes.CDLL(name) - except OSError as exc: - raise ImportError( - f"cannot load the native library {name!r} ({exc}); " - "set KITCHEN_SINK_LIBRARY to its path" - ) from exc + return _open(bundled, f"the bundled kitchen_sink native library {bundled!r}") + return _open( + name, + f"the kitchen_sink native library {name!r} (set KITCHEN_SINK_LIBRARY to its path, " + "or install a wheel that bundles it)", + ) _lib = _load_library() @@ -120,49 +140,83 @@ class _ContractEntry(ctypes.Structure): # The C ABI revision these bindings were generated against and, per # top-level module's contract symbol, the declarations they rely on: -# (id, hash, dotted path). -_ABI_VERSION = 4 -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { +# (id, hash, dotted path), with each declaration's canonical signature. +_ABI_VERSION = 5 +_CONTRACTS: dict[str, list[tuple[int, int, str]]] = { "kitchen_sink_shared_contract": [ - (0x709e234caa962446, 0x3894f2ca452e9f98, "shared.ping"), - (0xcfb306fa5801ee5b, 0x1cbd9e10983cac2c, "shared.Token"), + (0x709e234caa962446, 0x3894f2ca452e9f98, "shared.ping"), # function ping() -> string + (0xcfb306fa5801ee5b, 0x18e9fc204a13df6f, "shared.Token"), # record Token {i64, string} ], "kitchen_sink_kitchen_contract": [ - (0x01d4f09bbddc986f, 0x29eb88fecc66544d, "kitchen.Gadget.new"), - (0x10472d8fe748c2ee, 0xb265832b6a759fc4, "kitchen.maybe_gadget"), - (0x11368eec07a74078, 0xfa049f78b77e146a, "kitchen.gadgets"), - (0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, "kitchen.stream_gadgets"), - (0x2111b54980826dd7, 0x452783f3e6ca6e1d, "kitchen.do_cancellable"), - (0x29dddbc60bf8d7db, 0x452a3e573d57f6db, "kitchen.Priority"), - (0x2b227003b74a0d16, 0x12332957470e9782, "kitchen.maybe_subscribe"), - (0x2dd66cb0064f0494, 0x60197363fefd92dd, "kitchen.Item"), - (0x33c265bbf079b0e9, 0xc3dcd0927e6c1deb, "kitchen.u32_id"), - (0x33e6381d01660dfe, 0x077c63c31fd7f90a, "kitchen.KitchenErrors"), - (0x3f5fc99d73504b76, 0x2df88d710f1ea10e, "kitchen.Gadget.poke"), - (0x4c5764cf109528d1, 0x2a65529af4338a58, "kitchen.maybe_item"), - (0x4de17dfb93fd494d, 0xba0ad0dd52ed74af, "kitchen.i32_id"), - (0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, "kitchen.stream_items"), - (0x68cdb9a4bd3e3dd5, 0xc678b9164348d497, "kitchen.bool_id"), - (0x69e7cd703d16eb85, 0xf3b28dd22ae9e689, "kitchen.do_async"), - (0x70865079ce41be82, 0xbe3b69464672af46, "kitchen.Gadget.describe"), - (0x71c5a1116cb7c49a, 0xcba1837108a89b84, "kitchen.echo_bytes"), - (0x735ee1e0049ee91a, 0xca74320bc53eddab, "kitchen.echo_string"), - (0x76d7fc58f4e41d84, 0x7373a0e47d30e690, "kitchen.i64_id"), - (0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, "kitchen.nested.hello"), - (0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, "kitchen.new_op"), - (0x89a5e664bc25c67b, 0x5764a95e81ecfd31, "kitchen.f64_id"), - (0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, "kitchen.Gadget.version"), - (0x974b9f3e823c1738, 0xb81b91e975695f27, "kitchen.cross_module_token"), - (0xaca0655fac15d230, 0x11c4d2b28abe07bb, "kitchen.list_items"), - (0xc883b4afae20543e, 0xc40560e5411ff392, "kitchen.legacy_op"), - (0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, "kitchen.get_attrs"), - (0xfae25d3391922ead, 0x1c7f95739f3210b8, "kitchen.subscribe"), - (0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, "kitchen.Gadget"), - (0xfed9c551fb9fd2d6, 0x59aec980ef3f3291, "kitchen.ReadyListener"), + (0x01d4f09bbddc986f, 0xffcf2bd3b04aa002, "kitchen.Gadget.new"), # constructor new(i64) -> Gadget + (0x02bbd5f31c953704, 0x2cfa679ea1373e09, "kitchen.Sampler.samples"), # callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + (0x10472d8fe748c2ee, 0xcc570a2649bbf7dc, "kitchen.maybe_gadget"), # function maybe_gadget(Gadget?) -> Gadget? + (0x11368eec07a74078, 0xfa049f78b77e146a, "kitchen.gadgets"), # function gadgets() -> [Gadget] + (0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, "kitchen.stream_gadgets"), # function stream_gadgets() -> iter + (0x184bfc4ab0e3fe45, 0xb64587477022696b, "kitchen.ids_u64"), # function ids_u64([u64]) -> [u64] + (0x2111b54980826dd7, 0x627bf01ab365b209, "kitchen.do_cancellable"), # function do_cancellable(string) -> string async cancellable + (0x21c70f9371374d74, 0x25d5d88a30fe0a79, "kitchen.ReadyListener.favorite"), # callback_method #4 favorite() -> Gadget + (0x23de5bd597372f46, 0xcf1bc6bb37872798, "kitchen.stream_chunks"), # function stream_chunks() -> iter<[i32]> + (0x249f1ddaec6a6cbe, 0x0710d6c9a4de93da, "kitchen.take_from_pantry"), # function take_from_pantry(string) -> i32 throws PantryError + (0x29dddbc60bf8d7db, 0x452a3e573d57f6db, "kitchen.Priority"), # enum Priority {Low = 0, Normal = 1, High = 2} + (0x2b227003b74a0d16, 0x5dae036b8a6aed30, "kitchen.maybe_subscribe"), # function maybe_subscribe(ReadyListener?) -> bool + (0x2dd66cb0064f0494, 0x74b131724dd0d867, "kitchen.Item"), # record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + (0x33c265bbf079b0e9, 0x97c32b764693f2cb, "kitchen.u32_id"), # function u32_id(u32) -> u32 + (0x33e6381d01660dfe, 0x05e35a563089bf7d, "kitchen.KitchenErrors"), # errors KitchenErrors + (0x3d969ae76db6cb23, 0x65a325cfc772296b, "kitchen.ReadyListener.latest"), # callback_method #3 latest() -> Item? + (0x3f5fc99d73504b76, 0x417e4a2acd3210b3, "kitchen.Gadget.poke"), # method poke(i32) -> i32 throws KitchenErrors + (0x4082f0085db28781, 0x18848e701962912e, "kitchen.Sampler"), # callback Sampler + (0x4340a6b1e7ec8c50, 0xa59d0f32a66dc7a2, "kitchen.Sampler.describe"), # callback_method #2 describe() -> string throws any + (0x4c5764cf109528d1, 0xb374accf14676d90, "kitchen.maybe_item"), # function maybe_item(i64) -> Item? throws KitchenErrors + (0x4de17dfb93fd494d, 0x78be3203bcd10727, "kitchen.i32_id"), # function i32_id(i32) -> i32 + (0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, "kitchen.stream_items"), # function stream_items() -> iter + (0x5dafabb7775fcb2d, 0x6c4b78c7d3f858c1, "kitchen.PantryError.Spoiled"), # code Spoiled = 2 + (0x5e0a8f6be4b00600, 0x18ea1a42036c96a0, "kitchen.Sampler.threshold"), # callback_method #0 threshold(u32?) -> f64? + (0x5ec0af1d4ecc6fed, 0x6540bdb74246420c, "kitchen.async_samples"), # function async_samples(u32) -> [f32] async cancellable + (0x61a358b0571e0a4e, 0x11a24c4b50d11996, "kitchen.async_maybe_count"), # function async_maybe_count() -> i64? async + (0x68cdb9a4bd3e3dd5, 0xcde022fe6bc3b1bf, "kitchen.bool_id"), # function bool_id(bool) -> bool + (0x69e7cd703d16eb85, 0xb13cb8d39e702c79, "kitchen.do_async"), # function do_async(string) -> string async + (0x70865079ce41be82, 0xbe3b69464672af46, "kitchen.Gadget.describe"), # method describe() -> string + (0x71c5a1116cb7c49a, 0x1506cfe6f548fac4, "kitchen.echo_bytes"), # function echo_bytes(bytes) -> bytes + (0x735ee1e0049ee91a, 0xa713dbd1be707752, "kitchen.echo_string"), # function echo_string(string) -> string + (0x76d7fc58f4e41d84, 0x6bdb4ccea2841d5e, "kitchen.i64_id"), # function i64_id(i64) -> i64 + (0x78d91fd617e162b8, 0xd88e2753503b36df, "kitchen.stream_maybe"), # function stream_maybe() -> iter + (0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, "kitchen.nested.hello"), # function hello() -> string + (0x7b3865b536da998c, 0x1c4e79d60a20e74f, "kitchen.mixed_slices"), # function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + (0x7ba266dc26a58989, 0x0f86001cac0c21e8, "kitchen.KitchenErrors.InvalidInput"), # code InvalidInput = 2 + (0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, "kitchen.new_op"), # function new_op() -> i32 + (0x81ded69adee0abf2, 0x8c71141a931906f6, "kitchen.scale_i32"), # function scale_i32([i32], i32) -> [i32] + (0x81fa72769330fed6, 0x89681e305b55a4f4, "kitchen.ReadyListener.label"), # callback_method #2 label() -> string throws KitchenErrors + (0x89a5e664bc25c67b, 0x357fd9b65ad879a9, "kitchen.f64_id"), # function f64_id(f64) -> f64 + (0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, "kitchen.Gadget.version"), # static version() -> string + (0x8dc42397e47614b9, 0xcddf6fede09b2e7f, "kitchen.opt_priority"), # function opt_priority(Priority?) -> Priority? + (0x92f060ef68961fc1, 0x4af6ce5b4f9fa508, "kitchen.ReadyListener.on_item"), # callback_method #1 on_item(Item, Gadget) -> bool + (0x974b9f3e823c1738, 0xb81b91e975695f27, "kitchen.cross_module_token"), # function cross_module_token() -> Token + (0x9f0f001aa85f97d5, 0x55815247f895bd45, "kitchen.sum_f64"), # function sum_f64([f64]) -> f64 + (0xab2a29fd1ef5458b, 0x471a34aa4c09ae2b, "kitchen.KitchenErrors.NotFound"), # code NotFound = 1 + (0xaca0655fac15d230, 0x11c4d2b28abe07bb, "kitchen.list_items"), # function list_items() -> [Item] + (0xb57c8e6ae3fb4383, 0x3f857851ab75366c, "kitchen.PantryError"), # errors PantryError + (0xbdc795b195edd428, 0x36127462f8b0969f, "kitchen.opt_mixed"), # function opt_mixed(f64, f32?, i64?, string?) -> u16? + (0xbe4821ab3c340e71, 0xe1d32e38b766e623, "kitchen.ReadyListener.on_ready"), # callback_method #0 on_ready(i32, string) -> void + (0xc2ad53f2a6a36323, 0x071e29abee823f17, "kitchen.opt_i32_id"), # function opt_i32_id(i32?) -> i32? + (0xc7ab409554fd3d7b, 0xfea12b6bc219fc46, "kitchen.parse_any"), # function parse_any(string) -> i64 throws any + (0xc82a7f2d85f99f4e, 0xa9bcb8bacedb036c, "kitchen.sample_with"), # function sample_with(Sampler, u32?) -> [i16] throws PantryError + (0xc883b4afae20543e, 0xc40560e5411ff392, "kitchen.legacy_op"), # function legacy_op() -> i32 + (0xd180bf646bff59e2, 0xa4515a6d00d7c600, "kitchen.PantryError.OutOfStock"), # code OutOfStock = 1 {string} + (0xe6778df0f5c1b82b, 0x38eaafdb350559e1, "kitchen.opt_bool_id"), # function opt_bool_id(bool?) -> bool? + (0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, "kitchen.get_attrs"), # function get_attrs() -> {string:i32} + (0xfae25d3391922ead, 0xcc034cf506cae182, "kitchen.subscribe"), # function subscribe(ReadyListener) -> i32 + (0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, "kitchen.Gadget"), # interface Gadget + (0xfed9c551fb9fd2d6, 0xe1020573f084dc1d, "kitchen.ReadyListener"), # callback ReadyListener + (0xff3a99b52597dee2, 0x14c4ecac97bdfc9b, "kitchen.Gadget.readings"), # method readings(u32?) -> [f64] throws any ], } +def _refuse(message: str) -> LibraryLoadError: + return LibraryLoadError(f"{message} ({_lib._name})", name=__name__, path=_lib._name) + + def _check_contract() -> None: """Refuse to load a library built for another ABI revision, or one that lacks or changed a declaration these bindings use, before any other @@ -170,15 +224,16 @@ def _check_contract() -> None: try: abi_version = _bind("kitchen_sink_abi_version", ctypes.c_uint32) except AttributeError: - raise ImportError( - f"{_lib._name} does not export kitchen_sink_abi_version; " - "it is not the library these bindings were generated for" + raise _refuse( + "the library doesn't export kitchen_sink_abi_version, so it isn't the " + "kitchen_sink library these bindings were generated for" ) from None found = abi_version() if found != _ABI_VERSION: - raise ImportError( - f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " - f"but {_lib._name} reports revision {found}" + raise _refuse( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, but the " + f"library reports revision {found}; regenerate the bindings or rebuild " + "the library" ) for symbol, expected in _CONTRACTS.items(): try: @@ -186,18 +241,16 @@ def _check_contract() -> None: symbol, ctypes.POINTER(_ContractEntry), ctypes.POINTER(ctypes.c_size_t) ) except AttributeError: - raise ImportError(f"{_lib._name} does not export {symbol}") from None + raise _refuse(f"the library doesn't export {symbol}") from None length = ctypes.c_size_t() table = contract(ctypes.byref(length)) hashes = {table[i].id: table[i].hash for i in range(length.value)} if table else {} for entry_id, entry_hash, path in expected: actual = hashes.get(entry_id) if actual is None: - raise ImportError(f"{path} is missing from the library ({_lib._name})") + raise _refuse(f"{path} is missing from the library") if actual != entry_hash: - raise ImportError( - f"{path} changed since these bindings were generated ({_lib._name})" - ) + raise _refuse(f"{path} changed since these bindings were generated") _check_contract() @@ -216,12 +269,18 @@ def _debug_live(kind: int) -> int: return live -def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: +def _peek_bytes(ptr: int | None, length: int) -> bytes: + """Copy a byte run the producer only lends (a zero-length run's pointer + is never read).""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +def _read_error(err: _ErrorStruct) -> tuple[int, str, bytes]: """Copy a filled out-err slot's code, message, and payload, then release them.""" code = err.code - message = err.message.decode("utf-8", "replace") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + message = _peek_bytes(err.message_ptr, err.message_len).decode("utf-8", "replace") + payload = _peek_bytes(err.payload_ptr, err.payload_len) _error_clear(ctypes.byref(err)) return code, message, payload @@ -231,7 +290,19 @@ def _trap_from(code: int, message: str, payload: bytes = b"") -> InternalError: return InternalError(code, message) -def _take_bytes(ptr: Optional[int], length: int) -> bytes: +def _error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a failed call declared `throws: any`.""" + return Error(code, message) + + +def _unknown_code(domain: type[Error], code: int, message: str) -> Error: + """The exception for a code a domain's factory doesn't know: a positive + code (from a library newer than these bindings) raises the domain's base + class with the code and message; a runtime code raises the root error.""" + return domain(code, message) if code > 0 else Error(code, message) + + +def _take_bytes(ptr: int | None, length: int) -> bytes: """Copy a producer-owned byte run (a string, bytes, or value buffer) and release it with kitchen_sink_free_bytes.""" if not ptr: @@ -242,17 +313,100 @@ def _take_bytes(ptr: Optional[int], length: int) -> bytes: _free_bytes(ptr, length) -def _take_str(ptr: Optional[int], length: int) -> str: +def _take_str(ptr: int | None, length: int) -> str: """Copy and release a producer-owned UTF-8 string.""" return _take_bytes(ptr, length).decode("utf-8") -def _peek_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a byte run the producer only lends for the current call.""" - return ctypes.string_at(ptr, length) if ptr and length else b"" +# Range checks. ctypes truncates an out-of-range integer argument to the C +# width silently, so every integer crossing directly is checked first. + + +def _int_check(kind: str, lo: int, hi: int) -> Callable[[int, str], int]: + def check(value: int, what: str) -> int: + try: + v = operator.index(value) + except TypeError: + raise TypeError(f"{what}: expected an integer, got {type(value).__name__}") from None + if lo <= v <= hi: + return v + raise OverflowError(f"{what}: {v} is out of range for {kind}") + + return check + + +_i8 = _int_check("i8", -(1 << 7), (1 << 7) - 1) +_i16 = _int_check("i16", -(1 << 15), (1 << 15) - 1) +_i32 = _int_check("i32", -(1 << 31), (1 << 31) - 1) +_i64 = _int_check("i64", -(1 << 63), (1 << 63) - 1) +_u8 = _int_check("u8", 0, (1 << 8) - 1) +_u16 = _int_check("u16", 0, (1 << 16) - 1) +_u32 = _int_check("u32", 0, (1 << 32) - 1) +_u64 = _int_check("u64", 0, (1 << 64) - 1) + + +def _float(value: float, what: str) -> float: + """A number as a float, rejecting anything else.""" + if isinstance(value, (int, float)): + return float(value) + raise TypeError(f"{what}: expected a number, got {type(value).__name__}") + + +# Typed arrays: numeric lists crossing directly. Each element kind's +# `array` typecode and ctypes element type. +_ARRAY_TYPES: dict[str, tuple[str, Any]] = { + "i8": ("b", ctypes.c_int8), + "i16": ("h", ctypes.c_int16), + "i32": ("i", ctypes.c_int32), + "i64": ("q", ctypes.c_int64), + "u16": ("H", ctypes.c_uint16), + "u32": ("I", ctypes.c_uint32), + "u64": ("Q", ctypes.c_uint64), + "f32": ("f", ctypes.c_float), + "f64": ("d", ctypes.c_double), +} + + +def _array(values: Iterable[Any], kind: str, what: str) -> array.array[Any]: + """Pack a sequence of numbers into a range-checked typed array whose + buffer is lent to one call. An `array.array` of the matching typecode + is lent as is, without a copy.""" + code = _ARRAY_TYPES[kind][0] + if isinstance(values, array.array) and values.typecode == code: + return values + if isinstance(values, (str, bytes, bytearray, memoryview)): + raise TypeError(f"{what}: expected a sequence of numbers, got {type(values).__name__}") + try: + return array.array(code, values) + except OverflowError: + raise OverflowError(f"{what}: an element is out of range for {kind}") from None + except TypeError as exc: + raise TypeError(f"{what}: {exc}") from None + + +def _peek_array(ptr: int | None, count: int, kind: str) -> list[Any]: + """Copy a typed array the producer only lends.""" + if not ptr or not count: + return [] + values: list[Any] = (_ARRAY_TYPES[kind][1] * count).from_address(ptr)[:] + return values + + +def _take_array(ptr: int | None, count: int, kind: str) -> list[Any]: + """Copy a producer-owned typed array and release it with + kitchen_sink_free_bytes.""" + if not ptr: + return [] + ctype = _ARRAY_TYPES[kind][1] + try: + values: list[Any] = (ctype * count).from_address(ptr)[:] + return values + finally: + _free_bytes(ptr, count * ctypes.sizeof(ctype)) _H = TypeVar("_H", bound="_Handle") +_T = TypeVar("_T") class _Handle: @@ -263,22 +417,15 @@ class _Handle: last such call returns instead of freeing the object mid-call. """ - # The native destroy (or iterator release) function, set per subclass. - _destroy: Callable[[int], None] - def _init_handle(self, ptr: int) -> None: - self._ptr: Optional[int] = ptr + self._ptr: int | None = ptr self._key = ptr self._calls = 0 self._closing = False self._lock = threading.Lock() - @classmethod - def _adopt(cls: Type[_H], ptr: int) -> _H: - """Wrap one strong reference the producer handed over.""" - obj = cls.__new__(cls) - obj._init_handle(ptr) - return obj + def _free(self, ptr: int) -> None: + """Release the native reference (each subclass releases its kind).""" def _acquire(self) -> int: with self._lock: @@ -295,10 +442,10 @@ class _Handle: return ptr, self._ptr = self._ptr, None if ptr is not None: - type(self)._destroy(ptr) + self._free(ptr) def close(self) -> None: - """Release the native reference. Idempotent; the wrapper is unusable + """Release the native reference. Idempotent; the object is unusable afterwards.""" lock = getattr(self, "_lock", None) if lock is None: @@ -311,7 +458,7 @@ class _Handle: return ptr, self._ptr = self._ptr, None if ptr is not None: - type(self)._destroy(ptr) + self._free(ptr) def __enter__(self: _H) -> _H: return self @@ -320,19 +467,30 @@ class _Handle: self.close() def __del__(self) -> None: - # An unreachable wrapper has no call in flight: release directly. + # An unreachable handle has no call in flight: release directly. ptr = getattr(self, "_ptr", None) if ptr is not None: self._ptr = None - type(self)._destroy(ptr) + self._free(ptr) class _Object(_Handle): """Base of every interface wrapper: a reference-counted native object. Two wrappers are equal when they hold the same native object.""" - # The native clone function, set per subclass. - _clone: Callable[[int], int] + # The native destroy and clone functions, set per subclass. + _destroy: ClassVar[Callable[[int], None]] + _clone: ClassVar[Callable[[int], int]] + + @classmethod + def _adopt(cls: type[_H], ptr: int) -> _H: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _free(self, ptr: int) -> None: + type(self)._destroy(ptr) def _clone_ref(self) -> int: """A new strong reference to the same object, for a receiver that @@ -352,59 +510,93 @@ class _Object(_Handle): return hash(self._key) -class _Iterator(_Handle): - """Base of every lazy iterator over a producer stream: one native `next` - call per step; the handle is released on exhaustion, `close()`, or - garbage collection.""" +class NativeIterator(Iterator[_T], _Handle, Generic[_T]): + """A lazy iterator over a sequence the native library produces. - def __iter__(self: _H) -> _H: - return self + Each step pulls one element from the producer. The native handle is + released when the iterator is exhausted, on `close()`, at the end of a + `with` block, or by garbage collection; a closed iterator is exhausted. + Iterator-returning calls create these; there's no public constructor. + """ + + _pull: Callable[[int], _T] + _destroy_fn: Callable[[int], None] + + def __init__(self) -> None: + raise TypeError("NativeIterator objects come from the API's iterator-returning calls") + + def _free(self, ptr: int) -> None: + self._destroy_fn(ptr) - def _step(self) -> int: + def __next__(self) -> _T: with self._lock: ptr = self._ptr if ptr is None or self._closing: raise StopIteration self._calls += 1 - return ptr + try: + item = self._pull(ptr) + except StopIteration: + self._release() + self.close() + raise + except BaseException: + self._release() + raise + self._release() + return item + + def __repr__(self) -> str: + state = "closed" if self._ptr is None or self._closing else "open" + return f"" -def _required(ptr: Optional[int]) -> int: +def _iterate( + ptr: int, pull: Callable[[int], _T], destroy: Callable[[int], None] +) -> NativeIterator[_T]: + """Wrap an iterator handle the producer returned.""" + it: NativeIterator[_T] = NativeIterator.__new__(NativeIterator) + it._init_handle(ptr) + it._pull = pull + it._destroy_fn = destroy + return it + + +def _required(ptr: int | None) -> int: """A non-null object pointer the producer returned.""" if not ptr: raise InternalError(-3, "the producer returned a null object") return ptr -def _lend(obj: Any, cls: Type[_Object]) -> int: +def _lend(obj: Any, cls: type[_Object]) -> int: """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" if not isinstance(obj, cls): raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") return obj._acquire() -def _lend_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: +def _lend_opt(obj: Any, cls: type[_Object]) -> int | None: """`_lend` for an optional object: None lends a null pointer.""" return None if obj is None else _lend(obj, cls) -def _release_opt(obj: Optional[_Object]) -> None: +def _release_opt(obj: _Object | None) -> None: if obj is not None: obj._release() -_I8 = struct.Struct(" InternalError: return InternalError(-3, f"malformed value buffer: {what}") +def _bad_value(kind: str, value: object) -> Exception: + """The exception for a value `struct` can't pack as `kind`: out of range + for a number, else the wrong type.""" + if isinstance(value, (int, float)): + return OverflowError(f"{value} is out of range for {kind}") + return TypeError(f"expected a number for {kind}, got {type(value).__name__}") + + class _Writer: """Encodes values in the value-buffer wire format: little-endian, packed, no alignment.""" @@ -420,17 +620,17 @@ class _Writer: def __init__(self) -> None: self._buf = bytearray() - self._objects: List[Tuple[int, _Object]] = [] + self._objects: list[tuple[int, _Object]] = [] def finish(self) -> bytes: # Object tokens are minted last, so an encoding that fails part-way # never leaks a strong reference. - minted: List[Tuple[_Object, int]] = [] + minted: list[tuple[_Object, int]] = [] try: for pos, obj in self._objects: ptr = obj._clone_ref() minted.append((obj, ptr)) - _U64.pack_into(self._buf, pos, ptr) + _S_U64.pack_into(self._buf, pos, ptr) except BaseException: for obj, ptr in minted: type(obj)._destroy(ptr) @@ -438,61 +638,77 @@ class _Writer: self._objects = [] return bytes(self._buf) + def _put(self, fmt: struct.Struct, kind: str, v: Any) -> None: + try: + self._buf += fmt.pack(v) + except struct.error: + raise _bad_value(kind, v) from None + def write_bool(self, v: bool) -> None: self._buf.append(1 if v else 0) def write_i8(self, v: int) -> None: - self._buf += _I8.pack(v) + self._put(_S_I8, "i8", v) def write_u8(self, v: int) -> None: - self._buf += _U8.pack(v) + self._put(_S_U8, "u8", v) def write_i16(self, v: int) -> None: - self._buf += _I16.pack(v) + self._put(_S_I16, "i16", v) def write_u16(self, v: int) -> None: - self._buf += _U16.pack(v) + self._put(_S_U16, "u16", v) def write_i32(self, v: int) -> None: - self._buf += _I32.pack(v) + self._put(_S_I32, "i32", v) def write_u32(self, v: int) -> None: - self._buf += _U32.pack(v) + self._put(_S_U32, "u32", v) def write_i64(self, v: int) -> None: - self._buf += _I64.pack(v) + self._put(_S_I64, "i64", v) def write_u64(self, v: int) -> None: - self._buf += _U64.pack(v) + self._put(_S_U64, "u64", v) def write_f32(self, v: float) -> None: - self._buf += _F32.pack(v) + self._put(_S_F32, "f32", v) def write_f64(self, v: float) -> None: - self._buf += _F64.pack(v) + self._put(_S_F64, "f64", v) def write_count(self, n: int) -> None: - self._buf += _U32.pack(n) + self._buf += _S_U32.pack(n) def write_flag(self, present: bool) -> None: self._buf.append(1 if present else 0) def write_string(self, v: str) -> None: data = v.encode("utf-8") - self._buf += _U32.pack(len(data)) + self._buf += _S_U32.pack(len(data)) self._buf += data def write_bytes(self, v: bytes) -> None: - self._buf += _U32.pack(len(v)) + self._buf += _S_U32.pack(len(v)) self._buf += v - def write_numbers(self, code: str, values: List[Any]) -> None: + def write_numbers(self, code: str, kind: str, values: Sequence[Any]) -> None: """A list of one fixed-width numeric primitive (`code` is its struct format character), packed in one call.""" - self._buf += _U32.pack(len(values)) - self._buf += struct.pack(f"<{len(values)}{code}", *values) + try: + data = struct.pack(f"<{len(values)}{code}", *values) + except struct.error: + one = struct.Struct(f"<{code}") + for v in values: + try: + one.pack(v) + except struct.error: + raise _bad_value(kind, v) from None + raise + self._buf += _S_U32.pack(len(values)) + self._buf += data - def write_object(self, obj: Any, cls: Type[_Object]) -> None: + def write_object(self, obj: Any, cls: type[_Object]) -> None: """Reserve an object token: one strong reference to `obj` the reader adopts, cloned in `finish()`. A closed wrapper is rejected now.""" if not isinstance(obj, cls): @@ -528,39 +744,39 @@ class _Reader: return b == 1 def read_i8(self) -> int: - return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + return int(_S_I8.unpack_from(self._data, self._take(1, "i8"))[0]) def read_u8(self) -> int: return self._data[self._take(1, "u8")] def read_i16(self) -> int: - return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + return int(_S_I16.unpack_from(self._data, self._take(2, "i16"))[0]) def read_u16(self) -> int: - return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + return int(_S_U16.unpack_from(self._data, self._take(2, "u16"))[0]) def read_i32(self) -> int: - return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + return int(_S_I32.unpack_from(self._data, self._take(4, "i32"))[0]) def read_u32(self) -> int: - return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + return int(_S_U32.unpack_from(self._data, self._take(4, "u32"))[0]) def read_i64(self) -> int: - return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + return int(_S_I64.unpack_from(self._data, self._take(8, "i64"))[0]) def read_u64(self) -> int: - return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + return int(_S_U64.unpack_from(self._data, self._take(8, "u64"))[0]) def read_f32(self) -> float: - return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + return float(_S_F32.unpack_from(self._data, self._take(4, "f32"))[0]) def read_f64(self) -> float: - return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + return float(_S_F64.unpack_from(self._data, self._take(8, "f64"))[0]) def read_count(self) -> int: # An element count, not a byte length: elements can encode to zero - # bytes, so it is not bounded by the remaining data. - return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + # bytes, so it isn't bounded by the remaining data. + return int(_S_U32.unpack_from(self._data, self._take(4, "count"))[0]) def read_flag(self) -> bool: b = self._data[self._take(1, "option flag")] @@ -569,7 +785,7 @@ class _Reader: return b == 1 def read_bytes(self) -> bytes: - n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + n = int(_S_U32.unpack_from(self._data, self._take(4, "length"))[0]) pos = self._take(n, "bytes") return self._data[pos:pos + n] @@ -579,14 +795,14 @@ class _Reader: except UnicodeDecodeError: raise _malformed("string is not valid UTF-8") from None - def read_enum(self, cls: Type[_E]) -> _E: + def read_enum(self, cls: type[_E]) -> _E: value = self.read_i32() try: return cls(value) except ValueError: raise _malformed(f"undeclared {cls.__name__} value {value}") from None - def read_numbers(self, code: str, size: int) -> List[Any]: + def read_numbers(self, code: str, size: int) -> list[Any]: """A list of one fixed-width numeric primitive, unpacked in one call.""" n = self.read_count() pos = self._take(n * size, "list") @@ -594,7 +810,7 @@ class _Reader: def read_object(self) -> int: """An object token: one strong reference the caller adopts.""" - ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + ptr = int(_S_U64.unpack_from(self._data, self._take(8, "object token"))[0]) if ptr == 0: raise _malformed("null object token") return ptr @@ -618,14 +834,14 @@ def _decode(data: bytes, read: Callable[[_Reader], _T]) -> _T: # The trampolines live at module scope, so nothing a pending call needs can # be garbage collected before the producer fires the completion, even when # the awaiting coroutine is gone. -_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, asyncio.Future[Any]]] = {} +_pending: dict[int, tuple[asyncio.AbstractEventLoop, asyncio.Future[Any]]] = {} _pending_lock = threading.Lock() _pending_next = 0 _error_free = _bind("kitchen_sink_error_free", None, ctypes.POINTER(_ErrorStruct)) -def _async_begin() -> Tuple[int, asyncio.Future[Any]]: +def _async_begin() -> tuple[int, asyncio.Future[Any]]: """Register a new pending call on the running event loop.""" global _pending_next loop = asyncio.get_running_loop() @@ -648,15 +864,15 @@ def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) exception it reports. The cancelled code maps to CancelledError.""" e = err.contents code = e.code - message = e.message.decode("utf-8", "replace") if e.message else "" - payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" + message = _peek_bytes(e.message_ptr, e.message_len).decode("utf-8", "replace") + payload = _peek_bytes(e.payload_ptr, e.payload_len) _error_free(err) if code == Error.CANCELLED_ERROR_CODE: return asyncio.CancelledError(message) return factory(code, message, payload) -def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: +def _async_settle(key: int | None, exc: BaseException | None, value: Any) -> None: """Hand a completion back to the event loop that awaits it.""" with _pending_lock: entry = _pending.pop(key or 0, None) @@ -670,7 +886,7 @@ def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) pass -def _async_resolve(future: asyncio.Future[Any], exc: Optional[BaseException], value: Any) -> None: +def _async_resolve(future: asyncio.Future[Any], exc: BaseException | None, value: Any) -> None: if future.done(): return if isinstance(exc, asyncio.CancelledError): @@ -714,13 +930,18 @@ async def _async_wait_cancellable(future: asyncio.Future[Any], token: int) -> An # producer never holds a raw reference to a Python object. ctypes acquires # the GIL on entry to a trampoline, so the producer may call (and free) from # any thread. -_callbacks: Dict[int, Any] = {} +_callbacks: dict[int, Any] = {} _callbacks_lock = threading.Lock() _callbacks_next = 0 _alloc = _bind("kitchen_sink_alloc", ctypes.c_void_p, ctypes.c_size_t) _error_set = _bind( - "kitchen_sink_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p + "kitchen_sink_error_set", + None, + ctypes.POINTER(_ErrorStruct), + ctypes.c_int32, + ctypes.c_char_p, + ctypes.c_size_t, ) _error_set_payload = _bind( "kitchen_sink_error_set_payload", @@ -743,16 +964,16 @@ def _callback_register(impl: Any, cls: type) -> int: return ctx -def _callback_register_opt(impl: Any, cls: type) -> Optional[int]: +def _callback_register_opt(impl: Any, cls: type) -> int | None: """`_callback_register` for an optional callback: None passes none.""" return None if impl is None else _callback_register(impl, cls) -def _callback_get(ctx: Optional[int]) -> Any: +def _callback_get(ctx: int | None) -> Any: return _callbacks[ctx or 0] -def _callback_free(ctx: Optional[int]) -> None: +def _callback_free(ctx: int | None) -> None: # The producer's last reference is gone; it never passes `ctx` again. with _callbacks_lock: _callbacks.pop(ctx or 0, None) @@ -776,7 +997,24 @@ def _callback_return_bytes(out_ptr: Any, out_len: Any, data: bytes) -> None: out_len[0] = n -def _callback_return_object(obj: Any, cls: Type[_Object]) -> int: +def _callback_return_array( + out_ptr: Any, out_len: Any, values: Iterable[Any], kind: str, what: str +) -> None: + """Hand a typed-array return to the producer: a range-checked run + allocated with kitchen_sink_alloc, which the producer adopts and frees. + `out_len` is the element count.""" + data = _array(values, kind, what) + size = len(data) * data.itemsize + ptr = _alloc(size) if size else None + if size: + if not ptr: + raise MemoryError(f"kitchen_sink_alloc({size}) failed") + ctypes.memmove(ptr, data.buffer_info()[0], size) + out_ptr[0] = ptr + out_len[0] = len(data) + + +def _callback_return_object(obj: Any, cls: type[_Object]) -> int: """Hand an object return to the producer: one fresh strong reference it adopts.""" if not isinstance(obj, cls): @@ -784,34 +1022,46 @@ def _callback_return_object(obj: Any, cls: Type[_Object]) -> int: return obj._clone_ref() -def _callback_return_object_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: +def _callback_return_object_opt(obj: Any, cls: type[_Object]) -> int | None: """`_callback_return_object` for an optional object: None returns null.""" return None if obj is None else _callback_return_object(obj, cls) -def _callback_fail(out_err: Any, exc: BaseException, domain: Optional[Type[Error]] = None) -> None: +# The codes a failed callback method reports when it raised anything but a +# code of its domain: -1 for a method that throws (a domain or `any`), -4 +# for one that declares no errors. +_GENERIC = Error.GENERIC_ERROR_CODE +_FOREIGN = Error.FOREIGN_ERROR_CODE + + +def _callback_fail( + out_err: Any, exc: BaseException, code: int, domain: type[Error] | None = None +) -> None: """Report an implementation's exception to the producer. A method that - declares errors passes its module's error domain, and an exception of - that domain carrying a declared code travels as that code with its - fields; every other exception reaches the producer as the foreign error - code (-4). Nothing unwinds through C.""" + throws an error domain passes it, and an exception of that domain + carrying a positive code travels as that code with its fields as the + payload; every other exception travels as `code` with its message. + Nothing unwinds through C.""" if domain is not None and isinstance(exc, domain) and exc.code > 0: - _error_set(out_err, exc.code, exc.message.encode("utf-8", "replace")) + code = exc.code payload = exc._payload() - if payload: - _error_set_payload(out_err, payload, len(payload)) - return - if isinstance(exc, (Error, InternalError)): message = exc.message else: - message = str(exc) or type(exc).__name__ - _error_set(out_err, Error.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) + payload = b"" + if isinstance(exc, (Error, InternalError)): + message = exc.message + else: + message = str(exc) or type(exc).__name__ + data = message.encode("utf-8", "replace") + _error_set(out_err, code, data, len(data)) + if payload: + _error_set_payload(out_err, payload, len(payload)) # === Module: shared === -@dataclass +@dataclass(frozen=True, slots=True) class Token: """A reusable token referenced across modules""" @@ -836,8 +1086,8 @@ _c_shared_ping = _bind("kitchen_sink_shared_ping", ctypes.c_void_p, ctypes.POINT def ping() -> str: """Trivial cross-module helper""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_shared_ping(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -847,14 +1097,19 @@ def ping() -> str: # === Module: kitchen === -class KitchenErrorsError(Error): - """Base exception for the `kitchen` module's error domain.""" +class KitchenError(Error): + """Base exception of the `KitchenErrors` error domain (module `kitchen`). - NotFound: Type[NotFound] - InvalidInput: Type[InvalidInput] + Each declared code raises its own subclass. A positive code these + bindings don't know (from a newer library) raises this class itself, + with `code` and `message` preserved. + """ + + NotFound: ClassVar[type[NotFoundError]] + InvalidInput: ClassVar[type[InvalidInputError]] -class NotFound(KitchenErrorsError): +class NotFoundError(KitchenError): """Item not found""" CODE = 1 @@ -863,7 +1118,7 @@ class NotFound(KitchenErrorsError): super().__init__(1, message) -class InvalidInput(KitchenErrorsError): +class InvalidInputError(KitchenError): """Invalid input""" CODE = 2 @@ -872,19 +1127,71 @@ class InvalidInput(KitchenErrorsError): super().__init__(2, message) -KitchenErrorsError.NotFound = NotFound -KitchenErrorsError.InvalidInput = InvalidInput +KitchenError.NotFound = NotFoundError +KitchenError.InvalidInput = InvalidInputError + + +def _kitchen_error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a failure of a call that throws KitchenError.""" + if code == 1: + return NotFoundError(message) + if code == 2: + return InvalidInputError(message) + return _unknown_code(KitchenError, code, message) -def _kitchen_errors_error_from(code: int, message: str, payload: bytes = b"") -> Error: - """The KitchenErrorsError subclass for `code` with its payload fields, or the - plain Error for a runtime code (a panic, a marshalling failure). +class PantryError(Error): + """Base exception of the `PantryError` error domain (module `kitchen`). + + Each declared code raises its own subclass. A positive code these + bindings don't know (from a newer library) raises this class itself, + with `code` and `message` preserved. """ + + OutOfStock: ClassVar[type[OutOfStockError]] + Spoiled: ClassVar[type[SpoiledError]] + + +class OutOfStockError(PantryError): + """Out of stock""" + + CODE = 1 + + item: str + + def __init__(self, item: str, message: str = "Out of stock") -> None: + super().__init__(1, message) + self.item = item + + def _payload(self) -> bytes: + _w = _Writer() + _w.write_string(self.item) + return _w.finish() + + +class SpoiledError(PantryError): + """Spoiled""" + + CODE = 2 + + def __init__(self, message: str = "Spoiled") -> None: + super().__init__(2, message) + + +PantryError.OutOfStock = OutOfStockError +PantryError.Spoiled = SpoiledError + + +def _pantry_error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a failure of a call that throws PantryError.""" if code == 1: - return NotFound(message) + return _decode(payload, lambda _r: OutOfStockError( + item=_r.read_string(), + message=message, + )) if code == 2: - return InvalidInput(message) - return Error(code, message) + return SpoiledError(message) + return _unknown_code(PantryError, code, message) class Priority(IntEnum): @@ -898,7 +1205,7 @@ class Priority(IntEnum): High = 2 -@dataclass +@dataclass(frozen=True, slots=True) class Item: """Kitchen sink struct exercising every field feature""" @@ -914,14 +1221,14 @@ class Item: ratio: float nick: int payload: bytes - tags: List[str] - attrs: Dict[str, str] - parent: Optional[int] + tags: list[str] + attrs: dict[str, str] + parent: int | None # Cross-module struct reference token: Token priority: Priority # An optional object inside a record - gadget: Optional[Gadget] + gadget: Gadget | None def _write_Item(_w: _Writer, value: Item) -> None: @@ -963,9 +1270,9 @@ class ReadyListener(abc.ABC): Subclass it and implement every method to pass an instance where the API takes a `ReadyListener`. The producer may call the methods from any - thread until it releases the instance. A method that declares errors may - raise a `KitchenErrorsError` code, which reaches the producer with its fields; - any other exception reaches it as Error.FOREIGN_ERROR_CODE (-4). + thread until it releases the instance. An exception a method raises + reaches the producer as a failure of that call (a method that throws + an error domain reports that domain's codes with their fields). """ @abc.abstractmethod @@ -982,12 +1289,12 @@ class ReadyListener(abc.ABC): Raises ------ - KitchenErrorsError - To report one of the domain's error codes to the producer. + KitchenError + To report one of the domain's codes, with its fields, to the producer. """ @abc.abstractmethod - def latest(self) -> Optional[Item]: + def latest(self) -> Item | None: """The most recent item the listener kept, if any""" @abc.abstractmethod @@ -1017,52 +1324,50 @@ class _ReadyListenerVtable(ctypes.Structure): ] -def _ReadyListener_on_ready(ctx: Optional[int], code: int, msg_ptr: Optional[int], msg_len: int, out_err: Any) -> None: +def _ReadyListener_on_ready(ctx: int | None, code: int, msg_ptr: int | None, msg_len: int, out_err: Any) -> None: try: _callback_get(ctx).on_ready(code, _peek_bytes(msg_ptr, msg_len).decode("utf-8")) except BaseException as exc: - _callback_fail(out_err, exc) + _callback_fail(out_err, exc, _FOREIGN) -def _ReadyListener_on_item(ctx: Optional[int], item_ptr: Optional[int], item_len: int, gadget: Optional[int], out_err: Any) -> bool: +def _ReadyListener_on_item(ctx: int | None, item_ptr: int | None, item_len: int, gadget: int | None, out_err: Any) -> bool: try: _gadget = Gadget._adopt(_required(gadget)) return bool(_callback_get(ctx).on_item(_decode(_peek_bytes(item_ptr, item_len), _read_Item), _gadget)) except BaseException as exc: - _callback_fail(out_err, exc) + _callback_fail(out_err, exc, _FOREIGN) return False -def _ReadyListener_label(ctx: Optional[int], out_ptr: Any, out_len: Any, out_err: Any) -> None: +def _ReadyListener_label(ctx: int | None, out_ptr: Any, out_len: Any, out_err: Any) -> None: try: - _ret = _callback_get(ctx).label() - _callback_return_bytes(out_ptr, out_len, _ret.encode("utf-8")) + _callback_return_bytes(out_ptr, out_len, _callback_get(ctx).label().encode("utf-8")) except BaseException as exc: - _callback_fail(out_err, exc, KitchenErrorsError) + _callback_fail(out_err, exc, _GENERIC, KitchenError) -def _ReadyListener_latest(ctx: Optional[int], out_ptr: Any, out_len: Any, out_err: Any) -> None: +def _ReadyListener_latest(ctx: int | None, out_ptr: Any, out_len: Any, out_err: Any) -> None: try: _ret = _callback_get(ctx).latest() _w = _Writer() _write_opt_Item(_w, _ret) _callback_return_bytes(out_ptr, out_len, _w.finish()) except BaseException as exc: - _callback_fail(out_err, exc) + _callback_fail(out_err, exc, _FOREIGN) -def _ReadyListener_favorite(ctx: Optional[int], out_err: Any) -> Optional[int]: +def _ReadyListener_favorite(ctx: int | None, out_err: Any) -> int | None: try: - _ret = _callback_get(ctx).favorite() - return _callback_return_object(_ret, Gadget) + return _callback_return_object(_callback_get(ctx).favorite(), Gadget) except BaseException as exc: - _callback_fail(out_err, exc) + _callback_fail(out_err, exc, _FOREIGN) return None _ReadyListener_vtable = _ReadyListenerVtable( ctypes.sizeof(_ReadyListenerVtable), - 0, + 0, # flags: callable from any thread _callback_free_fn, _ReadyListener_on_ready_t(_ReadyListener_on_ready), _ReadyListener_on_item_t(_ReadyListener_on_item), @@ -1073,9 +1378,94 @@ _ReadyListener_vtable = _ReadyListenerVtable( _ReadyListener_vtable_ptr = ctypes.addressof(_ReadyListener_vtable) +class Sampler(abc.ABC): + """Consumer-implemented source of scalar samples + + Subclass it and implement every method to pass an instance where the + API takes a `Sampler`. The producer may call the methods from any + thread until it releases the instance. An exception a method raises + reaches the producer as a failure of that call (a method that throws + an error domain reports that domain's codes with their fields). + """ + + @abc.abstractmethod + def threshold(self, level: int | None) -> float | None: + """The cutoff for a level, if the sampler has one""" + + @abc.abstractmethod + def samples(self, limit: int | None, weights: list[float]) -> Sequence[int]: + """Up to `limit` samples, failing when the pantry is empty + + Raises + ------ + PantryError + To report one of the domain's codes, with its fields, to the producer. + """ + + @abc.abstractmethod + def describe(self) -> str: + """A free-form description, failing with an untyped error""" + + +_Sampler_threshold_t = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_bool, ctypes.c_uint32, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(_ErrorStruct)) +_Sampler_samples_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_bool, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_Sampler_describe_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +class _SamplerVtable(ctypes.Structure): + """The C vtable `kitchen_sink_kitchen_Sampler_vtable`.""" + + _fields_ = [ + ("size", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("free", _CallbackFree), + ("threshold", _Sampler_threshold_t), + ("samples", _Sampler_samples_t), + ("describe", _Sampler_describe_t), + ] + + +def _Sampler_threshold(ctx: int | None, has_level: bool, level: int, out_value: Any, out_err: Any) -> bool: + try: + _ret = _callback_get(ctx).threshold(level if has_level else None) + if _ret is None: + return False + out_value[0] = _float(_ret, "Sampler.threshold() result") + return True + except BaseException as exc: + _callback_fail(out_err, exc, _FOREIGN) + return False + + +def _Sampler_samples(ctx: int | None, has_limit: bool, limit: int, weights_ptr: int | None, weights_len: int, out_ptr: Any, out_len: Any, out_err: Any) -> None: + try: + _callback_return_array(out_ptr, out_len, _callback_get(ctx).samples(limit if has_limit else None, _peek_array(weights_ptr, weights_len, "f64")), "i16", "Sampler.samples() result") + except BaseException as exc: + _callback_fail(out_err, exc, _GENERIC, PantryError) + + +def _Sampler_describe(ctx: int | None, out_ptr: Any, out_len: Any, out_err: Any) -> None: + try: + _callback_return_bytes(out_ptr, out_len, _callback_get(ctx).describe().encode("utf-8")) + except BaseException as exc: + _callback_fail(out_err, exc, _GENERIC) + + +_Sampler_vtable = _SamplerVtable( + ctypes.sizeof(_SamplerVtable), + 0, # flags: callable from any thread + _callback_free_fn, + _Sampler_threshold_t(_Sampler_threshold), + _Sampler_samples_t(_Sampler_samples), + _Sampler_describe_t(_Sampler_describe), +) +_Sampler_vtable_ptr = ctypes.addressof(_Sampler_vtable) + + _c_kitchen_Gadget_new = _bind("kitchen_sink_kitchen_Gadget_new", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(_ErrorStruct)) _c_kitchen_Gadget_describe = _bind("kitchen_sink_kitchen_Gadget_describe", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) _c_kitchen_Gadget_poke = _bind("kitchen_sink_kitchen_Gadget_poke", ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_Gadget_readings = _bind("kitchen_sink_kitchen_Gadget_readings", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_bool, ctypes.c_uint32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) _c_kitchen_Gadget_version = _bind("kitchen_sink_kitchen_Gadget_version", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) _c_kitchen_Gadget_clone = _bind("kitchen_sink_kitchen_Gadget_clone", ctypes.c_void_p, ctypes.c_void_p) _c_kitchen_Gadget_destroy = _bind("kitchen_sink_kitchen_Gadget_destroy", None, ctypes.c_void_p) @@ -1090,15 +1480,15 @@ class Gadget(_Object): def __init__(self, id: int) -> None: """Create a gadget with the given id""" _err = _ErrorStruct() - _ret = _c_kitchen_Gadget_new(id, ctypes.byref(_err)) + _ret = _c_kitchen_Gadget_new(_i64(id, "id"), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) self._init_handle(_required(_ret)) def describe(self) -> str: """Render the gadget as a human-readable string""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _self_p = self._acquire() try: _ret = _c_kitchen_Gadget_describe(_self_p, ctypes.byref(_out_len), ctypes.byref(_err)) @@ -1113,24 +1503,45 @@ class Gadget(_Object): Raises ------ - KitchenErrorsError - If the call reports one of the domain's error codes. + KitchenError + If the call fails with one of the domain's codes. """ + _times_v = _i32(times, "times") _err = _ErrorStruct() _self_p = self._acquire() try: - _ret: int = _c_kitchen_Gadget_poke(_self_p, times, ctypes.byref(_err)) + _ret: int = _c_kitchen_Gadget_poke(_self_p, _times_v, ctypes.byref(_err)) finally: self._release() if _err.code: - raise _kitchen_errors_error_from(*_read_error(_err)) + raise _kitchen_error_from(*_read_error(_err)) return _ret + def readings(self, limit: int | None) -> list[float]: + """The gadget's sensor readings, optionally limited + + Raises + ------ + Error + If the call fails. + """ + _limit_v = _u32(limit, "limit") if limit is not None else 0 + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _self_p = self._acquire() + try: + _ret = _c_kitchen_Gadget_readings(_self_p, limit is not None, _limit_v, ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + self._release() + if _err.code: + raise _error_from(*_read_error(_err)) + return _take_array(_ret, _out_len.value, "f64") + @staticmethod def version() -> str: """The gadget subsystem version string""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_Gadget_version(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1154,7 +1565,7 @@ _c_kitchen_i32_id = _bind("kitchen_sink_kitchen_i32_id", ctypes.c_int32, ctypes. def i32_id(x: int) -> int: _err = _ErrorStruct() - _ret: int = _c_kitchen_i32_id(x, ctypes.byref(_err)) + _ret: int = _c_kitchen_i32_id(_i32(x, "x"), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) return _ret @@ -1165,7 +1576,7 @@ _c_kitchen_u32_id = _bind("kitchen_sink_kitchen_u32_id", ctypes.c_uint32, ctypes def u32_id(x: int) -> int: _err = _ErrorStruct() - _ret: int = _c_kitchen_u32_id(x, ctypes.byref(_err)) + _ret: int = _c_kitchen_u32_id(_u32(x, "x"), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) return _ret @@ -1176,7 +1587,7 @@ _c_kitchen_i64_id = _bind("kitchen_sink_kitchen_i64_id", ctypes.c_int64, ctypes. def i64_id(x: int) -> int: _err = _ErrorStruct() - _ret: int = _c_kitchen_i64_id(x, ctypes.byref(_err)) + _ret: int = _c_kitchen_i64_id(_i64(x, "x"), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) return _ret @@ -1198,8 +1609,8 @@ _c_kitchen_echo_string = _bind("kitchen_sink_kitchen_echo_string", ctypes.c_void def echo_string(s: str) -> str: _s_b = s.encode("utf-8") - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_echo_string(_s_b, len(_s_b), ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1211,8 +1622,8 @@ _c_kitchen_echo_bytes = _bind("kitchen_sink_kitchen_echo_bytes", ctypes.c_void_p def echo_bytes(b: bytes) -> bytes: _b_b = bytes(b) - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_echo_bytes(_b_b, len(_b_b), ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1235,7 +1646,7 @@ def subscribe(listener: ReadyListener) -> int: _c_kitchen_maybe_subscribe = _bind("kitchen_sink_kitchen_maybe_subscribe", ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -def maybe_subscribe(listener: Optional[ReadyListener]) -> bool: +def maybe_subscribe(listener: ReadyListener | None) -> bool: """Register a listener if one is given""" _err = _ErrorStruct() _listener_ctx = _callback_register_opt(listener, ReadyListener) @@ -1248,7 +1659,7 @@ def maybe_subscribe(listener: Optional[ReadyListener]) -> bool: _c_kitchen_maybe_gadget = _bind("kitchen_sink_kitchen_maybe_gadget", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) -def maybe_gadget(fallback: Optional[Gadget]) -> Optional[Gadget]: +def maybe_gadget(fallback: Gadget | None) -> Gadget | None: """Optional object in both directions""" _err = _ErrorStruct() _fallback_p = _lend_opt(fallback, Gadget) @@ -1264,10 +1675,10 @@ def maybe_gadget(fallback: Optional[Gadget]) -> Optional[Gadget]: _c_kitchen_gadgets = _bind("kitchen_sink_kitchen_gadgets", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -def gadgets() -> List[Gadget]: +def gadgets() -> list[Gadget]: """A list of objects""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_gadgets(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1279,68 +1690,58 @@ _c_kitchen_StreamGadgetsIterator_next = _bind("kitchen_sink_kitchen_StreamGadget _c_kitchen_StreamGadgetsIterator_destroy = _bind("kitchen_sink_kitchen_StreamGadgetsIterator_destroy", None, ctypes.c_void_p) -class _kitchen_StreamGadgetsIterator(_Iterator): - """The lazy iterator `stream_gadgets` returns: each step pulls one element - from the producer.""" - - _destroy = staticmethod(_c_kitchen_StreamGadgetsIterator_destroy) - - def __next__(self) -> Gadget: - _item = ctypes.c_void_p() - _err = _ErrorStruct() - _p = self._step() - try: - _more = _c_kitchen_StreamGadgetsIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - if not _more: - self.close() - raise StopIteration - return Gadget._adopt(_required(_item.value)) +def _pull_kitchen_StreamGadgetsIterator(_p: int) -> Gadget: + _out_item = ctypes.c_void_p() + _err = _ErrorStruct() + _more = _c_kitchen_StreamGadgetsIterator_next(_p, ctypes.byref(_out_item), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + if not _more: + raise StopIteration + return Gadget._adopt(_required(_out_item.value)) -def stream_gadgets() -> Iterator[Gadget]: +def stream_gadgets() -> NativeIterator[Gadget]: """An iterator over objects - Returns a lazy iterator: each step pulls one element from the - producer. Exhaust or close() the iterator to release its native - handle (garbage collection also releases it). + Returns a lazy `NativeIterator`: each step pulls one element from + the producer. Exhaust it, `close()` it, or use it in a `with` + statement to release its native handle (garbage collection also + releases it). """ _err = _ErrorStruct() _ret = _c_kitchen_stream_gadgets(ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) - return _kitchen_StreamGadgetsIterator._adopt(_ret) + return _iterate(_required(_ret), _pull_kitchen_StreamGadgetsIterator, _c_kitchen_StreamGadgetsIterator_destroy) _c_kitchen_maybe_item = _bind("kitchen_sink_kitchen_maybe_item", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -def maybe_item(id: int) -> Optional[Item]: +def maybe_item(id: int) -> Item | None: """Optional struct return Raises ------ - KitchenErrorsError - If the call reports one of the domain's error codes. + KitchenError + If the call fails with one of the domain's codes. """ - _err = _ErrorStruct() _out_len = ctypes.c_size_t() - _ret = _c_kitchen_maybe_item(id, ctypes.byref(_out_len), ctypes.byref(_err)) + _err = _ErrorStruct() + _ret = _c_kitchen_maybe_item(_i64(id, "id"), ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: - raise _kitchen_errors_error_from(*_read_error(_err)) + raise _kitchen_error_from(*_read_error(_err)) return _decode(_take_bytes(_ret, _out_len.value), _read_opt_Item) _c_kitchen_list_items = _bind("kitchen_sink_kitchen_list_items", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -def list_items() -> List[Item]: +def list_items() -> list[Item]: """List of structs""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_list_items(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1350,10 +1751,10 @@ def list_items() -> List[Item]: _c_kitchen_get_attrs = _bind("kitchen_sink_kitchen_get_attrs", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) -def get_attrs() -> Dict[str, int]: +def get_attrs() -> dict[str, int]: """Map return type""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_get_attrs(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1365,41 +1766,31 @@ _c_kitchen_StreamItemsIterator_next = _bind("kitchen_sink_kitchen_StreamItemsIte _c_kitchen_StreamItemsIterator_destroy = _bind("kitchen_sink_kitchen_StreamItemsIterator_destroy", None, ctypes.c_void_p) -class _kitchen_StreamItemsIterator(_Iterator): - """The lazy iterator `stream_items` returns: each step pulls one element - from the producer.""" - - _destroy = staticmethod(_c_kitchen_StreamItemsIterator_destroy) - - def __next__(self) -> Item: - _item = ctypes.c_void_p() - _len = ctypes.c_size_t() - _err = _ErrorStruct() - _p = self._step() - try: - _more = _c_kitchen_StreamItemsIterator_next(_p, ctypes.byref(_item), ctypes.byref(_len), ctypes.byref(_err)) - finally: - self._release() - if _err.code: - raise _trap_from(*_read_error(_err)) - if not _more: - self.close() - raise StopIteration - return _decode(_take_bytes(_item.value, _len.value), _read_Item) +def _pull_kitchen_StreamItemsIterator(_p: int) -> Item: + _out_item = ctypes.c_void_p() + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _more = _c_kitchen_StreamItemsIterator_next(_p, ctypes.byref(_out_item), ctypes.byref(_out_len), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + if not _more: + raise StopIteration + return _decode(_take_bytes(_out_item.value, _out_len.value), _read_Item) -def stream_items() -> Iterator[Item]: +def stream_items() -> NativeIterator[Item]: """Iterator return type - Returns a lazy iterator: each step pulls one element from the - producer. Exhaust or close() the iterator to release its native - handle (garbage collection also releases it). + Returns a lazy `NativeIterator`: each step pulls one element from + the producer. Exhaust it, `close()` it, or use it in a `with` + statement to release its native handle (garbage collection also + releases it). """ _err = _ErrorStruct() _ret = _c_kitchen_stream_items(ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) - return _kitchen_StreamItemsIterator._adopt(_ret) + return _iterate(_required(_ret), _pull_kitchen_StreamItemsIterator, _c_kitchen_StreamItemsIterator_destroy) _c_kitchen_cross_module_token = _bind("kitchen_sink_kitchen_cross_module_token", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) @@ -1407,8 +1798,8 @@ _c_kitchen_cross_module_token = _bind("kitchen_sink_kitchen_cross_module_token", def cross_module_token() -> Token: """Returns the shared Token type from another module""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_cross_module_token(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1419,7 +1810,7 @@ _c_kitchen_do_async_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.PO _c_kitchen_do_async = _bind("kitchen_sink_kitchen_do_async", None, ctypes.c_char_p, ctypes.c_size_t, _c_kitchen_do_async_callback, ctypes.c_void_p) -def _kitchen_do_async_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: +def _kitchen_do_async_complete(context: int | None, err: Any, result_ptr: int | None, result_len: int) -> None: # Runs once, on a producer thread: take ownership of the result # here, then hand it to the awaiting event loop. try: @@ -1451,7 +1842,7 @@ _c_kitchen_do_cancellable_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, cty _c_kitchen_do_cancellable = _bind("kitchen_sink_kitchen_do_cancellable", None, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p, _c_kitchen_do_cancellable_callback, ctypes.c_void_p) -def _kitchen_do_cancellable_complete(context: Optional[int], err: Any, result_ptr: Optional[int], result_len: int) -> None: +def _kitchen_do_cancellable_complete(context: int | None, err: Any, result_ptr: int | None, result_len: int) -> None: # Runs once, on a producer thread: take ownership of the result # here, then hand it to the awaiting event loop. try: @@ -1487,9 +1878,9 @@ _c_kitchen_legacy_op = _bind("kitchen_sink_kitchen_legacy_op", ctypes.c_int32, c def legacy_op() -> int: """Legacy operation kept for compatibility - Deprecated: Use new_op instead + Deprecated: Use `new_op` instead """ - warnings.warn("Use new_op instead", DeprecationWarning, stacklevel=2) + warnings.warn("Use `new_op` instead", DeprecationWarning, stacklevel=2) _err = _ErrorStruct() _ret: int = _c_kitchen_legacy_op(ctypes.byref(_err)) if _err.code: @@ -1501,7 +1892,7 @@ _c_kitchen_new_op = _bind("kitchen_sink_kitchen_new_op", ctypes.c_int32, ctypes. def new_op() -> int: - """Replacement for legacy_op""" + """Replacement for `legacy_op`""" _err = _ErrorStruct() _ret: int = _c_kitchen_new_op(ctypes.byref(_err)) if _err.code: @@ -1509,6 +1900,308 @@ def new_op() -> int: return _ret +_c_kitchen_opt_i32_id = _bind("kitchen_sink_kitchen_opt_i32_id", ctypes.c_bool, ctypes.c_bool, ctypes.c_int32, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) + + +def opt_i32_id(x: int | None) -> int | None: + """Optional scalar in both directions""" + _out_value = ctypes.c_int32() + _err = _ErrorStruct() + _ret = _c_kitchen_opt_i32_id(x is not None, _i32(x, "x") if x is not None else 0, ctypes.byref(_out_value), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _out_value.value if _ret else None + + +_c_kitchen_opt_bool_id = _bind("kitchen_sink_kitchen_opt_bool_id", ctypes.c_bool, ctypes.c_bool, ctypes.c_bool, ctypes.POINTER(ctypes.c_bool), ctypes.POINTER(_ErrorStruct)) + + +def opt_bool_id(x: bool | None) -> bool | None: + _out_value = ctypes.c_bool() + _err = _ErrorStruct() + _ret = _c_kitchen_opt_bool_id(x is not None, x if x is not None else False, ctypes.byref(_out_value), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _out_value.value if _ret else None + + +_c_kitchen_opt_priority = _bind("kitchen_sink_kitchen_opt_priority", ctypes.c_bool, ctypes.c_bool, ctypes.c_int32, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) + + +def opt_priority(p: Priority | None) -> Priority | None: + """Optional C-style enum in both directions""" + _out_value = ctypes.c_int32() + _err = _ErrorStruct() + _ret = _c_kitchen_opt_priority(p is not None, _i32(p, "p") if p is not None else 0, ctypes.byref(_out_value), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return Priority(_out_value.value) if _ret else None + + +_c_kitchen_opt_mixed = _bind("kitchen_sink_kitchen_opt_mixed", ctypes.c_bool, ctypes.c_double, ctypes.c_bool, ctypes.c_float, ctypes.c_bool, ctypes.c_int64, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint16), ctypes.POINTER(_ErrorStruct)) + + +def opt_mixed(base: float, scale: float | None, offset: int | None, label: str | None) -> int | None: + """Optional scalars next to ordinary parameters""" + _scale_v = scale if scale is not None else 0.0 + _offset_v = _i64(offset, "offset") if offset is not None else 0 + _label_w = _Writer() + _write_opt_string(_label_w, label) + _out_value = ctypes.c_uint16() + _err = _ErrorStruct() + _label_b = _label_w.finish() + _ret = _c_kitchen_opt_mixed(base, scale is not None, _scale_v, offset is not None, _offset_v, _label_b, len(_label_b), ctypes.byref(_out_value), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _out_value.value if _ret else None + + +_c_kitchen_sum_f64 = _bind("kitchen_sink_kitchen_sum_f64", ctypes.c_double, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) + + +def sum_f64(xs: Sequence[float]) -> float: + """A typed-array parameter""" + _xs_a = _array(xs, "f64", "xs") + _err = _ErrorStruct() + _ret: float = _c_kitchen_sum_f64(_xs_a.buffer_info()[0], len(_xs_a), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _ret + + +_c_kitchen_scale_i32 = _bind("kitchen_sink_kitchen_scale_i32", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def scale_i32(xs: Sequence[int], factor: int) -> list[int]: + """Typed arrays in both directions""" + _xs_a = _array(xs, "i32", "xs") + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _ret = _c_kitchen_scale_i32(_xs_a.buffer_info()[0], len(_xs_a), _i32(factor, "factor"), ctypes.byref(_out_len), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _take_array(_ret, _out_len.value, "i32") + + +_c_kitchen_ids_u64 = _bind("kitchen_sink_kitchen_ids_u64", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def ids_u64(ids: Sequence[int]) -> list[int]: + _ids_a = _array(ids, "u64", "ids") + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _ret = _c_kitchen_ids_u64(_ids_a.buffer_info()[0], len(_ids_a), ctypes.byref(_out_len), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _take_array(_ret, _out_len.value, "u64") + + +_c_kitchen_mixed_slices = _bind("kitchen_sink_kitchen_mixed_slices", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def mixed_slices(a: Sequence[int], b: Sequence[int], c: Sequence[int], d: Sequence[int], e: Sequence[int], f: Sequence[float]) -> list[int]: + """Every slice element type""" + _a_a = _array(a, "i8", "a") + _b_a = _array(b, "i16", "b") + _c_a = _array(c, "u16", "c") + _d_a = _array(d, "u32", "d") + _e_a = _array(e, "i64", "e") + _f_a = _array(f, "f32", "f") + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _ret = _c_kitchen_mixed_slices(_a_a.buffer_info()[0], len(_a_a), _b_a.buffer_info()[0], len(_b_a), _c_a.buffer_info()[0], len(_c_a), _d_a.buffer_info()[0], len(_d_a), _e_a.buffer_info()[0], len(_e_a), _f_a.buffer_info()[0], len(_f_a), ctypes.byref(_out_len), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _take_array(_ret, _out_len.value, "u32") + + +_c_kitchen_async_maybe_count_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_bool, ctypes.c_int64) +_c_kitchen_async_maybe_count = _bind("kitchen_sink_kitchen_async_maybe_count", None, _c_kitchen_async_maybe_count_callback, ctypes.c_void_p) + + +def _kitchen_async_maybe_count_complete(context: int | None, err: Any, has_result: bool, result: int) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _trap_from), None) + else: + _async_settle(context, None, result if has_result else None) + except BaseException as exc: + _async_settle(context, exc, None) + + +_kitchen_async_maybe_count_completion = _c_kitchen_async_maybe_count_callback(_kitchen_async_maybe_count_complete) + + +async def async_maybe_count() -> int | None: + """Async returning an optional scalar""" + _call, _future = _async_begin() + try: + _c_kitchen_async_maybe_count(_kitchen_async_maybe_count_completion, _call) + except BaseException: + _async_abandon(_call) + raise + _result: int | None = await _future + return _result + + +_c_kitchen_async_samples_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_kitchen_async_samples = _bind("kitchen_sink_kitchen_async_samples", None, ctypes.c_uint32, ctypes.c_void_p, _c_kitchen_async_samples_callback, ctypes.c_void_p) + + +def _kitchen_async_samples_complete(context: int | None, err: Any, result_ptr: int | None, result_len: int) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _trap_from), None) + else: + _async_settle(context, None, _take_array(result_ptr, result_len, "f32")) + except BaseException as exc: + _async_settle(context, exc, None) + + +_kitchen_async_samples_completion = _c_kitchen_async_samples_callback(_kitchen_async_samples_complete) + + +async def async_samples(n: int) -> list[float]: + """Async returning a typed array""" + _call, _future = _async_begin() + _token = _cancel_token_create() + try: + _c_kitchen_async_samples(_u32(n, "n"), _token, _kitchen_async_samples_completion, _call) + except BaseException: + _async_abandon(_call) + _cancel_token_destroy(_token) + raise + _result: list[float] = await _async_wait_cancellable(_future, _token) + return _result + + +_c_kitchen_stream_maybe = _bind("kitchen_sink_kitchen_stream_maybe", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamMaybeIterator_next = _bind("kitchen_sink_kitchen_StreamMaybeIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_bool), ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamMaybeIterator_destroy = _bind("kitchen_sink_kitchen_StreamMaybeIterator_destroy", None, ctypes.c_void_p) + + +def _pull_kitchen_StreamMaybeIterator(_p: int) -> int | None: + _out_has_item = ctypes.c_bool() + _out_item = ctypes.c_int32() + _err = _ErrorStruct() + _more = _c_kitchen_StreamMaybeIterator_next(_p, ctypes.byref(_out_has_item), ctypes.byref(_out_item), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + if not _more: + raise StopIteration + return _out_item.value if _out_has_item.value else None + + +def stream_maybe() -> NativeIterator[int | None]: + """Iterator of optional scalars + + Returns a lazy `NativeIterator`: each step pulls one element from + the producer. Exhaust it, `close()` it, or use it in a `with` + statement to release its native handle (garbage collection also + releases it). + """ + _err = _ErrorStruct() + _ret = _c_kitchen_stream_maybe(ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _iterate(_required(_ret), _pull_kitchen_StreamMaybeIterator, _c_kitchen_StreamMaybeIterator_destroy) + + +_c_kitchen_stream_chunks = _bind("kitchen_sink_kitchen_stream_chunks", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamChunksIterator_next = _bind("kitchen_sink_kitchen_StreamChunksIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamChunksIterator_destroy = _bind("kitchen_sink_kitchen_StreamChunksIterator_destroy", None, ctypes.c_void_p) + + +def _pull_kitchen_StreamChunksIterator(_p: int) -> list[int]: + _out_item = ctypes.c_void_p() + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _more = _c_kitchen_StreamChunksIterator_next(_p, ctypes.byref(_out_item), ctypes.byref(_out_len), ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + if not _more: + raise StopIteration + return _take_array(_out_item.value, _out_len.value, "i32") + + +def stream_chunks() -> NativeIterator[list[int]]: + """Iterator of typed arrays + + Returns a lazy `NativeIterator`: each step pulls one element from + the producer. Exhaust it, `close()` it, or use it in a `with` + statement to release its native handle (garbage collection also + releases it). + """ + _err = _ErrorStruct() + _ret = _c_kitchen_stream_chunks(ctypes.byref(_err)) + if _err.code: + raise _trap_from(*_read_error(_err)) + return _iterate(_required(_ret), _pull_kitchen_StreamChunksIterator, _c_kitchen_StreamChunksIterator_destroy) + + +_c_kitchen_sample_with = _bind("kitchen_sink_kitchen_sample_with", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_bool, ctypes.c_uint32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def sample_with(sampler: Sampler, level: int | None) -> list[int]: + """Drive a consumer-implemented sampler + + Raises + ------ + PantryError + If the call fails with one of the domain's codes. + """ + _level_v = _u32(level, "level") if level is not None else 0 + _out_len = ctypes.c_size_t() + _err = _ErrorStruct() + _sampler_ctx = _callback_register(sampler, Sampler) + _ret = _c_kitchen_sample_with(_sampler_ctx, _Sampler_vtable_ptr, level is not None, _level_v, ctypes.byref(_out_len), ctypes.byref(_err)) + if _err.code: + raise _pantry_error_from(*_read_error(_err)) + return _take_array(_ret, _out_len.value, "i16") + + +_c_kitchen_take_from_pantry = _bind("kitchen_sink_kitchen_take_from_pantry", ctypes.c_int32, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) + + +def take_from_pantry(item: str) -> int: + """Fails with the second domain + + Raises + ------ + PantryError + If the call fails with one of the domain's codes. + """ + _item_b = item.encode("utf-8") + _err = _ErrorStruct() + _ret: int = _c_kitchen_take_from_pantry(_item_b, len(_item_b), ctypes.byref(_err)) + if _err.code: + raise _pantry_error_from(*_read_error(_err)) + return _ret + + +_c_kitchen_parse_any = _bind("kitchen_sink_kitchen_parse_any", ctypes.c_int64, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) + + +def parse_any(text: str) -> int: + """Fails with an untyped error + + Raises + ------ + Error + If the call fails. + """ + _text_b = text.encode("utf-8") + _err = _ErrorStruct() + _ret: int = _c_kitchen_parse_any(_text_b, len(_text_b), ctypes.byref(_err)) + if _err.code: + raise _error_from(*_read_error(_err)) + return _ret + + # === Module: kitchen.nested === @@ -1517,8 +2210,8 @@ _c_kitchen_nested_hello = _bind("kitchen_sink_kitchen_nested_hello", ctypes.c_vo def hello() -> str: """Trivial nested-module function""" - _err = _ErrorStruct() _out_len = ctypes.c_size_t() + _err = _ErrorStruct() _ret = _c_kitchen_nested_hello(ctypes.byref(_out_len), ctypes.byref(_err)) if _err.code: raise _trap_from(*_read_error(_err)) @@ -1528,24 +2221,24 @@ def hello() -> str: # === Value-buffer codecs of composite types === -def _write_list_string(_w: _Writer, value: List[str]) -> None: +def _write_list_string(_w: _Writer, value: Sequence[str]) -> None: _w.write_count(len(value)) for item in value: _w.write_string(item) -def _read_list_string(_r: _Reader) -> List[str]: +def _read_list_string(_r: _Reader) -> list[str]: return [_r.read_string() for _ in range(_r.read_count())] -def _write_map_string_string(_w: _Writer, value: Dict[str, str]) -> None: +def _write_map_string_string(_w: _Writer, value: Mapping[str, str]) -> None: _w.write_count(len(value)) for key, item in value.items(): _w.write_string(key) _w.write_string(item) -def _read_map_string_string(_r: _Reader) -> Dict[str, str]: +def _read_map_string_string(_r: _Reader) -> dict[str, str]: n = _r.read_count() value = {_r.read_string(): _r.read_string() for _ in range(n)} if len(value) != n: @@ -1553,64 +2246,64 @@ def _read_map_string_string(_r: _Reader) -> Dict[str, str]: return value -def _write_opt_i64(_w: _Writer, value: Optional[int]) -> None: +def _write_opt_i64(_w: _Writer, value: int | None) -> None: _w.write_flag(value is not None) if value is not None: _w.write_i64(value) -def _read_opt_i64(_r: _Reader) -> Optional[int]: +def _read_opt_i64(_r: _Reader) -> int | None: return _r.read_i64() if _r.read_flag() else None -def _write_opt_Gadget(_w: _Writer, value: Optional[Gadget]) -> None: +def _write_opt_Gadget(_w: _Writer, value: Gadget | None) -> None: _w.write_flag(value is not None) if value is not None: _w.write_object(value, Gadget) -def _read_opt_Gadget(_r: _Reader) -> Optional[Gadget]: +def _read_opt_Gadget(_r: _Reader) -> Gadget | None: return Gadget._adopt(_r.read_object()) if _r.read_flag() else None -def _write_list_Gadget(_w: _Writer, value: List[Gadget]) -> None: +def _write_list_Gadget(_w: _Writer, value: Sequence[Gadget]) -> None: _w.write_count(len(value)) for item in value: _w.write_object(item, Gadget) -def _read_list_Gadget(_r: _Reader) -> List[Gadget]: +def _read_list_Gadget(_r: _Reader) -> list[Gadget]: return [Gadget._adopt(_r.read_object()) for _ in range(_r.read_count())] -def _write_opt_Item(_w: _Writer, value: Optional[Item]) -> None: +def _write_opt_Item(_w: _Writer, value: Item | None) -> None: _w.write_flag(value is not None) if value is not None: _write_Item(_w, value) -def _read_opt_Item(_r: _Reader) -> Optional[Item]: +def _read_opt_Item(_r: _Reader) -> Item | None: return _read_Item(_r) if _r.read_flag() else None -def _write_list_Item(_w: _Writer, value: List[Item]) -> None: +def _write_list_Item(_w: _Writer, value: Sequence[Item]) -> None: _w.write_count(len(value)) for item in value: _write_Item(_w, item) -def _read_list_Item(_r: _Reader) -> List[Item]: +def _read_list_Item(_r: _Reader) -> list[Item]: return [_read_Item(_r) for _ in range(_r.read_count())] -def _write_map_string_i32(_w: _Writer, value: Dict[str, int]) -> None: +def _write_map_string_i32(_w: _Writer, value: Mapping[str, int]) -> None: _w.write_count(len(value)) for key, item in value.items(): _w.write_string(key) _w.write_i32(item) -def _read_map_string_i32(_r: _Reader) -> Dict[str, int]: +def _read_map_string_i32(_r: _Reader) -> dict[str, int]: n = _r.read_count() value = {_r.read_string(): _r.read_i32() for _ in range(n)} if len(value) != n: @@ -1618,17 +2311,33 @@ def _read_map_string_i32(_r: _Reader) -> Dict[str, int]: return value +def _write_opt_string(_w: _Writer, value: str | None) -> None: + _w.write_flag(value is not None) + if value is not None: + _w.write_string(value) + + +def _read_opt_string(_r: _Reader) -> str | None: + return _r.read_string() if _r.read_flag() else None + + __all__ = [ "Error", "InternalError", + "LibraryLoadError", + "NativeIterator", "Token", "ping", - "KitchenErrorsError", - "NotFound", - "InvalidInput", + "KitchenError", + "NotFoundError", + "InvalidInputError", + "PantryError", + "OutOfStockError", + "SpoiledError", "Priority", "Item", "ReadyListener", + "Sampler", "Gadget", "bool_id", "i32_id", @@ -1651,6 +2360,21 @@ __all__ = [ "do_cancellable", "legacy_op", "new_op", + "opt_i32_id", + "opt_bool_id", + "opt_priority", + "opt_mixed", + "sum_f64", + "scale_i32", + "ids_u64", + "mixed_slices", + "async_maybe_count", + "async_samples", + "stream_maybe", + "stream_chunks", + "sample_with", + "take_from_pantry", + "parse_any", "hello", ] diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_py_typed.snap deleted file mode 100644 index 6ac1df61..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_py_typed.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap deleted file mode 100644 index 6cc575f6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap +++ /dev/null @@ -1,24 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "kitchen_sink" -version = "0.1.0" -description = "Native bindings for kitchen_sink" -requires-python = ">=3.9" - -[tool.setuptools] -packages = ["kitchen_sink"] - -[tool.setuptools.package-data] -"kitchen_sink" = ["py.typed", "*.so", "*.dylib", "*.dll"] - -# END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_py.snap deleted file mode 100644 index 6232b8e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_py.snap +++ /dev/null @@ -1,769 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -"""ctypes bindings for the nested_modules native library.""" -from __future__ import annotations - -import builtins -import ctypes -import os -import platform -import struct -import threading -import warnings -from dataclasses import dataclass -from enum import IntEnum -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar - - -class Error(Exception): - """An error a fallible call reports. - - `code` is positive for one of the API's declared error codes, raised as - the declaring module's typed subclasses, and negative for a runtime - failure of a call that declares errors: a generic failure (-1), a - producer panic (-2), a marshalling failure (-3), or an exception raised - by a callback-interface implementation (-4). A cancelled async call (-5) - raises `asyncio.CancelledError` instead. A call that declares no errors - never raises this; its failures are bugs and raise `InternalError`. - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - CANCELLED_ERROR_CODE = -5 - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - def _payload(self) -> bytes: - """The code's fields as a value buffer (a code with fields overrides - this).""" - return b"" - - -class InternalError(RuntimeError): - """A failure of a call that declares no errors: a producer panic (-2), a - marshalling failure (-3), a callback-interface failure the producer let - through (-4), or another runtime failure (-1). These are bugs, so they - are unchecked; `code` and `message` say what went wrong. - """ - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - -class _ErrorStruct(ctypes.Structure): - """The C `nested_modules_error` out-parameter every call reports through.""" - - _fields_ = [ - ("code", ctypes.c_int32), - ("message", ctypes.c_char_p), - ("payload_ptr", ctypes.c_void_p), - ("payload_len", ctypes.c_size_t), - ] - - -def _load_library() -> ctypes.CDLL: - # An explicit path wins; then a library bundled inside this package (a - # platform wheel); then the system loader's search path. - override = os.environ.get("NESTED_MODULES_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libnested_modules.dylib" - elif system == "Windows": - name = "nested_modules.dll" - else: - name = "libnested_modules.so" - bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) - if os.path.exists(bundled): - return ctypes.CDLL(bundled) - try: - return ctypes.CDLL(name) - except OSError as exc: - raise ImportError( - f"cannot load the native library {name!r} ({exc}); " - "set NESTED_MODULES_LIBRARY to its path" - ) from exc - - -_lib = _load_library() - - -def _bind(name: str, restype: Any, *argtypes: Any) -> Any: - """Look up one exported symbol and fix its C signature. Every prototype - is bound once, at import time.""" - fn = getattr(_lib, name) - fn.restype = restype - fn.argtypes = argtypes - return fn - - -class _ContractEntry(ctypes.Structure): - """The C `nested_modules_contract_entry`: one declaration's id and hash.""" - - _fields_ = [("id", ctypes.c_uint64), ("hash", ctypes.c_uint64)] - - -# The C ABI revision these bindings were generated against and, per -# top-level module's contract symbol, the declarations they rely on: -# (id, hash, dotted path). -_ABI_VERSION = 4 -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { - "nested_modules_graphics_contract": [ - (0x2c29eef9327a6bd4, 0x12983d5271651389, "graphics.Unit"), - (0x304088bbc28f0015, 0x97e6e809913be6b6, "graphics.new_canvas"), - (0x49f8837532df9ac6, 0x32e5b27e81ee0957, "graphics.Canvas"), - (0x55717fa027336b47, 0xb2bd3a6bdcce48c8, "graphics.shapes.describe"), - (0x7626b91b8f1f53cd, 0x867bbf7cf0f41c74, "graphics.shapes.geometry.compute"), - (0xb3efcb50d9a61ab7, 0x3cb0207dfad2a149, "graphics.GraphicsError"), - (0xc62b16a9591b744b, 0x7895610c8487eb1b, "graphics.shapes.Shape"), - (0xe13001f4983ccd35, 0x16e7de070b4a4cfc, "graphics.shapes.geometry.Detail"), - (0xf3f658bcfc5dba62, 0x2665ccb47356e746, "graphics.shapes.make_shape"), - ], -} - - -def _check_contract() -> None: - """Refuse to load a library built for another ABI revision, or one that - lacks or changed a declaration these bindings use, before any other - symbol is bound.""" - try: - abi_version = _bind("nested_modules_abi_version", ctypes.c_uint32) - except AttributeError: - raise ImportError( - f"{_lib._name} does not export nested_modules_abi_version; " - "it is not the library these bindings were generated for" - ) from None - found = abi_version() - if found != _ABI_VERSION: - raise ImportError( - f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " - f"but {_lib._name} reports revision {found}" - ) - for symbol, expected in _CONTRACTS.items(): - try: - contract = _bind( - symbol, ctypes.POINTER(_ContractEntry), ctypes.POINTER(ctypes.c_size_t) - ) - except AttributeError: - raise ImportError(f"{_lib._name} does not export {symbol}") from None - length = ctypes.c_size_t() - table = contract(ctypes.byref(length)) - hashes = {table[i].id: table[i].hash for i in range(length.value)} if table else {} - for entry_id, entry_hash, path in expected: - actual = hashes.get(entry_id) - if actual is None: - raise ImportError(f"{path} is missing from the library ({_lib._name})") - if actual != entry_hash: - raise ImportError( - f"{path} changed since these bindings were generated ({_lib._name})" - ) - - -_check_contract() - -_error_clear = _bind("nested_modules_error_clear", None, ctypes.POINTER(_ErrorStruct)) -_free_bytes = _bind("nested_modules_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) -_debug_live_fn = _bind("nested_modules_debug_live", ctypes.c_uint64, ctypes.c_int32) - - -def _debug_live(kind: int) -> int: - """The producer's live-resource counter `kind` (0 objects, 1 callbacks, - 2 iterators, 3 cancel tokens, 4 byte runs; -1 is 1 when the producer - counts at all). Always 0 unless the producer was built with its leak - counters enabled.""" - live: int = _debug_live_fn(kind) - return live - - -def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: - """Copy a filled out-err slot's code, message, and payload, then release - them.""" - code = err.code - message = err.message.decode("utf-8", "replace") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _error_clear(ctypes.byref(err)) - return code, message, payload - - -def _trap_from(code: int, message: str, payload: bytes = b"") -> InternalError: - """The exception for a failed call that declares no errors.""" - return InternalError(code, message) - - -def _take_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a producer-owned byte run (a string, bytes, or value buffer) and - release it with nested_modules_free_bytes.""" - if not ptr: - return b"" - try: - return ctypes.string_at(ptr, length) - finally: - _free_bytes(ptr, length) - - -def _take_str(ptr: Optional[int], length: int) -> str: - """Copy and release a producer-owned UTF-8 string.""" - return _take_bytes(ptr, length).decode("utf-8") - - -def _peek_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a byte run the producer only lends for the current call.""" - return ctypes.string_at(ptr, length) if ptr and length else b"" - - -_H = TypeVar("_H", bound="_Handle") - - -class _Handle: - """Owns one native reference and releases it exactly once. - - Calls lend the pointer through `_acquire`/`_release`, so `close()` from - another thread while a call is in flight defers the release until the - last such call returns instead of freeing the object mid-call. - """ - - # The native destroy (or iterator release) function, set per subclass. - _destroy: Callable[[int], None] - - def _init_handle(self, ptr: int) -> None: - self._ptr: Optional[int] = ptr - self._key = ptr - self._calls = 0 - self._closing = False - self._lock = threading.Lock() - - @classmethod - def _adopt(cls: Type[_H], ptr: int) -> _H: - """Wrap one strong reference the producer handed over.""" - obj = cls.__new__(cls) - obj._init_handle(ptr) - return obj - - def _acquire(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise ValueError(f"{type(self).__name__} is closed") - self._calls += 1 - return ptr - - def _release(self) -> None: - with self._lock: - self._calls -= 1 - if self._calls or not self._closing: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def close(self) -> None: - """Release the native reference. Idempotent; the wrapper is unusable - afterwards.""" - lock = getattr(self, "_lock", None) - if lock is None: - return - with lock: - if self._closing: - return - self._closing = True - if self._calls: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def __enter__(self: _H) -> _H: - return self - - def __exit__(self, *exc: object) -> None: - self.close() - - def __del__(self) -> None: - # An unreachable wrapper has no call in flight: release directly. - ptr = getattr(self, "_ptr", None) - if ptr is not None: - self._ptr = None - type(self)._destroy(ptr) - - -class _Object(_Handle): - """Base of every interface wrapper: a reference-counted native object. - Two wrappers are equal when they hold the same native object.""" - - # The native clone function, set per subclass. - _clone: Callable[[int], int] - - def _clone_ref(self) -> int: - """A new strong reference to the same object, for a receiver that - adopts it (an object token, or a callback method's return).""" - ptr = self._acquire() - try: - return type(self)._clone(ptr) - finally: - self._release() - - def __eq__(self, other: object) -> bool: - if not isinstance(other, _Object): - return NotImplemented - return self is other or (self._ptr is not None and self._ptr == other._ptr) - - def __hash__(self) -> int: - return hash(self._key) - - -class _Iterator(_Handle): - """Base of every lazy iterator over a producer stream: one native `next` - call per step; the handle is released on exhaustion, `close()`, or - garbage collection.""" - - def __iter__(self: _H) -> _H: - return self - - def _step(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise StopIteration - self._calls += 1 - return ptr - - -def _required(ptr: Optional[int]) -> int: - """A non-null object pointer the producer returned.""" - if not ptr: - raise InternalError(-3, "the producer returned a null object") - return ptr - - -def _lend(obj: Any, cls: Type[_Object]) -> int: - """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - return obj._acquire() - - -def _lend_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: - """`_lend` for an optional object: None lends a null pointer.""" - return None if obj is None else _lend(obj, cls) - - -def _release_opt(obj: Optional[_Object]) -> None: - if obj is not None: - obj._release() - - -_I8 = struct.Struct(" InternalError: - return InternalError(-3, f"malformed value buffer: {what}") - - -class _Writer: - """Encodes values in the value-buffer wire format: little-endian, - packed, no alignment.""" - - __slots__ = ("_buf", "_objects") - - def __init__(self) -> None: - self._buf = bytearray() - self._objects: List[Tuple[int, _Object]] = [] - - def finish(self) -> bytes: - # Object tokens are minted last, so an encoding that fails part-way - # never leaks a strong reference. - minted: List[Tuple[_Object, int]] = [] - try: - for pos, obj in self._objects: - ptr = obj._clone_ref() - minted.append((obj, ptr)) - _U64.pack_into(self._buf, pos, ptr) - except BaseException: - for obj, ptr in minted: - type(obj)._destroy(ptr) - raise - self._objects = [] - return bytes(self._buf) - - def write_bool(self, v: bool) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += _I8.pack(v) - - def write_u8(self, v: int) -> None: - self._buf += _U8.pack(v) - - def write_i16(self, v: int) -> None: - self._buf += _I16.pack(v) - - def write_u16(self, v: int) -> None: - self._buf += _U16.pack(v) - - def write_i32(self, v: int) -> None: - self._buf += _I32.pack(v) - - def write_u32(self, v: int) -> None: - self._buf += _U32.pack(v) - - def write_i64(self, v: int) -> None: - self._buf += _I64.pack(v) - - def write_u64(self, v: int) -> None: - self._buf += _U64.pack(v) - - def write_f32(self, v: float) -> None: - self._buf += _F32.pack(v) - - def write_f64(self, v: float) -> None: - self._buf += _F64.pack(v) - - def write_count(self, n: int) -> None: - self._buf += _U32.pack(n) - - def write_flag(self, present: bool) -> None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - data = v.encode("utf-8") - self._buf += _U32.pack(len(data)) - self._buf += data - - def write_bytes(self, v: bytes) -> None: - self._buf += _U32.pack(len(v)) - self._buf += v - - def write_numbers(self, code: str, values: List[Any]) -> None: - """A list of one fixed-width numeric primitive (`code` is its struct - format character), packed in one call.""" - self._buf += _U32.pack(len(values)) - self._buf += struct.pack(f"<{len(values)}{code}", *values) - - def write_object(self, obj: Any, cls: Type[_Object]) -> None: - """Reserve an object token: one strong reference to `obj` the reader - adopts, cloned in `finish()`. A closed wrapper is rejected now.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - obj._acquire() - obj._release() - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -class _Reader: - """Decodes values from the value-buffer wire format, rejecting truncated - buffers, invalid flag bytes, invalid UTF-8, undeclared enum values, and - trailing data.""" - - __slots__ = ("_data", "_pos") - - def __init__(self, data: bytes) -> None: - self._data = data - self._pos = 0 - - def _take(self, n: int, what: str) -> int: - pos = self._pos - if len(self._data) - pos < n: - raise _malformed(f"truncated {what}") - self._pos = pos + n - return pos - - def read_bool(self) -> bool: - b = self._data[self._take(1, "bool")] - if b > 1: - raise _malformed("invalid bool byte") - return b == 1 - - def read_i8(self) -> int: - return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) - - def read_u8(self) -> int: - return self._data[self._take(1, "u8")] - - def read_i16(self) -> int: - return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) - - def read_u16(self) -> int: - return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) - - def read_i32(self) -> int: - return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) - - def read_u32(self) -> int: - return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) - - def read_i64(self) -> int: - return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) - - def read_u64(self) -> int: - return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) - - def read_f32(self) -> float: - return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) - - def read_f64(self) -> float: - return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) - - def read_count(self) -> int: - # An element count, not a byte length: elements can encode to zero - # bytes, so it is not bounded by the remaining data. - return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) - - def read_flag(self) -> bool: - b = self._data[self._take(1, "option flag")] - if b > 1: - raise _malformed("invalid option flag") - return b == 1 - - def read_bytes(self) -> bytes: - n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) - pos = self._take(n, "bytes") - return self._data[pos:pos + n] - - def read_string(self) -> str: - try: - return self.read_bytes().decode("utf-8") - except UnicodeDecodeError: - raise _malformed("string is not valid UTF-8") from None - - def read_enum(self, cls: Type[_E]) -> _E: - value = self.read_i32() - try: - return cls(value) - except ValueError: - raise _malformed(f"undeclared {cls.__name__} value {value}") from None - - def read_numbers(self, code: str, size: int) -> List[Any]: - """A list of one fixed-width numeric primitive, unpacked in one call.""" - n = self.read_count() - pos = self._take(n * size, "list") - return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) - - def read_object(self) -> int: - """An object token: one strong reference the caller adopts.""" - ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) - if ptr == 0: - raise _malformed("null object token") - return ptr - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise _malformed("trailing bytes") - - -def _decode(data: bytes, read: Callable[[_Reader], _T]) -> _T: - """Decode exactly one value from `data`.""" - r = _Reader(data) - value = read(r) - r.expect_end() - return value - - -# === Module: graphics === - - -class GraphicsError(Error): - """Base exception for the `graphics` module's error domain.""" - - OutOfBounds: Type[OutOfBounds] - - -class OutOfBounds(GraphicsError): - """coordinate out of bounds""" - - CODE = 1 - - def __init__(self, message: str = "coordinate out of bounds") -> None: - super().__init__(1, message) - - -GraphicsError.OutOfBounds = OutOfBounds - - -def _graphics_error_from(code: int, message: str, payload: bytes = b"") -> Error: - """The GraphicsError subclass for `code` with its payload fields, or the - plain Error for a runtime code (a panic, a marshalling failure). - """ - if code == 1: - return OutOfBounds(message) - return Error(code, message) - - -class Unit(IntEnum): - """A measurement unit shared with nested submodules.""" - - pixels = 0 - points = 1 - - -@dataclass -class Canvas: - """Top-level struct that references a struct defined one level deeper.""" - - width: int - # Reference down into a nested module - root: Shape - - -def _write_Canvas(_w: _Writer, value: Canvas) -> None: - _w.write_i32(value.width) - _write_Shape(_w, value.root) - - -def _read_Canvas(_r: _Reader) -> Canvas: - return Canvas( - width=_r.read_i32(), - root=_read_Shape(_r), - ) - - -_c_graphics_new_canvas = _bind("nested_modules_graphics_new_canvas", ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def new_canvas(width: int) -> Canvas: - """Construct a Canvas.""" - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_graphics_new_canvas(width, ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Canvas) - - -# === Module: graphics.shapes === - - -@dataclass -class Shape: - """A struct one level deep that references both an ancestor enum and a deeper struct.""" - - sides: int - # Reference up to the parent module's enum - unit: Unit - # Reference down into a deeper module - detail: Detail - - -def _write_Shape(_w: _Writer, value: Shape) -> None: - _w.write_i32(value.sides) - _w.write_i32(value.unit) - _write_Detail(_w, value.detail) - - -def _read_Shape(_r: _Reader) -> Shape: - return Shape( - sides=_r.read_i32(), - unit=_r.read_enum(Unit), - detail=_read_Detail(_r), - ) - - -_c_graphics_shapes_make_shape = _bind("nested_modules_graphics_shapes_make_shape", ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def make_shape(sides: int) -> Shape: - """Construct a Shape.""" - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_graphics_shapes_make_shape(sides, ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Shape) - - -_c_graphics_shapes_describe = _bind("nested_modules_graphics_shapes_describe", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def describe() -> Canvas: - """Nested function returning a struct owned by an ancestor module.""" - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_graphics_shapes_describe(ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Canvas) - - -# === Module: graphics.shapes.geometry === - - -@dataclass -class Detail: - """A struct two levels deep.""" - - area: float - - -def _write_Detail(_w: _Writer, value: Detail) -> None: - _w.write_f64(value.area) - - -def _read_Detail(_r: _Reader) -> Detail: - return Detail( - area=_r.read_f64(), - ) - - -_c_graphics_shapes_geometry_compute = _bind("nested_modules_graphics_shapes_geometry_compute", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def compute(shape: Shape) -> Detail: - """Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - - Raises - ------ - GraphicsError - If the call reports one of the domain's error codes. - """ - _shape_w = _Writer() - _write_Shape(_shape_w, shape) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _shape_b = _shape_w.finish() - _ret = _c_graphics_shapes_geometry_compute(_shape_b, len(_shape_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _graphics_error_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Detail) - - -__all__ = [ - "Error", - "InternalError", - "GraphicsError", - "OutOfBounds", - "Unit", - "Canvas", - "new_canvas", - "Shape", - "make_shape", - "describe", - "Detail", - "compute", -] - -# END nested_modules.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_py.snap deleted file mode 100644 index 6c94bd07..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_py.snap +++ /dev/null @@ -1,852 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -"""ctypes bindings for the shapes native library.""" -from __future__ import annotations - -import builtins -import ctypes -import os -import platform -import struct -import threading -import warnings -from dataclasses import dataclass -from enum import IntEnum -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar - - -class Error(Exception): - """An error a fallible call reports. - - `code` is positive for one of the API's declared error codes, raised as - the declaring module's typed subclasses, and negative for a runtime - failure of a call that declares errors: a generic failure (-1), a - producer panic (-2), a marshalling failure (-3), or an exception raised - by a callback-interface implementation (-4). A cancelled async call (-5) - raises `asyncio.CancelledError` instead. A call that declares no errors - never raises this; its failures are bugs and raise `InternalError`. - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - CANCELLED_ERROR_CODE = -5 - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - def _payload(self) -> bytes: - """The code's fields as a value buffer (a code with fields overrides - this).""" - return b"" - - -class InternalError(RuntimeError): - """A failure of a call that declares no errors: a producer panic (-2), a - marshalling failure (-3), a callback-interface failure the producer let - through (-4), or another runtime failure (-1). These are bugs, so they - are unchecked; `code` and `message` say what went wrong. - """ - - def __init__(self, code: int, message: str) -> None: - super().__init__(f"({code}) {message}") - self.code = code - self.message = message - - -class _ErrorStruct(ctypes.Structure): - """The C `shapes_error` out-parameter every call reports through.""" - - _fields_ = [ - ("code", ctypes.c_int32), - ("message", ctypes.c_char_p), - ("payload_ptr", ctypes.c_void_p), - ("payload_len", ctypes.c_size_t), - ] - - -def _load_library() -> ctypes.CDLL: - # An explicit path wins; then a library bundled inside this package (a - # platform wheel); then the system loader's search path. - override = os.environ.get("SHAPES_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libshapes.dylib" - elif system == "Windows": - name = "shapes.dll" - else: - name = "libshapes.so" - bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) - if os.path.exists(bundled): - return ctypes.CDLL(bundled) - try: - return ctypes.CDLL(name) - except OSError as exc: - raise ImportError( - f"cannot load the native library {name!r} ({exc}); " - "set SHAPES_LIBRARY to its path" - ) from exc - - -_lib = _load_library() - - -def _bind(name: str, restype: Any, *argtypes: Any) -> Any: - """Look up one exported symbol and fix its C signature. Every prototype - is bound once, at import time.""" - fn = getattr(_lib, name) - fn.restype = restype - fn.argtypes = argtypes - return fn - - -class _ContractEntry(ctypes.Structure): - """The C `shapes_contract_entry`: one declaration's id and hash.""" - - _fields_ = [("id", ctypes.c_uint64), ("hash", ctypes.c_uint64)] - - -# The C ABI revision these bindings were generated against and, per -# top-level module's contract symbol, the declarations they rely on: -# (id, hash, dotted path). -_ABI_VERSION = 4 -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { - "shapes_shapes_contract": [ - (0x0d7b3eec49243e7a, 0x5f232311808e1b1f, "shapes.Channel"), - (0x183fa8198100860e, 0xe40c6993cb31d359, "shapes.describe"), - (0x2d1395eb837b848e, 0x42202a0c12f3a16e, "shapes.sum_bytes"), - (0x7f9a5b87ba09b86b, 0xc5b32fb0efbbaa17, "shapes.scale"), - (0xc8edd779240fda17, 0x68e1be78fddb20e4, "shapes.Numerics"), - (0xd3d3881b2fd18a62, 0xf24d0eb76a99d918, "shapes.make_numerics"), - (0xf010844afff5ac3c, 0x50de28ae9b41ff15, "shapes.Shape"), - ], -} - - -def _check_contract() -> None: - """Refuse to load a library built for another ABI revision, or one that - lacks or changed a declaration these bindings use, before any other - symbol is bound.""" - try: - abi_version = _bind("shapes_abi_version", ctypes.c_uint32) - except AttributeError: - raise ImportError( - f"{_lib._name} does not export shapes_abi_version; " - "it is not the library these bindings were generated for" - ) from None - found = abi_version() - if found != _ABI_VERSION: - raise ImportError( - f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " - f"but {_lib._name} reports revision {found}" - ) - for symbol, expected in _CONTRACTS.items(): - try: - contract = _bind( - symbol, ctypes.POINTER(_ContractEntry), ctypes.POINTER(ctypes.c_size_t) - ) - except AttributeError: - raise ImportError(f"{_lib._name} does not export {symbol}") from None - length = ctypes.c_size_t() - table = contract(ctypes.byref(length)) - hashes = {table[i].id: table[i].hash for i in range(length.value)} if table else {} - for entry_id, entry_hash, path in expected: - actual = hashes.get(entry_id) - if actual is None: - raise ImportError(f"{path} is missing from the library ({_lib._name})") - if actual != entry_hash: - raise ImportError( - f"{path} changed since these bindings were generated ({_lib._name})" - ) - - -_check_contract() - -_error_clear = _bind("shapes_error_clear", None, ctypes.POINTER(_ErrorStruct)) -_free_bytes = _bind("shapes_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) -_debug_live_fn = _bind("shapes_debug_live", ctypes.c_uint64, ctypes.c_int32) - - -def _debug_live(kind: int) -> int: - """The producer's live-resource counter `kind` (0 objects, 1 callbacks, - 2 iterators, 3 cancel tokens, 4 byte runs; -1 is 1 when the producer - counts at all). Always 0 unless the producer was built with its leak - counters enabled.""" - live: int = _debug_live_fn(kind) - return live - - -def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: - """Copy a filled out-err slot's code, message, and payload, then release - them.""" - code = err.code - message = err.message.decode("utf-8", "replace") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _error_clear(ctypes.byref(err)) - return code, message, payload - - -def _trap_from(code: int, message: str, payload: bytes = b"") -> InternalError: - """The exception for a failed call that declares no errors.""" - return InternalError(code, message) - - -def _take_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a producer-owned byte run (a string, bytes, or value buffer) and - release it with shapes_free_bytes.""" - if not ptr: - return b"" - try: - return ctypes.string_at(ptr, length) - finally: - _free_bytes(ptr, length) - - -def _take_str(ptr: Optional[int], length: int) -> str: - """Copy and release a producer-owned UTF-8 string.""" - return _take_bytes(ptr, length).decode("utf-8") - - -def _peek_bytes(ptr: Optional[int], length: int) -> bytes: - """Copy a byte run the producer only lends for the current call.""" - return ctypes.string_at(ptr, length) if ptr and length else b"" - - -_H = TypeVar("_H", bound="_Handle") - - -class _Handle: - """Owns one native reference and releases it exactly once. - - Calls lend the pointer through `_acquire`/`_release`, so `close()` from - another thread while a call is in flight defers the release until the - last such call returns instead of freeing the object mid-call. - """ - - # The native destroy (or iterator release) function, set per subclass. - _destroy: Callable[[int], None] - - def _init_handle(self, ptr: int) -> None: - self._ptr: Optional[int] = ptr - self._key = ptr - self._calls = 0 - self._closing = False - self._lock = threading.Lock() - - @classmethod - def _adopt(cls: Type[_H], ptr: int) -> _H: - """Wrap one strong reference the producer handed over.""" - obj = cls.__new__(cls) - obj._init_handle(ptr) - return obj - - def _acquire(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise ValueError(f"{type(self).__name__} is closed") - self._calls += 1 - return ptr - - def _release(self) -> None: - with self._lock: - self._calls -= 1 - if self._calls or not self._closing: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def close(self) -> None: - """Release the native reference. Idempotent; the wrapper is unusable - afterwards.""" - lock = getattr(self, "_lock", None) - if lock is None: - return - with lock: - if self._closing: - return - self._closing = True - if self._calls: - return - ptr, self._ptr = self._ptr, None - if ptr is not None: - type(self)._destroy(ptr) - - def __enter__(self: _H) -> _H: - return self - - def __exit__(self, *exc: object) -> None: - self.close() - - def __del__(self) -> None: - # An unreachable wrapper has no call in flight: release directly. - ptr = getattr(self, "_ptr", None) - if ptr is not None: - self._ptr = None - type(self)._destroy(ptr) - - -class _Object(_Handle): - """Base of every interface wrapper: a reference-counted native object. - Two wrappers are equal when they hold the same native object.""" - - # The native clone function, set per subclass. - _clone: Callable[[int], int] - - def _clone_ref(self) -> int: - """A new strong reference to the same object, for a receiver that - adopts it (an object token, or a callback method's return).""" - ptr = self._acquire() - try: - return type(self)._clone(ptr) - finally: - self._release() - - def __eq__(self, other: object) -> bool: - if not isinstance(other, _Object): - return NotImplemented - return self is other or (self._ptr is not None and self._ptr == other._ptr) - - def __hash__(self) -> int: - return hash(self._key) - - -class _Iterator(_Handle): - """Base of every lazy iterator over a producer stream: one native `next` - call per step; the handle is released on exhaustion, `close()`, or - garbage collection.""" - - def __iter__(self: _H) -> _H: - return self - - def _step(self) -> int: - with self._lock: - ptr = self._ptr - if ptr is None or self._closing: - raise StopIteration - self._calls += 1 - return ptr - - -def _required(ptr: Optional[int]) -> int: - """A non-null object pointer the producer returned.""" - if not ptr: - raise InternalError(-3, "the producer returned a null object") - return ptr - - -def _lend(obj: Any, cls: Type[_Object]) -> int: - """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - return obj._acquire() - - -def _lend_opt(obj: Any, cls: Type[_Object]) -> Optional[int]: - """`_lend` for an optional object: None lends a null pointer.""" - return None if obj is None else _lend(obj, cls) - - -def _release_opt(obj: Optional[_Object]) -> None: - if obj is not None: - obj._release() - - -_I8 = struct.Struct(" InternalError: - return InternalError(-3, f"malformed value buffer: {what}") - - -class _Writer: - """Encodes values in the value-buffer wire format: little-endian, - packed, no alignment.""" - - __slots__ = ("_buf", "_objects") - - def __init__(self) -> None: - self._buf = bytearray() - self._objects: List[Tuple[int, _Object]] = [] - - def finish(self) -> bytes: - # Object tokens are minted last, so an encoding that fails part-way - # never leaks a strong reference. - minted: List[Tuple[_Object, int]] = [] - try: - for pos, obj in self._objects: - ptr = obj._clone_ref() - minted.append((obj, ptr)) - _U64.pack_into(self._buf, pos, ptr) - except BaseException: - for obj, ptr in minted: - type(obj)._destroy(ptr) - raise - self._objects = [] - return bytes(self._buf) - - def write_bool(self, v: bool) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += _I8.pack(v) - - def write_u8(self, v: int) -> None: - self._buf += _U8.pack(v) - - def write_i16(self, v: int) -> None: - self._buf += _I16.pack(v) - - def write_u16(self, v: int) -> None: - self._buf += _U16.pack(v) - - def write_i32(self, v: int) -> None: - self._buf += _I32.pack(v) - - def write_u32(self, v: int) -> None: - self._buf += _U32.pack(v) - - def write_i64(self, v: int) -> None: - self._buf += _I64.pack(v) - - def write_u64(self, v: int) -> None: - self._buf += _U64.pack(v) - - def write_f32(self, v: float) -> None: - self._buf += _F32.pack(v) - - def write_f64(self, v: float) -> None: - self._buf += _F64.pack(v) - - def write_count(self, n: int) -> None: - self._buf += _U32.pack(n) - - def write_flag(self, present: bool) -> None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - data = v.encode("utf-8") - self._buf += _U32.pack(len(data)) - self._buf += data - - def write_bytes(self, v: bytes) -> None: - self._buf += _U32.pack(len(v)) - self._buf += v - - def write_numbers(self, code: str, values: List[Any]) -> None: - """A list of one fixed-width numeric primitive (`code` is its struct - format character), packed in one call.""" - self._buf += _U32.pack(len(values)) - self._buf += struct.pack(f"<{len(values)}{code}", *values) - - def write_object(self, obj: Any, cls: Type[_Object]) -> None: - """Reserve an object token: one strong reference to `obj` the reader - adopts, cloned in `finish()`. A closed wrapper is rejected now.""" - if not isinstance(obj, cls): - raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") - obj._acquire() - obj._release() - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -class _Reader: - """Decodes values from the value-buffer wire format, rejecting truncated - buffers, invalid flag bytes, invalid UTF-8, undeclared enum values, and - trailing data.""" - - __slots__ = ("_data", "_pos") - - def __init__(self, data: bytes) -> None: - self._data = data - self._pos = 0 - - def _take(self, n: int, what: str) -> int: - pos = self._pos - if len(self._data) - pos < n: - raise _malformed(f"truncated {what}") - self._pos = pos + n - return pos - - def read_bool(self) -> bool: - b = self._data[self._take(1, "bool")] - if b > 1: - raise _malformed("invalid bool byte") - return b == 1 - - def read_i8(self) -> int: - return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) - - def read_u8(self) -> int: - return self._data[self._take(1, "u8")] - - def read_i16(self) -> int: - return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) - - def read_u16(self) -> int: - return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) - - def read_i32(self) -> int: - return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) - - def read_u32(self) -> int: - return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) - - def read_i64(self) -> int: - return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) - - def read_u64(self) -> int: - return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) - - def read_f32(self) -> float: - return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) - - def read_f64(self) -> float: - return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) - - def read_count(self) -> int: - # An element count, not a byte length: elements can encode to zero - # bytes, so it is not bounded by the remaining data. - return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) - - def read_flag(self) -> bool: - b = self._data[self._take(1, "option flag")] - if b > 1: - raise _malformed("invalid option flag") - return b == 1 - - def read_bytes(self) -> bytes: - n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) - pos = self._take(n, "bytes") - return self._data[pos:pos + n] - - def read_string(self) -> str: - try: - return self.read_bytes().decode("utf-8") - except UnicodeDecodeError: - raise _malformed("string is not valid UTF-8") from None - - def read_enum(self, cls: Type[_E]) -> _E: - value = self.read_i32() - try: - return cls(value) - except ValueError: - raise _malformed(f"undeclared {cls.__name__} value {value}") from None - - def read_numbers(self, code: str, size: int) -> List[Any]: - """A list of one fixed-width numeric primitive, unpacked in one call.""" - n = self.read_count() - pos = self._take(n * size, "list") - return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) - - def read_object(self) -> int: - """An object token: one strong reference the caller adopts.""" - ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) - if ptr == 0: - raise _malformed("null object token") - return ptr - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise _malformed("trailing bytes") - - -def _decode(data: bytes, read: Callable[[_Reader], _T]) -> _T: - """Decode exactly one value from `data`.""" - r = _Reader(data) - value = read(r) - r.expect_end() - return value - - -# === Module: shapes === - - -class Shape: - """An algebraic shape (sum type with associated data)""" - - class Tag(IntEnum): - # The empty shape (a unit variant) - Empty = 0 - # A circle with a radius - Circle = 1 - # An axis-aligned rectangle - Rectangle = 2 - # An integer-bounded shape (exercises narrow + wide ints) - Bounded = 3 - # A labeled shape with a small count - Labeled = 4 - - Empty: Type[ShapeEmpty] - Circle: Type[ShapeCircle] - Rectangle: Type[ShapeRectangle] - Bounded: Type[ShapeBounded] - Labeled: Type[ShapeLabeled] - TAG: Shape.Tag - - @property - def tag(self) -> Shape.Tag: - """The discriminant of this value's active variant.""" - return type(self).TAG - - -@dataclass -class ShapeEmpty(Shape): - """The empty shape (a unit variant)""" - - TAG = Shape.Tag.Empty - - -@dataclass -class ShapeCircle(Shape): - """A circle with a radius""" - - TAG = Shape.Tag.Circle - - # Radius in points - radius: float - - -@dataclass -class ShapeRectangle(Shape): - """An axis-aligned rectangle""" - - TAG = Shape.Tag.Rectangle - - width: float - height: float - - -@dataclass -class ShapeBounded(Shape): - """An integer-bounded shape (exercises narrow + wide ints)""" - - TAG = Shape.Tag.Bounded - - min: int - max: int - weight: int - - -@dataclass -class ShapeLabeled(Shape): - """A labeled shape with a small count""" - - TAG = Shape.Tag.Labeled - - label: str - count: int - - -Shape.Empty = ShapeEmpty -Shape.Circle = ShapeCircle -Shape.Rectangle = ShapeRectangle -Shape.Bounded = ShapeBounded -Shape.Labeled = ShapeLabeled - - -def _write_Shape(_w: _Writer, value: Shape) -> None: - if isinstance(value, ShapeEmpty): - _w.write_i32(0) - return - if isinstance(value, ShapeCircle): - _w.write_i32(1) - _w.write_f64(value.radius) - return - if isinstance(value, ShapeRectangle): - _w.write_i32(2) - _w.write_f32(value.width) - _w.write_f32(value.height) - return - if isinstance(value, ShapeBounded): - _w.write_i32(3) - _w.write_i16(value.min) - _w.write_i16(value.max) - _w.write_u64(value.weight) - return - if isinstance(value, ShapeLabeled): - _w.write_i32(4) - _w.write_string(value.label) - _w.write_u8(value.count) - return - raise TypeError(f"expected a Shape variant, got {type(value).__name__}") - - -def _read_Shape(_r: _Reader) -> Shape: - tag = _r.read_i32() - if tag == 0: - return ShapeEmpty() - if tag == 1: - return ShapeCircle( - radius=_r.read_f64(), - ) - if tag == 2: - return ShapeRectangle( - width=_r.read_f32(), - height=_r.read_f32(), - ) - if tag == 3: - return ShapeBounded( - min=_r.read_i16(), - max=_r.read_i16(), - weight=_r.read_u64(), - ) - if tag == 4: - return ShapeLabeled( - label=_r.read_string(), - count=_r.read_u8(), - ) - raise _malformed(f"unknown Shape tag {tag}") - - -class Channel(IntEnum): - """A plain C-style enum (no payloads)""" - - Red = 0 - Green = 1 - Blue = 2 - - -@dataclass -class Numerics: - """One field per supported numeric primitive""" - - a: int - b: int - c: int - d: int - e: int - f: int - g: int - h: int - i: float - j: float - - -def _write_Numerics(_w: _Writer, value: Numerics) -> None: - _w.write_i8(value.a) - _w.write_i16(value.b) - _w.write_i32(value.c) - _w.write_i64(value.d) - _w.write_u8(value.e) - _w.write_u16(value.f) - _w.write_u32(value.g) - _w.write_u64(value.h) - _w.write_f32(value.i) - _w.write_f64(value.j) - - -def _read_Numerics(_r: _Reader) -> Numerics: - return Numerics( - a=_r.read_i8(), - b=_r.read_i16(), - c=_r.read_i32(), - d=_r.read_i64(), - e=_r.read_u8(), - f=_r.read_u16(), - g=_r.read_u32(), - h=_r.read_u64(), - i=_r.read_f32(), - j=_r.read_f64(), - ) - - -_c_shapes_describe = _bind("shapes_shapes_describe", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def describe(shape: Shape) -> str: - """Render a shape to a string""" - _shape_w = _Writer() - _write_Shape(_shape_w, shape) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _shape_b = _shape_w.finish() - _ret = _c_shapes_describe(_shape_b, len(_shape_b), ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _take_str(_ret, _out_len.value) - - -_c_shapes_scale = _bind("shapes_shapes_scale", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_double, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def scale(shape: Shape, factor: float) -> Shape: - """Scale a shape by a factor, returning a new shape""" - _shape_w = _Writer() - _write_Shape(_shape_w, shape) - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _shape_b = _shape_w.finish() - _ret = _c_shapes_scale(_shape_b, len(_shape_b), factor, ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Shape) - - -_c_shapes_sum_bytes = _bind("shapes_shapes_sum_bytes", ctypes.c_uint64, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) - - -def sum_bytes(values: bytes) -> int: - """Sum a list of bytes into a wide integer (numerics smoke)""" - _values_b = bytes(values) - _err = _ErrorStruct() - _ret: int = _c_shapes_sum_bytes(_values_b, len(_values_b), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _ret - - -_c_shapes_make_numerics = _bind("shapes_shapes_make_numerics", ctypes.c_void_p, ctypes.c_int8, ctypes.c_int16, ctypes.c_int32, ctypes.c_int64, ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint32, ctypes.c_uint64, ctypes.c_float, ctypes.c_double, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) - - -def make_numerics(a: int, b: int, c: int, d: int, e: int, f: int, g: int, h: int, i: float, j: float) -> Numerics: - """Build a Numerics struct from every numeric primitive""" - _err = _ErrorStruct() - _out_len = ctypes.c_size_t() - _ret = _c_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, ctypes.byref(_out_len), ctypes.byref(_err)) - if _err.code: - raise _trap_from(*_read_error(_err)) - return _decode(_take_bytes(_ret, _out_len.value), _read_Numerics) - - -__all__ = [ - "Error", - "InternalError", - "Shape", - "ShapeEmpty", - "ShapeCircle", - "ShapeRectangle", - "ShapeBounded", - "ShapeLabeled", - "Channel", - "Numerics", - "describe", - "scale", - "sum_bytes", - "make_numerics", -] - -# END shapes.py diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_rb.snap deleted file mode 100644 index 0aebff09..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_rb.snap +++ /dev/null @@ -1,387 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -# frozen_string_literal: true - -require_relative 'docs_everywhere/runtime' - -# Ruby bindings for docs_everywhere. -module DocsEverywhere - # The declarations these bindings were generated with, per top-level - # module, as [id, hash, path]. Loading fails, naming the path, unless - # the library's contract table holds each one with an equal hash. - _wv_check_contract!( - docs_everywhere_docs_contract: [ - [0x00b2187fd75634ec, 0x2263c6f787bf6081, 'docs.publish_async'], - [0x12fc242b9dc6444c, 0x96405f484a642b31, 'docs.Notebook.insert'], - [0x2213822ed05cd5bd, 0x26c5833392aa63ce, 'docs.Notebook.new'], - [0x2bd762bcdaa207ff, 0x1b0ebab2d04c49b0, 'docs.SavedListener'], - [0x2e086c45e9d617de, 0x4d43bf32358c01ff, 'docs.Notebook.find'], - [0x4817dc98197ad960, 0xf8017b5a7d58b2c7, 'docs.Notebook.size'], - [0x614de47452d5a55c, 0xd9aae1472494ffef, 'docs.get_document'], - [0x67c5cb9fc119bcd5, 0x08d5b25a1a22a7fa, 'docs.Document'], - [0x8d484bf2d78c9b70, 0xe9093cf96912194c, 'docs.Notebook.max_documents'], - [0xaea61d6451691c67, 0xe4704128b3524b3d, 'docs.watch'], - [0xd68d6ad1540cdec5, 0x01afeb40b0a8bfe1, 'docs.Notebook'], - [0xdb0601444a4760cc, 0xe56c69629b444797, 'docs.create_document'], - [0xe12d6f4838347b3e, 0xa9bab86646d84f51, 'docs.Status'], - [0xee2f8d25b3313b50, 0x9691232f35439a2b, 'docs.DocsErrors'], - ], - ) - - # === Module: docs === - - # Base error for the `docs` module's error domain. - class DocsErrorsError < Error - # Returned when no document matches the requested id - class NotFound < DocsErrorsError - CODE = 1 - - def initialize(message = nil) - super(CODE, message || 'Document not found') - end - end - - # Raised when attempting to move into a forbidden state - class InvalidStatus < DocsErrorsError - CODE = 2 - - def initialize(message = nil) - super(CODE, message || 'Invalid status transition') - end - end - end - - # @api private - # The DocsErrorsError for a domain `code`, with its payload fields decoded; - # the root Error (or Cancelled) for any other code. - def self._wv_docs_errors_error_from(code, message, payload = nil) - message = nil if message.empty? - r = WvBufferReader.new(payload) - error = - case code - when 1 - DocsErrorsError::NotFound.new(message) - when 2 - DocsErrorsError::InvalidStatus.new(message) - else - return _wv_error(code, message.to_s) - end - r.expect_end! - error - end - - # @api private - # Raises the DocsErrorsError for a non-zero error slot. - def self._wv_check_docs_errors_error!(err) - taken = _wv_take_error(err) - raise _wv_docs_errors_error_from(*taken) unless taken.nil? - end - - # Lifecycle status of a document. - # - # Used to drive UI badges and filters. - module Status - # Document is being edited - DRAFT = 0 - # Document is publicly visible - PUBLISHED = 1 - # Document is hidden from listings - ARCHIVED = 2 - end - - # Represents a single document tracked by the system. - # - # Documents are persisted to disk and exposed via the public API. - class Document - # Stable opaque identifier - attr_reader :id - - # Human-readable title shown in lists - attr_reader :title - - # Current lifecycle status - attr_reader :status - - # Free-form tags for grouping - attr_reader :tags - - def initialize(id:, title:, status:, tags:) - @id = id - @title = title - @status = status - @tags = tags - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Document) && - id == other.id && - title == other.title && - status == other.status && - tags == other.tags - end - end - - # @api private - # Packs a Document into the value-buffer wire format. - def self._wv_write_document(w, v) - w.write_i64(v.id) - w.write_string(v.title) - w.write_i32(v.status) - _wv_write_list_string(w, v.tags) - end - - # @api private - # Unpacks a Document from the value-buffer wire format. - def self._wv_read_document(r) - Document.new( - id: r.read_i64, - title: r.read_string, - status: r.read_i32, - tags: _wv_read_list_string(r), - ) - end - - attach_function :docs_everywhere_docs_Notebook_clone, [:pointer], :pointer - attach_function :docs_everywhere_docs_Notebook_destroy, [:pointer], :void - attach_function :docs_everywhere_docs_Notebook_new, [:pointer], :pointer, blocking: true - attach_function :docs_everywhere_docs_Notebook_insert, [:pointer, :pointer, :size_t, :pointer], :int32, blocking: true - attach_function :docs_everywhere_docs_Notebook_find, [:pointer, :int64, :pointer, :pointer], :pointer, blocking: true - attach_function :docs_everywhere_docs_Notebook_size, [:pointer, :pointer], :int32, blocking: true - attach_function :docs_everywhere_docs_Notebook_max_documents, [:pointer], :int32, blocking: true - attach_function :docs_everywhere_docs_create_document, [:pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :docs_everywhere_docs_get_document, [:int64, :pointer, :pointer], :pointer, blocking: true - attach_function :docs_everywhere_docs_publish_async, [:int64, :pointer, :pointer], :void, blocking: true - attach_function :docs_everywhere_docs_watch, [:pointer, :pointer, :pointer], :void, blocking: true - - # @api private - # Completion trampoline for publish_async. - DOCS_EVERYWHERE_DOCS_PUBLISH_ASYNC_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :bool]) do |ctx, err, result| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = result - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # Consumer-implemented observer of document-saved notifications. - # - # Implementations must be safe to call from any thread. - # - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a SavedListener is expected; include - # this module to inherit NotImplementedError defaults. The library - # may call the methods from any thread until it releases the - # implementation. - module SavedListener - # Fires after a document has been persisted successfully - # @param doc_id [Integer] Identifier of the saved document - # @param revision [Integer] Monotonically increasing revision - def on_saved(doc_id, revision) - raise NotImplementedError, "#{self.class}#on_saved is not implemented" - end - end - - # @api private - # The C vtable layout the library calls a SavedListener through. - class WvSavedListenerVtable < FFI::Struct - layout :wv_size, :uint32, - :wv_flags, :uint32, - :wv_free, :pointer, - :on_saved, :pointer - end - - # @api private - # Releases a SavedListener implementation when the library drops its last - # reference. May run on any library thread. - WV_SAVED_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # @api private - # Trampoline for SavedListener#on_saved. - WV_SAVED_LISTENER_ON_SAVED = FFI::Function.new(:void, [:pointer, :int64, :int32, :pointer]) do |wv_ctx, doc_id, revision, wv_err| - _wv_cb_lookup(wv_ctx).on_saved(doc_id, revision) - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # The one process-wide SavedListener vtable every registration hands the - # library; its entries live for the process lifetime. - WV_SAVED_LISTENER_VTABLE = WvSavedListenerVtable.new - WV_SAVED_LISTENER_VTABLE[:wv_size] = WvSavedListenerVtable.size - WV_SAVED_LISTENER_VTABLE[:wv_flags] = 0 - WV_SAVED_LISTENER_VTABLE[:wv_free] = WV_SAVED_LISTENER_FREE - WV_SAVED_LISTENER_VTABLE[:on_saved] = WV_SAVED_LISTENER_ON_SAVED - - # @api private - # Owns one strong reference to a Notebook; releases it exactly once. - class NotebookPtr < FFI::AutoPointer - def self.release(ptr) - DocsEverywhere.docs_everywhere_docs_Notebook_destroy(ptr) - end - end - - # An ordered collection of documents. - # - # Notebooks own their documents and release them when destroyed. - class Notebook < WvObject - WV_PTR = NotebookPtr - - # @api private - def self._wv_clone(ptr) - DocsEverywhere.docs_everywhere_docs_Notebook_clone(ptr) - end - - # Create an empty notebook - def initialize - err = ErrorStruct.new - result = DocsEverywhere.docs_everywhere_docs_Notebook_new(err) - DocsEverywhere._wv_check!(err) - _wv_init(DocsEverywhere._wv_nonnull(result)) - end - - # Add a document, returning its position in the notebook - # @param document [Document] The document to add - # @return [Integer] - def insert(document) - document_s = DocsEverywhere._wv_encode { |w| DocsEverywhere._wv_write_document(w, document) } - DocsEverywhere._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - result = DocsEverywhere.docs_everywhere_docs_Notebook_insert(_wv_self, document_s, document_s.bytesize, err) - DocsEverywhere._wv_check!(err) - result - end - end - - # Look up a document by identifier, failing when none matches - # @param id [Integer] Identifier of the document to find - # @return [Document] - def find(id) - DocsEverywhere._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = DocsEverywhere.docs_everywhere_docs_Notebook_find(_wv_self, id, out_len, err) - DocsEverywhere._wv_check_docs_errors_error!(err) - DocsEverywhere._wv_decode(result, out_len.read(:size_t)) { |r| DocsEverywhere._wv_read_document(r) } - end - end - - # The number of documents currently held - # @return [Integer] - def size - DocsEverywhere._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - result = DocsEverywhere.docs_everywhere_docs_Notebook_size(_wv_self, err) - DocsEverywhere._wv_check!(err) - result - end - end - - # The largest number of documents one notebook will hold - # @return [Integer] - def self.max_documents - err = ErrorStruct.new - result = DocsEverywhere.docs_everywhere_docs_Notebook_max_documents(err) - DocsEverywhere._wv_check!(err) - result - end - end - - # Create a brand new document. - # - # The document starts in `Draft` status; callers must publish - # it explicitly to make it visible. - # @param title [String] Human-readable title for the new document - # @param tags [Array] Initial set of tags (may be empty) - # @return [Document] - def self.create_document(title, tags) - title_s = _wv_str(title) - tags_s = _wv_encode { |w| _wv_write_list_string(w, tags) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = docs_everywhere_docs_create_document(title_s, title_s.bytesize, tags_s, tags_s.bytesize, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_document(r) } - end - - # Look up a document by identifier - # @param id [Integer] Identifier returned from create_document - # @return [Document, nil] - def self.get_document(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = docs_everywhere_docs_get_document(id, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_opt_document(r) } - end - - # Publish a document in the background - # Blocks until the call completes on a producer thread. - # @param id [Integer] Identifier of the document to publish - # @return [Boolean] - def self.publish_async(id) - ctx, queue = _wv_async_begin - docs_everywhere_docs_publish_async(id, DOCS_EVERYWHERE_DOCS_PUBLISH_ASYNC_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Register a listener for saved notifications - # @param listener [SavedListener] Receiver of saved notifications - def self.watch(listener) - _wv_present!(listener, 'listener') - err = ErrorStruct.new - listener_ctx, listener_vtable = _wv_cb_register(listener, WV_SAVED_LISTENER_VTABLE) - docs_everywhere_docs_watch(listener_ctx, listener_vtable, err) - _wv_check!(err) - nil - end - - # === Value-buffer codecs of the composite types === - - # @api private - # Packs `[string]` into the value-buffer wire format. - def self._wv_write_list_string(w, v) - w.write_len(v.length) - v.each { |e| w.write_string(e) } - end - - # @api private - # Unpacks `[string]` from the value-buffer wire format. - def self._wv_read_list_string(r) - Array.new(r.read_len) { r.read_string } - end - - # @api private - # Packs `Document?` into the value-buffer wire format. - def self._wv_write_opt_document(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - _wv_write_document(w, v) - end - end - - # @api private - # Unpacks `Document?` from the value-buffer wire format. - def self._wv_read_opt_document(r) - r.read_flag ? _wv_read_document(r) : nil - end -end - -# END docs_everywhere.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_rb.snap deleted file mode 100644 index 150ad23d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_rb.snap +++ /dev/null @@ -1,1990 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -# frozen_string_literal: true - -require_relative 'edge_cases/runtime' - -# Ruby bindings for edge_cases. -module EdgeCases - # The declarations these bindings were generated with, per top-level - # module, as [id, hash, path]. Loading fails, naming the path, unless - # the library's contract table holds each one with an equal hash. - _wv_check_contract!( - edge_cases_edge_cases_contract: [ - [0x056a86511785ba2b, 0x5cba3f16d57c6cdb, 'edge_cases.make_widget'], - [0x07bfa9ecb2b720d2, 0x3813b3f57c64bf35, 'edge_cases.EdgeErrors'], - [0x0aaa11867e37f991, 0xf090dd52146ebdf0, 'edge_cases.Widget.refresh'], - [0x0f9701493898ae4c, 0x6944f47a275c96e8, 'edge_cases.Inner'], - [0x1226b1adac179f7d, 0x0e95a00ab0bef637, 'edge_cases.async_keywords'], - [0x16d36bef1541136c, 0x5ea7021ec324298f, 'edge_cases.find_widget'], - [0x1b7324c55e980feb, 0xabf87fe372358f42, 'edge_cases.Widget.old_name'], - [0x1cbd731467c2bab8, 0x4915b9989c2c9074, 'edge_cases.Vault'], - [0x2c2a6de67d894b0a, 0x50a4496b1e7191ca, 'edge_cases.Vault.open'], - [0x322b4cfcd2d6861c, 0xca7527a379ae8465, 'edge_cases.Keywords'], - [0x3f8e23a5f32906f4, 0x8d4c908d796f130d, 'edge_cases.Leaf'], - [0x42ab8a685457b97c, 0xe5ce30500cadb32d, 'edge_cases.Vault.handle'], - [0x4a68540e2836d3d0, 0xa20e44c71e420ed2, 'edge_cases.mode_stream'], - [0x4e030580ac0b111d, 0xb68b7a18d910e175, 'edge_cases.legacy_value'], - [0x4f5e4e5291864ef3, 0x55a1a88b21a08f8b, 'edge_cases.Legacy'], - [0x4f84b06171b4213d, 0xbaedcfdebb6a2820, 'edge_cases.scalar_stream'], - [0x5067a5ff2d9ef910, 0xbfc79b18fc4714fc, 'edge_cases.Widget'], - [0x59aff159112fbf5b, 0x18e4def57a8cfa49, 'edge_cases.optional_params'], - [0x5daf65b98fa77c48, 0xc817a8cb202e59c2, 'edge_cases.keywords_roundtrip'], - [0x5de8a5c9726c2c5a, 0x7c50b5b65f7a896d, 'edge_cases.register_batch'], - [0x618a620309994c3c, 0x826ad0b1d208d723, 'edge_cases.async_bytes'], - [0x648285cdba0a7b8e, 0xcf6e80e783a4d8be, 'edge_cases.WidgetHolder'], - [0x649c78079824b10d, 0x5b12dd26e0634b69, 'edge_cases.async_widget'], - [0x7da7a7357d122a37, 0xf544e025ffaa3d20, 'edge_cases.async_void'], - [0x7e4015bc3a472e2d, 0xfe22291d90b82e4a, 'edge_cases.widget_stream'], - [0x805276e5d81fdba7, 0xd9b64e360bd03b2b, 'edge_cases.Outer'], - [0x80e1183326542df4, 0xe8a57e12cc6ed276, 'edge_cases.select'], - [0x923ce5e9bcd769a1, 0x0b973f5dc07dda8b, 'edge_cases.Vault.to_string'], - [0x98ca8c01f5239404, 0xfe11c93eb6b303ef, 'edge_cases.Widget.rename'], - [0xa13e22e98ea278d4, 0xf1fae215d7387800, 'edge_cases.widgets_everywhere'], - [0xa84a8c9bf2a881e1, 0xbe1dc840bf221996, 'edge_cases.async_optional'], - [0xad958c478fbf205d, 0xe386b0716d24f19b, 'edge_cases.Retired'], - [0xb2fc3964ef56ab39, 0x73c325c8bc351236, 'edge_cases.async_list'], - [0xb3403af521cf0006, 0x6abc30d48206a3ba, 'edge_cases.Widget.close'], - [0xb794304a22a406b8, 0xf6fb98711c516105, 'edge_cases.async_struct'], - [0xba3936106a49c47d, 0xb7025f2fadd6288e, 'edge_cases.Vault.peek'], - [0xc155b0c3cfb164f4, 0xc777ae57d92ebc47, 'edge_cases.noop'], - [0xcdc18607777019dd, 0xb81a31e5c20aa7a8, 'edge_cases.inspect_widget'], - [0xd164a82aeb64b8cc, 0xded02c68005c4737, 'edge_cases.nested_optional'], - [0xd978ccad2468038f, 0x30c39c65d6235153, 'edge_cases.Mode'], - [0xdbe9d947431c9124, 0x39537fb848fdf980, 'edge_cases.BatchListener'], - [0xeae8b7862c08bb07, 0xfe12eeaa830452ea, 'edge_cases.Keyword'], - [0xf1e155a05ae262b8, 0xb575eb818a423d42, 'edge_cases.async_enum'], - [0xf22e210bdbce7343, 0x2a5e0f801bc8c768, 'edge_cases.Widget.dispose'], - [0xf3c7cf675563ee6c, 0x0ecffeef399facc8, 'edge_cases.Vault.probe'], - [0xfd34bc67acac379e, 0x576caa14f3c0901a, 'edge_cases.Widget.fetch'], - [0xff2e9d2bff941786, 0x7e96764ef1f5fbaa, 'edge_cases.string_stream'], - ], - ) - - # === Module: edge_cases === - - # Base error for the `edge_cases` module's error domain. - class EdgeErrorsError < Error - # Resource is busy - class Busy < EdgeErrorsError - CODE = 7 - - def initialize(message = nil) - super(CODE, message || 'Resource is busy') - end - end - end - - # @api private - # The EdgeErrorsError for a domain `code`, with its payload fields decoded; - # the root Error (or Cancelled) for any other code. - def self._wv_edge_errors_error_from(code, message, payload = nil) - message = nil if message.empty? - r = WvBufferReader.new(payload) - error = - case code - when 7 - EdgeErrorsError::Busy.new(message) - else - return _wv_error(code, message.to_s) - end - r.expect_end! - error - end - - # @api private - # Raises the EdgeErrorsError for a non-zero error slot. - def self._wv_check_edge_errors_error!(err) - taken = _wv_take_error(err) - raise _wv_edge_errors_error_from(*taken) unless taken.nil? - end - - # Deprecated enum with non-contiguous values and keyword-like variants - # @deprecated Use Mode instead - module Legacy - NONE = 0 - DEFAULT = 5 - CLASS = 10 - end - - module Mode - FAST = 1 - SAFE = 2 - end - - class Leaf - attr_reader :weight - - def initialize(weight:) - @weight = weight - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Leaf) && - weight == other.weight - end - end - - # Middle layer of a three-deep struct nesting - class Inner - attr_reader :leaf - - attr_reader :leaves - - def initialize(leaf:, leaves:) - @leaf = leaf - @leaves = leaves - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Inner) && - leaf == other.leaf && - leaves == other.leaves - end - end - - class Outer - attr_reader :inner - - attr_reader :maybe_inner - - attr_reader :by_name - - attr_reader :modes - - attr_reader :matrix - - attr_reader :sparse - - attr_reader :blob - - attr_reader :note - - attr_reader :by_id - - attr_reader :_hidden - - attr_reader :v2 - - def initialize( - inner:, - maybe_inner:, - by_name:, - modes:, - matrix:, - sparse:, - blob:, - note:, - by_id:, - _hidden:, - v2: - ) - @inner = inner - @maybe_inner = maybe_inner - @by_name = by_name - @modes = modes - @matrix = matrix - @sparse = sparse - @blob = blob - @note = note - @by_id = by_id - @_hidden = _hidden - @v2 = v2 - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Outer) && - inner == other.inner && - maybe_inner == other.maybe_inner && - by_name == other.by_name && - modes == other.modes && - matrix == other.matrix && - sparse == other.sparse && - blob == other.blob && - note == other.note && - by_id == other.by_id && - _hidden == other._hidden && - v2 == other.v2 - end - end - - # Every field is a reserved word somewhere - class Keywords - attr_reader :class_ - - attr_reader :def_ - - attr_reader :import - - attr_reader :from - - attr_reader :func - - attr_reader :range - - attr_reader :register - - attr_reader :volatile - - attr_reader :new - - attr_reader :default - - attr_reader :end_ - - attr_reader :in_ - - attr_reader :is - - attr_reader :int - - attr_reader :var - - attr_reader :val - - attr_reader :self_ - - attr_reader :this - - attr_reader :object - - attr_reader :string - - attr_reader :case_ - - attr_reader :select - - attr_reader :switch - - attr_reader :void - - attr_reader :union - - attr_reader :namespace - - attr_reader :operator - - attr_reader :template - - attr_reader :package - - attr_reader :protocol - - attr_reader :guard - - attr_reader :defer - - attr_reader :inout - - attr_reader :internal - - attr_reader :when_ - - attr_reader :fun - - attr_reader :data - - attr_reader :late - - attr_reader :part - - attr_reader :out - - attr_reader :ref - - attr_reader :event - - attr_reader :base - - attr_reader :lambda - - attr_reader :yield_ - - attr_reader :nil_ - - attr_reader :null - - attr_reader :begin_ - - attr_reader :rescue_ - - attr_reader :module_ - - attr_reader :chan - - attr_reader :go - - attr_reader :let - - attr_reader :export - - attr_reader :delete - - attr_reader :typedef - - attr_reader :sizeof - - attr_reader :static - - attr_reader :super_ - - def initialize( - class_:, - def_:, - import:, - from:, - func:, - range:, - register:, - volatile:, - new:, - default:, - end_:, - in_:, - is:, - int:, - var:, - val:, - self_:, - this:, - object:, - string:, - case_:, - select:, - switch:, - void:, - union:, - namespace:, - operator:, - template:, - package:, - protocol:, - guard:, - defer:, - inout:, - internal:, - when_:, - fun:, - data:, - late:, - part:, - out:, - ref:, - event:, - base:, - lambda:, - yield_:, - nil_:, - null:, - begin_:, - rescue_:, - module_:, - chan:, - go:, - let:, - export:, - delete:, - typedef:, - sizeof:, - static:, - super_: - ) - @class_ = class_ - @def_ = def_ - @import = import - @from = from - @func = func - @range = range - @register = register - @volatile = volatile - @new = new - @default = default - @end_ = end_ - @in_ = in_ - @is = is - @int = int - @var = var - @val = val - @self_ = self_ - @this = this - @object = object - @string = string - @case_ = case_ - @select = select - @switch = switch - @void = void - @union = union - @namespace = namespace - @operator = operator - @template = template - @package = package - @protocol = protocol - @guard = guard - @defer = defer - @inout = inout - @internal = internal - @when_ = when_ - @fun = fun - @data = data - @late = late - @part = part - @out = out - @ref = ref - @event = event - @base = base - @lambda = lambda - @yield_ = yield_ - @nil_ = nil_ - @null = null - @begin_ = begin_ - @rescue_ = rescue_ - @module_ = module_ - @chan = chan - @go = go - @let = let - @export = export - @delete = delete - @typedef = typedef - @sizeof = sizeof - @static = static - @super_ = super_ - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Keywords) && - class_ == other.class_ && - def_ == other.def_ && - import == other.import && - from == other.from && - func == other.func && - range == other.range && - register == other.register && - volatile == other.volatile && - new == other.new && - default == other.default && - end_ == other.end_ && - in_ == other.in_ && - is == other.is && - int == other.int && - var == other.var && - val == other.val && - self_ == other.self_ && - this == other.this && - object == other.object && - string == other.string && - case_ == other.case_ && - select == other.select && - switch == other.switch && - void == other.void && - union == other.union && - namespace == other.namespace && - operator == other.operator && - template == other.template && - package == other.package && - protocol == other.protocol && - guard == other.guard && - defer == other.defer && - inout == other.inout && - internal == other.internal && - when_ == other.when_ && - fun == other.fun && - data == other.data && - late == other.late && - part == other.part && - out == other.out && - ref == other.ref && - event == other.event && - base == other.base && - lambda == other.lambda && - yield_ == other.yield_ && - nil_ == other.nil_ && - null == other.null && - begin_ == other.begin_ && - rescue_ == other.rescue_ && - module_ == other.module_ && - chan == other.chan && - go == other.go && - let == other.let && - export == other.export && - delete == other.delete && - typedef == other.typedef && - sizeof == other.sizeof && - static == other.static && - super_ == other.super_ - end - end - - # Deprecated struct - # @deprecated Use Outer instead - class Retired - attr_reader :id - - def initialize(id:) - @id = id - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Retired) && - id == other.id - end - end - - # Record carrying objects in every buffered position - class WidgetHolder - attr_reader :primary - - attr_reader :fallback - - attr_reader :extras - - attr_reader :by_name - - def initialize(primary:, fallback:, extras:, by_name:) - @primary = primary - @fallback = fallback - @extras = extras - @by_name = by_name - end - - # Structural equality over every field. - def ==(other) - other.is_a?(WidgetHolder) && - primary == other.primary && - fallback == other.fallback && - extras == other.extras && - by_name == other.by_name - end - end - - # @api private - # Packs a Leaf into the value-buffer wire format. - def self._wv_write_leaf(w, v) - w.write_f32(v.weight) - end - - # @api private - # Unpacks a Leaf from the value-buffer wire format. - def self._wv_read_leaf(r) - Leaf.new(weight: r.read_f32) - end - - # @api private - # Packs an Inner into the value-buffer wire format. - def self._wv_write_inner(w, v) - _wv_write_leaf(w, v.leaf) - _wv_write_list_leaf(w, v.leaves) - end - - # @api private - # Unpacks an Inner from the value-buffer wire format. - def self._wv_read_inner(r) - Inner.new(leaf: _wv_read_leaf(r), leaves: _wv_read_list_leaf(r)) - end - - # @api private - # Packs an Outer into the value-buffer wire format. - def self._wv_write_outer(w, v) - _wv_write_inner(w, v.inner) - _wv_write_opt_inner(w, v.maybe_inner) - _wv_write_map_string_inner(w, v.by_name) - _wv_write_list_mode(w, v.modes) - _wv_write_list_list_i32(w, v.matrix) - _wv_write_list_opt_i64(w, v.sparse) - _wv_write_opt_bytes(w, v.blob) - _wv_write_opt_string(w, v.note) - _wv_write_map_i64_string(w, v.by_id) - w.write_bool(v._hidden) - w.write_u8(v.v2) - end - - # @api private - # Unpacks an Outer from the value-buffer wire format. - def self._wv_read_outer(r) - Outer.new( - inner: _wv_read_inner(r), - maybe_inner: _wv_read_opt_inner(r), - by_name: _wv_read_map_string_inner(r), - modes: _wv_read_list_mode(r), - matrix: _wv_read_list_list_i32(r), - sparse: _wv_read_list_opt_i64(r), - blob: _wv_read_opt_bytes(r), - note: _wv_read_opt_string(r), - by_id: _wv_read_map_i64_string(r), - _hidden: r.read_bool, - v2: r.read_u8, - ) - end - - # @api private - # Packs a Keywords into the value-buffer wire format. - def self._wv_write_keywords(w, v) - w.write_i32(v.class_) - w.write_i32(v.def_) - w.write_i32(v.import) - w.write_i32(v.from) - w.write_i32(v.func) - w.write_i32(v.range) - w.write_i32(v.register) - w.write_i32(v.volatile) - w.write_i32(v.new) - w.write_i32(v.default) - w.write_i32(v.end_) - w.write_i32(v.in_) - w.write_i32(v.is) - w.write_i32(v.int) - w.write_i32(v.var) - w.write_i32(v.val) - w.write_i32(v.self_) - w.write_i32(v.this) - w.write_i32(v.object) - w.write_i32(v.string) - w.write_i32(v.case_) - w.write_i32(v.select) - w.write_i32(v.switch) - w.write_i32(v.void) - w.write_i32(v.union) - w.write_i32(v.namespace) - w.write_i32(v.operator) - w.write_i32(v.template) - w.write_i32(v.package) - w.write_i32(v.protocol) - w.write_i32(v.guard) - w.write_i32(v.defer) - w.write_i32(v.inout) - w.write_i32(v.internal) - w.write_i32(v.when_) - w.write_i32(v.fun) - w.write_i32(v.data) - w.write_i32(v.late) - w.write_i32(v.part) - w.write_i32(v.out) - w.write_i32(v.ref) - w.write_i32(v.event) - w.write_i32(v.base) - w.write_i32(v.lambda) - w.write_i32(v.yield_) - w.write_i32(v.nil_) - w.write_i32(v.null) - w.write_i32(v.begin_) - w.write_i32(v.rescue_) - w.write_i32(v.module_) - w.write_i32(v.chan) - w.write_i32(v.go) - w.write_i32(v.let) - w.write_i32(v.export) - w.write_i32(v.delete) - w.write_i32(v.typedef) - w.write_i32(v.sizeof) - w.write_i32(v.static) - w.write_i32(v.super_) - end - - # @api private - # Unpacks a Keywords from the value-buffer wire format. - def self._wv_read_keywords(r) - Keywords.new( - class_: r.read_i32, - def_: r.read_i32, - import: r.read_i32, - from: r.read_i32, - func: r.read_i32, - range: r.read_i32, - register: r.read_i32, - volatile: r.read_i32, - new: r.read_i32, - default: r.read_i32, - end_: r.read_i32, - in_: r.read_i32, - is: r.read_i32, - int: r.read_i32, - var: r.read_i32, - val: r.read_i32, - self_: r.read_i32, - this: r.read_i32, - object: r.read_i32, - string: r.read_i32, - case_: r.read_i32, - select: r.read_i32, - switch: r.read_i32, - void: r.read_i32, - union: r.read_i32, - namespace: r.read_i32, - operator: r.read_i32, - template: r.read_i32, - package: r.read_i32, - protocol: r.read_i32, - guard: r.read_i32, - defer: r.read_i32, - inout: r.read_i32, - internal: r.read_i32, - when_: r.read_i32, - fun: r.read_i32, - data: r.read_i32, - late: r.read_i32, - part: r.read_i32, - out: r.read_i32, - ref: r.read_i32, - event: r.read_i32, - base: r.read_i32, - lambda: r.read_i32, - yield_: r.read_i32, - nil_: r.read_i32, - null: r.read_i32, - begin_: r.read_i32, - rescue_: r.read_i32, - module_: r.read_i32, - chan: r.read_i32, - go: r.read_i32, - let: r.read_i32, - export: r.read_i32, - delete: r.read_i32, - typedef: r.read_i32, - sizeof: r.read_i32, - static: r.read_i32, - super_: r.read_i32, - ) - end - - # @api private - # Packs a Retired into the value-buffer wire format. - def self._wv_write_retired(w, v) - w.write_i64(v.id) - end - - # @api private - # Unpacks a Retired from the value-buffer wire format. - def self._wv_read_retired(r) - Retired.new(id: r.read_i64) - end - - # @api private - # Packs a WidgetHolder into the value-buffer wire format. - def self._wv_write_widget_holder(w, v) - w.write_object(v.primary, Widget) - _wv_write_opt_widget(w, v.fallback) - _wv_write_list_widget(w, v.extras) - _wv_write_map_string_widget(w, v.by_name) - end - - # @api private - # Unpacks a WidgetHolder from the value-buffer wire format. - def self._wv_read_widget_holder(r) - WidgetHolder.new( - primary: r.read_object(Widget), - fallback: _wv_read_opt_widget(r), - extras: _wv_read_list_widget(r), - by_name: _wv_read_map_string_widget(r), - ) - end - - attach_function :edge_cases_edge_cases_Widget_clone, [:pointer], :pointer - attach_function :edge_cases_edge_cases_Widget_destroy, [:pointer], :void - attach_function :edge_cases_edge_cases_Widget_refresh, [:pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_Widget_rename, [:pointer, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_Widget_fetch, [:pointer, :int64, :pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_Widget_old_name, [:pointer, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_Widget_close, [:pointer, :pointer], :bool, blocking: true - attach_function :edge_cases_edge_cases_Widget_dispose, [:pointer, :bool, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_Vault_clone, [:pointer], :pointer - attach_function :edge_cases_edge_cases_Vault_destroy, [:pointer], :void - attach_function :edge_cases_edge_cases_Vault_open, [:pointer, :size_t, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_Vault_peek, [:pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_Vault_handle, [:pointer, :pointer], :int64, blocking: true - attach_function :edge_cases_edge_cases_Vault_probe, [:pointer], :bool, blocking: true - attach_function :edge_cases_edge_cases_Vault_to_string, [:pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_noop, [:pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_select, [:int32, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :bool, :pointer], :int32, blocking: true - attach_function :edge_cases_edge_cases_nested_optional, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_optional_params, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_register_batch, [:pointer, :pointer, :pointer, :pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_widgets_everywhere, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_widget_stream, [:pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_WidgetStreamIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true - attach_function :edge_cases_edge_cases_WidgetStreamIterator_destroy, [:pointer], :void - attach_function :edge_cases_edge_cases_async_widget, [:pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_make_widget, [:pointer, :size_t, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_find_widget, [:int64, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_inspect_widget, [:pointer, :pointer, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_string_stream, [:pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_StringStreamIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true - attach_function :edge_cases_edge_cases_StringStreamIterator_destroy, [:pointer], :void - attach_function :edge_cases_edge_cases_scalar_stream, [:pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_ScalarStreamIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true - attach_function :edge_cases_edge_cases_ScalarStreamIterator_destroy, [:pointer], :void - attach_function :edge_cases_edge_cases_mode_stream, [:pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_ModeStreamIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true - attach_function :edge_cases_edge_cases_ModeStreamIterator_destroy, [:pointer], :void - attach_function :edge_cases_edge_cases_async_void, [:pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_async_bytes, [:uint32, :pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_async_struct, [:pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_async_list, [:pointer, :pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_async_optional, [:pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_async_enum, [:pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_async_keywords, [:pointer, :size_t, :int32, :pointer, :pointer], :void, blocking: true - attach_function :edge_cases_edge_cases_keywords_roundtrip, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :edge_cases_edge_cases_legacy_value, [:int32, :pointer, :pointer], :pointer, blocking: true - - # @api private - # Completion trampoline for fetch. - EDGE_CASES_EDGE_CASES_WIDGET_FETCH_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_edge_errors_error_from(*taken) - else - value = _wv_decode(result_ptr, result_len) { |r| _wv_read_outer(r) } - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_widget. - EDGE_CASES_EDGE_CASES_ASYNC_WIDGET_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer]) do |ctx, err, result| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_edge_errors_error_from(*taken) - else - value = Widget._from_ptr(_wv_nonnull(result)) - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_void. - EDGE_CASES_EDGE_CASES_ASYNC_VOID_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer]) do |ctx, err| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = nil - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_bytes. - EDGE_CASES_EDGE_CASES_ASYNC_BYTES_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = _wv_take_bytes(result_ptr, result_len) - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_struct. - EDGE_CASES_EDGE_CASES_ASYNC_STRUCT_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_edge_errors_error_from(*taken) - else - value = _wv_decode(result_ptr, result_len) { |r| _wv_read_outer(r) } - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_list. - EDGE_CASES_EDGE_CASES_ASYNC_LIST_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_edge_errors_error_from(*taken) - else - value = _wv_decode(result_ptr, result_len) { |r| _wv_read_list_outer(r) } - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_optional. - EDGE_CASES_EDGE_CASES_ASYNC_OPTIONAL_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = _wv_decode(result_ptr, result_len) { |r| _wv_read_opt_i64(r) } - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_enum. - EDGE_CASES_EDGE_CASES_ASYNC_ENUM_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :int32]) do |ctx, err, result| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = result - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for async_keywords. - EDGE_CASES_EDGE_CASES_ASYNC_KEYWORDS_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = _wv_take_string(result_ptr, result_len) - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # Callback interface carrying composite payloads and objects - # - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a BatchListener is expected; include - # this module to inherit NotImplementedError defaults. The library - # may call the methods from any thread until it releases the - # implementation. - module BatchListener - # Composite payloads cross as value buffers - def on_batch(items, raw, first, mode) - raise NotImplementedError, "#{self.class}#on_batch is not implemented" - end - - # Objects cross as strong references the consumer must release - def on_widget(widget, fallback, children) - raise NotImplementedError, "#{self.class}#on_widget is not implemented" - end - - def on_tick - raise NotImplementedError, "#{self.class}#on_tick is not implemented" - end - - # Scalar return travels back through the vtable - # @return [Boolean] - def should_continue(class_) - raise NotImplementedError, "#{self.class}#should_continue is not implemented" - end - - # Enum return travels back as its integer discriminant - # @return [Integer] - def pick_mode - raise NotImplementedError, "#{self.class}#pick_mode is not implemented" - end - end - - # @api private - # The C vtable layout the library calls a BatchListener through. - class WvBatchListenerVtable < FFI::Struct - layout :wv_size, :uint32, - :wv_flags, :uint32, - :wv_free, :pointer, - :on_batch, :pointer, - :on_widget, :pointer, - :on_tick, :pointer, - :should_continue, :pointer, - :pick_mode, :pointer - end - - # @api private - # Releases a BatchListener implementation when the library drops its last - # reference. May run on any library thread. - WV_BATCH_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # @api private - # Trampoline for BatchListener#on_batch. - WV_BATCH_LISTENER_ON_BATCH = FFI::Function.new(:void, [:pointer, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :int32, :pointer]) do |wv_ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, wv_err| - items = _wv_decode_borrowed(items_ptr, items_len) { |r| _wv_read_list_outer(r) } - raw = _wv_borrow_bytes(raw_ptr, raw_len) - first = _wv_decode_borrowed(first_ptr, first_len) { |r| _wv_read_opt_outer(r) } - _wv_cb_lookup(wv_ctx).on_batch(items, raw, first, mode) - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for BatchListener#on_widget. - WV_BATCH_LISTENER_ON_WIDGET = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer, :size_t, :pointer]) do |wv_ctx, widget_ptr, fallback_ptr, children_ptr, children_len, wv_err| - widget = Widget._from_ptr(widget_ptr) - fallback = fallback_ptr.null? ? nil : Widget._from_ptr(fallback_ptr) - children = _wv_decode_borrowed(children_ptr, children_len) { |r| _wv_read_list_widget(r) } - _wv_cb_lookup(wv_ctx).on_widget(widget, fallback, children) - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for BatchListener#on_tick. - WV_BATCH_LISTENER_ON_TICK = FFI::Function.new(:void, [:pointer, :pointer]) do |wv_ctx, wv_err| - _wv_cb_lookup(wv_ctx).on_tick - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for BatchListener#should_continue. - WV_BATCH_LISTENER_SHOULD_CONTINUE = FFI::Function.new(:bool, [:pointer, :int32, :pointer]) do |wv_ctx, class_, wv_err| - _wv_cb_lookup(wv_ctx).should_continue(class_) ? true : false - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - false - end - - # @api private - # Trampoline for BatchListener#pick_mode. - WV_BATCH_LISTENER_PICK_MODE = FFI::Function.new(:int32, [:pointer, :pointer]) do |wv_ctx, wv_err| - _wv_int(_wv_cb_lookup(wv_ctx).pick_mode, :i32) - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - 0 - end - - # The one process-wide BatchListener vtable every registration hands the - # library; its entries live for the process lifetime. - WV_BATCH_LISTENER_VTABLE = WvBatchListenerVtable.new - WV_BATCH_LISTENER_VTABLE[:wv_size] = WvBatchListenerVtable.size - WV_BATCH_LISTENER_VTABLE[:wv_flags] = 0 - WV_BATCH_LISTENER_VTABLE[:wv_free] = WV_BATCH_LISTENER_FREE - WV_BATCH_LISTENER_VTABLE[:on_batch] = WV_BATCH_LISTENER_ON_BATCH - WV_BATCH_LISTENER_VTABLE[:on_widget] = WV_BATCH_LISTENER_ON_WIDGET - WV_BATCH_LISTENER_VTABLE[:on_tick] = WV_BATCH_LISTENER_ON_TICK - WV_BATCH_LISTENER_VTABLE[:should_continue] = WV_BATCH_LISTENER_SHOULD_CONTINUE - WV_BATCH_LISTENER_VTABLE[:pick_mode] = WV_BATCH_LISTENER_PICK_MODE - - # Callback interface whose name and methods are keywords - # - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a Keyword is expected; include - # this module to inherit NotImplementedError defaults. The library - # may call the methods from any thread until it releases the - # implementation. - module Keyword - def delete(self_) - raise NotImplementedError, "#{self.class}#delete is not implemented" - end - - # @return [Integer] - def import - raise NotImplementedError, "#{self.class}#import is not implemented" - end - end - - # @api private - # The C vtable layout the library calls a Keyword through. - class WvKeywordVtable < FFI::Struct - layout :wv_size, :uint32, - :wv_flags, :uint32, - :wv_free, :pointer, - :delete, :pointer, - :import, :pointer - end - - # @api private - # Releases a Keyword implementation when the library drops its last - # reference. May run on any library thread. - WV_KEYWORD_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # @api private - # Trampoline for Keyword#delete. - WV_KEYWORD_DELETE = FFI::Function.new(:void, [:pointer, :int32, :pointer]) do |wv_ctx, self_, wv_err| - _wv_cb_lookup(wv_ctx).delete(self_) - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for Keyword#import. - WV_KEYWORD_IMPORT = FFI::Function.new(:int32, [:pointer, :pointer]) do |wv_ctx, wv_err| - _wv_int(_wv_cb_lookup(wv_ctx).import, :i32) - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - 0 - end - - # The one process-wide Keyword vtable every registration hands the - # library; its entries live for the process lifetime. - WV_KEYWORD_VTABLE = WvKeywordVtable.new - WV_KEYWORD_VTABLE[:wv_size] = WvKeywordVtable.size - WV_KEYWORD_VTABLE[:wv_flags] = 0 - WV_KEYWORD_VTABLE[:wv_free] = WV_KEYWORD_FREE - WV_KEYWORD_VTABLE[:delete] = WV_KEYWORD_DELETE - WV_KEYWORD_VTABLE[:import] = WV_KEYWORD_IMPORT - - # @api private - # Owns one strong reference to a Widget; releases it exactly once. - class WidgetPtr < FFI::AutoPointer - def self.release(ptr) - EdgeCases.edge_cases_edge_cases_Widget_destroy(ptr) - end - end - - # Interface with no constructor; instances come from make_widget - class Widget < WvObject - WV_PTR = WidgetPtr - private_class_method :new - - # @api private - def self._wv_clone(ptr) - EdgeCases.edge_cases_edge_cases_Widget_clone(ptr) - end - - # Void method that can fail - def refresh - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - EdgeCases.edge_cases_edge_cases_Widget_refresh(_wv_self, err) - EdgeCases._wv_check_edge_errors_error!(err) - nil - end - end - - # @return [String] - def rename(class_, import = nil) - class__s = EdgeCases._wv_str(class_) - import_s = EdgeCases._wv_encode { |w| EdgeCases._wv_write_opt_string(w, import) } - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = EdgeCases.edge_cases_edge_cases_Widget_rename(_wv_self, class__s, class__s.bytesize, import_s, import_s.bytesize, out_len, err) - EdgeCases._wv_check!(err) - EdgeCases._wv_take_string(result, out_len.read(:size_t)) - end - end - - # Async method with a struct result - # Blocks until the call completes on a producer thread. - # @return [Outer] - def fetch(id) - queue = EdgeCases._wv_pin(self) do |_wv_self| - ctx, pending = EdgeCases._wv_async_begin - EdgeCases.edge_cases_edge_cases_Widget_fetch(_wv_self, id, EDGE_CASES_EDGE_CASES_WIDGET_FETCH_CALLBACK, ctx) - pending - end - EdgeCases._wv_async_wait(queue) - end - - # @return [String] - # @deprecated Use rename instead - def old_name - warn('EdgeCases::Widget#old_name is deprecated: Use rename instead', uplevel: 1) - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = EdgeCases.edge_cases_edge_cases_Widget_old_name(_wv_self, out_len, err) - EdgeCases._wv_check!(err) - EdgeCases._wv_take_string(result, out_len.read(:size_t)) - end - end - - # Spelled like the release method of several wrappers - # @return [Boolean] - def close_ - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - result = EdgeCases.edge_cases_edge_cases_Widget_close(_wv_self, err) - EdgeCases._wv_check!(err) - result - end - end - - # Spelled like the release method of the Dart and .NET wrappers - def dispose(force) - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - EdgeCases.edge_cases_edge_cases_Widget_dispose(_wv_self, (force ? true : false), err) - EdgeCases._wv_check!(err) - nil - end - end - end - - # @api private - # Owns one strong reference to a Vault; releases it exactly once. - class VaultPtr < FFI::AutoPointer - def self.release(ptr) - EdgeCases.edge_cases_edge_cases_Vault_destroy(ptr) - end - end - - # Deprecated interface with a throwing constructor - # @deprecated Use Widget instead - class Vault < WvObject - WV_PTR = VaultPtr - private_class_method :new - - # @api private - def self._wv_clone(ptr) - EdgeCases.edge_cases_edge_cases_Vault_clone(ptr) - end - - # Fails when the vault is busy - # @return [Vault] - def self.open(path) - path_s = EdgeCases._wv_str(path) - err = ErrorStruct.new - result = EdgeCases.edge_cases_edge_cases_Vault_open(path_s, path_s.bytesize, err) - EdgeCases._wv_check_edge_errors_error!(err) - _from_ptr(EdgeCases._wv_nonnull(result)) - end - - # @return [Widget, nil] - def peek - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - result = EdgeCases.edge_cases_edge_cases_Vault_peek(_wv_self, err) - EdgeCases._wv_check!(err) - result.null? ? nil : Widget._from_ptr(result) - end - end - - # Spelled like the handle accessor of several wrappers - # @return [Integer] - def handle_ - EdgeCases._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - result = EdgeCases.edge_cases_edge_cases_Vault_handle(_wv_self, err) - EdgeCases._wv_check!(err) - result - end - end - - # Throwing static - # @return [Boolean] - def self.probe - err = ErrorStruct.new - result = EdgeCases.edge_cases_edge_cases_Vault_probe(err) - EdgeCases._wv_check_edge_errors_error!(err) - result - end - - # Static spelled like an inherited object member - # @return [String] - def self.to_string - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = EdgeCases.edge_cases_edge_cases_Vault_to_string(out_len, err) - EdgeCases._wv_check!(err) - EdgeCases._wv_take_string(result, out_len.read(:size_t)) - end - end - - # No parameters and no result - def self.noop - err = ErrorStruct.new - edge_cases_edge_cases_noop(err) - _wv_check!(err) - nil - end - - # Function named after a keyword whose parameters are all keywords - # @return [Integer] - def self.select(class_, from, func, range, type) - from_s = _wv_str(from) - func_s = _wv_encode { |w| _wv_write_list_string(w, func) } - range_s = _wv_encode { |w| _wv_write_opt_i32(w, range) } - err = ErrorStruct.new - result = edge_cases_edge_cases_select(class_, from_s, from_s.bytesize, func_s, func_s.bytesize, range_s, range_s.bytesize, (type ? true : false), err) - _wv_check!(err) - result - end - - # Optional list of optional structs, the deepest legal nesting - # @return [Array, nil] - def self.nested_optional(items = nil) - items_s = _wv_encode { |w| _wv_write_opt_list_opt_outer(w, items) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = edge_cases_edge_cases_nested_optional(items_s, items_s.bytesize, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_opt_list_opt_outer(r) } - end - - # @return [Integer, nil] - def self.optional_params(text = nil, count = nil, outer = nil, tags = nil, blob = nil, mode = nil) - text_s = _wv_encode { |w| _wv_write_opt_string(w, text) } - count_s = _wv_encode { |w| _wv_write_opt_i32(w, count) } - outer_s = _wv_encode { |w| _wv_write_opt_outer(w, outer) } - tags_s = _wv_encode { |w| _wv_write_opt_list_string(w, tags) } - blob_s = _wv_encode { |w| _wv_write_opt_bytes(w, blob) } - mode_s = _wv_encode { |w| _wv_write_opt_mode(w, mode) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = edge_cases_edge_cases_optional_params(text_s, text_s.bytesize, count_s, count_s.bytesize, outer_s, outer_s.bytesize, tags_s, tags_s.bytesize, blob_s, blob_s.bytesize, mode_s, mode_s.bytesize, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_opt_mode(r) } - end - - # Two callback interface parameters on one function - def self.register_batch(listener, keyword) - _wv_present!(listener, 'listener') - _wv_present!(keyword, 'keyword') - err = ErrorStruct.new - listener_ctx, listener_vtable = _wv_cb_register(listener, WV_BATCH_LISTENER_VTABLE) - keyword_ctx, keyword_vtable = _wv_cb_register(keyword, WV_KEYWORD_VTABLE) - edge_cases_edge_cases_register_batch(listener_ctx, listener_vtable, keyword_ctx, keyword_vtable, err) - _wv_check!(err) - nil - end - - # Objects in every buffered position - # @return [WidgetHolder] - def self.widgets_everywhere(list, by_name, sparse, holder) - list_w = WvBufferWriter.new - _wv_write_list_widget(list_w, list) - by_name_w = WvBufferWriter.new - _wv_write_map_string_widget(by_name_w, by_name) - sparse_w = WvBufferWriter.new - _wv_write_list_opt_widget(sparse_w, sparse) - holder_w = WvBufferWriter.new - _wv_write_widget_holder(holder_w, holder) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - list_s, by_name_s, sparse_s, holder_s = _wv_seal(list_w, by_name_w, sparse_w, holder_w) - result = edge_cases_edge_cases_widgets_everywhere(list_s, list_s.bytesize, by_name_s, by_name_s.bytesize, sparse_s, sparse_s.bytesize, holder_s, holder_s.bytesize, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_widget_holder(r) } - end - - # Iterator of objects - # Returns a lazy Enumerator that pulls one element per step. The - # producer iterator starts on the first pull and is released when - # iteration finishes or is abandoned early. - # @return [Enumerator] - def self.widget_stream - Enumerator.new do |y| - err = ErrorStruct.new - iter = edge_cases_edge_cases_widget_stream(err) - begin - _wv_check!(err) - out_item = FFI::MemoryPointer.new(:pointer) - loop do - has_item = edge_cases_edge_cases_WidgetStreamIterator_next(iter, out_item, err) - _wv_check!(err) - break if has_item.zero? - item = out_item.read_pointer - value = Widget._from_ptr(_wv_nonnull(item)) - y << value - end - ensure - edge_cases_edge_cases_WidgetStreamIterator_destroy(iter) unless iter.null? - end - end - end - - # Async returning an object - # Blocks until the call completes on a producer thread. - # @return [Widget] - def self.async_widget - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_widget(EDGE_CASES_EDGE_CASES_ASYNC_WIDGET_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Factory for an interface with no constructor - # @return [Widget] - def self.make_widget(label) - label_s = _wv_str(label) - err = ErrorStruct.new - result = edge_cases_edge_cases_make_widget(label_s, label_s.bytesize, err) - _wv_check!(err) - Widget._from_ptr(_wv_nonnull(result)) - end - - # Optional interface return - # @return [Widget, nil] - def self.find_widget(id) - err = ErrorStruct.new - result = edge_cases_edge_cases_find_widget(id, err) - _wv_check_edge_errors_error!(err) - result.null? ? nil : Widget._from_ptr(result) - end - - # Interface parameter, including an optional one - # @return [String] - def self.inspect_widget(widget, fallback = nil) - _wv_object!(widget, Widget, 'widget') - _wv_object!(fallback, Widget, 'fallback', true) - _wv_pin(widget, fallback) do |_wv_widget, _wv_fallback| - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = edge_cases_edge_cases_inspect_widget(_wv_widget, _wv_fallback, out_len, err) - _wv_check!(err) - _wv_take_string(result, out_len.read(:size_t)) - end - end - - # Iterator of strings - # Returns a lazy Enumerator that pulls one element per step. The - # producer iterator starts on the first pull and is released when - # iteration finishes or is abandoned early. - # @return [Enumerator] - def self.string_stream - Enumerator.new do |y| - err = ErrorStruct.new - iter = edge_cases_edge_cases_string_stream(err) - begin - _wv_check!(err) - out_item = FFI::MemoryPointer.new(:pointer) - out_len = FFI::MemoryPointer.new(:size_t) - loop do - has_item = edge_cases_edge_cases_StringStreamIterator_next(iter, out_item, out_len, err) - _wv_check!(err) - break if has_item.zero? - item = out_item.read_pointer - value = _wv_take_string(item, out_len.read(:size_t)) - y << value - end - ensure - edge_cases_edge_cases_StringStreamIterator_destroy(iter) unless iter.null? - end - end - end - - # Iterator of scalars - # Returns a lazy Enumerator that pulls one element per step. The - # producer iterator starts on the first pull and is released when - # iteration finishes or is abandoned early. - # @return [Enumerator] - def self.scalar_stream - Enumerator.new do |y| - err = ErrorStruct.new - iter = edge_cases_edge_cases_scalar_stream(err) - begin - _wv_check!(err) - out_item = FFI::MemoryPointer.new(:int32) - loop do - has_item = edge_cases_edge_cases_ScalarStreamIterator_next(iter, out_item, err) - _wv_check!(err) - break if has_item.zero? - item = out_item.read(:int32) - value = item - y << value - end - ensure - edge_cases_edge_cases_ScalarStreamIterator_destroy(iter) unless iter.null? - end - end - end - - # Iterator of enums - # Returns a lazy Enumerator that pulls one element per step. The - # producer iterator starts on the first pull and is released when - # iteration finishes or is abandoned early. - # @return [Enumerator] - def self.mode_stream - Enumerator.new do |y| - err = ErrorStruct.new - iter = edge_cases_edge_cases_mode_stream(err) - begin - _wv_check!(err) - out_item = FFI::MemoryPointer.new(:int32) - loop do - has_item = edge_cases_edge_cases_ModeStreamIterator_next(iter, out_item, err) - _wv_check!(err) - break if has_item.zero? - item = out_item.read(:int32) - value = item - y << value - end - ensure - edge_cases_edge_cases_ModeStreamIterator_destroy(iter) unless iter.null? - end - end - end - - # Async with no result - # Blocks until the call completes on a producer thread. - def self.async_void - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_void(EDGE_CASES_EDGE_CASES_ASYNC_VOID_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Blocks until the call completes on a producer thread. - # @return [String] - def self.async_bytes(size) - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_bytes(size, EDGE_CASES_EDGE_CASES_ASYNC_BYTES_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Blocks until the call completes on a producer thread. - # @return [Outer] - def self.async_struct - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_struct(EDGE_CASES_EDGE_CASES_ASYNC_STRUCT_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Blocks until the call completes on a producer thread. - # @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled - # @return [Array] - def self.async_list(cancel: nil) - own_token = CancelToken.new if cancel.nil? - token = cancel || own_token - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_list(token._wv_ptr, EDGE_CASES_EDGE_CASES_ASYNC_LIST_CALLBACK, ctx) - _wv_async_wait(queue, token) - ensure - own_token&.close - end - - # Blocks until the call completes on a producer thread. - # @return [Integer, nil] - def self.async_optional - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_optional(EDGE_CASES_EDGE_CASES_ASYNC_OPTIONAL_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Blocks until the call completes on a producer thread. - # @return [Integer] - def self.async_enum - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_enum(EDGE_CASES_EDGE_CASES_ASYNC_ENUM_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # Blocks until the call completes on a producer thread. - # @return [String] - def self.async_keywords(class_, import) - class__s = _wv_str(class_) - ctx, queue = _wv_async_begin - edge_cases_edge_cases_async_keywords(class__s, class__s.bytesize, import, EDGE_CASES_EDGE_CASES_ASYNC_KEYWORDS_CALLBACK, ctx) - _wv_async_wait(queue) - end - - # @return [Keywords] - def self.keywords_roundtrip(value) - value_s = _wv_encode { |w| _wv_write_keywords(w, value) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = edge_cases_edge_cases_keywords_roundtrip(value_s, value_s.bytesize, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_keywords(r) } - end - - # @return [Retired] - # @deprecated Use Mode-based APIs - def self.legacy_value(value) - warn('EdgeCases.legacy_value is deprecated: Use Mode-based APIs', uplevel: 1) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = edge_cases_edge_cases_legacy_value(value, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_retired(r) } - end - - # === Value-buffer codecs of the composite types === - - # @api private - # Packs `[string]` into the value-buffer wire format. - def self._wv_write_list_string(w, v) - w.write_len(v.length) - v.each { |e| w.write_string(e) } - end - - # @api private - # Unpacks `[string]` from the value-buffer wire format. - def self._wv_read_list_string(r) - Array.new(r.read_len) { r.read_string } - end - - # @api private - # Packs `i32?` into the value-buffer wire format. - def self._wv_write_opt_i32(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_i32(v) - end - end - - # @api private - # Unpacks `i32?` from the value-buffer wire format. - def self._wv_read_opt_i32(r) - r.read_flag ? r.read_i32 : nil - end - - # @api private - # Packs `[Outer?]?` into the value-buffer wire format. - def self._wv_write_opt_list_opt_outer(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - _wv_write_list_opt_outer(w, v) - end - end - - # @api private - # Unpacks `[Outer?]?` from the value-buffer wire format. - def self._wv_read_opt_list_opt_outer(r) - r.read_flag ? _wv_read_list_opt_outer(r) : nil - end - - # @api private - # Packs `[Outer?]` into the value-buffer wire format. - def self._wv_write_list_opt_outer(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_opt_outer(w, e) } - end - - # @api private - # Unpacks `[Outer?]` from the value-buffer wire format. - def self._wv_read_list_opt_outer(r) - Array.new(r.read_len) { _wv_read_opt_outer(r) } - end - - # @api private - # Packs `Outer?` into the value-buffer wire format. - def self._wv_write_opt_outer(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - _wv_write_outer(w, v) - end - end - - # @api private - # Unpacks `Outer?` from the value-buffer wire format. - def self._wv_read_opt_outer(r) - r.read_flag ? _wv_read_outer(r) : nil - end - - # @api private - # Packs `string?` into the value-buffer wire format. - def self._wv_write_opt_string(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_string(v) - end - end - - # @api private - # Unpacks `string?` from the value-buffer wire format. - def self._wv_read_opt_string(r) - r.read_flag ? r.read_string : nil - end - - # @api private - # Packs `[string]?` into the value-buffer wire format. - def self._wv_write_opt_list_string(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - _wv_write_list_string(w, v) - end - end - - # @api private - # Unpacks `[string]?` from the value-buffer wire format. - def self._wv_read_opt_list_string(r) - r.read_flag ? _wv_read_list_string(r) : nil - end - - # @api private - # Packs `bytes?` into the value-buffer wire format. - def self._wv_write_opt_bytes(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_bytes(v) - end - end - - # @api private - # Unpacks `bytes?` from the value-buffer wire format. - def self._wv_read_opt_bytes(r) - r.read_flag ? r.read_bytes : nil - end - - # @api private - # Packs `Mode?` into the value-buffer wire format. - def self._wv_write_opt_mode(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_i32(v) - end - end - - # @api private - # Unpacks `Mode?` from the value-buffer wire format. - def self._wv_read_opt_mode(r) - r.read_flag ? r.read_i32 : nil - end - - # @api private - # Packs `[Widget]` into the value-buffer wire format. - def self._wv_write_list_widget(w, v) - w.write_len(v.length) - v.each { |e| w.write_object(e, Widget) } - end - - # @api private - # Unpacks `[Widget]` from the value-buffer wire format. - def self._wv_read_list_widget(r) - Array.new(r.read_len) { r.read_object(Widget) } - end - - # @api private - # Packs `{string:Widget}` into the value-buffer wire format. - def self._wv_write_map_string_widget(w, v) - w.write_len(v.length) - v.each do |k, e| - w.write_string(k) - w.write_object(e, Widget) - end - end - - # @api private - # Unpacks `{string:Widget}` from the value-buffer wire format. - def self._wv_read_map_string_widget(r) - Array.new(r.read_len) { [r.read_string, r.read_object(Widget)] }.to_h - end - - # @api private - # Packs `[Widget?]` into the value-buffer wire format. - def self._wv_write_list_opt_widget(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_opt_widget(w, e) } - end - - # @api private - # Unpacks `[Widget?]` from the value-buffer wire format. - def self._wv_read_list_opt_widget(r) - Array.new(r.read_len) { _wv_read_opt_widget(r) } - end - - # @api private - # Packs `Widget?` into the value-buffer wire format. - def self._wv_write_opt_widget(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_object(v, Widget) - end - end - - # @api private - # Unpacks `Widget?` from the value-buffer wire format. - def self._wv_read_opt_widget(r) - r.read_flag ? r.read_object(Widget) : nil - end - - # @api private - # Packs `[Outer]` into the value-buffer wire format. - def self._wv_write_list_outer(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_outer(w, e) } - end - - # @api private - # Unpacks `[Outer]` from the value-buffer wire format. - def self._wv_read_list_outer(r) - Array.new(r.read_len) { _wv_read_outer(r) } - end - - # @api private - # Packs `i64?` into the value-buffer wire format. - def self._wv_write_opt_i64(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_i64(v) - end - end - - # @api private - # Unpacks `i64?` from the value-buffer wire format. - def self._wv_read_opt_i64(r) - r.read_flag ? r.read_i64 : nil - end - - # @api private - # Packs `[Leaf]` into the value-buffer wire format. - def self._wv_write_list_leaf(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_leaf(w, e) } - end - - # @api private - # Unpacks `[Leaf]` from the value-buffer wire format. - def self._wv_read_list_leaf(r) - Array.new(r.read_len) { _wv_read_leaf(r) } - end - - # @api private - # Packs `Inner?` into the value-buffer wire format. - def self._wv_write_opt_inner(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - _wv_write_inner(w, v) - end - end - - # @api private - # Unpacks `Inner?` from the value-buffer wire format. - def self._wv_read_opt_inner(r) - r.read_flag ? _wv_read_inner(r) : nil - end - - # @api private - # Packs `{string:Inner}` into the value-buffer wire format. - def self._wv_write_map_string_inner(w, v) - w.write_len(v.length) - v.each do |k, e| - w.write_string(k) - _wv_write_inner(w, e) - end - end - - # @api private - # Unpacks `{string:Inner}` from the value-buffer wire format. - def self._wv_read_map_string_inner(r) - Array.new(r.read_len) { [r.read_string, _wv_read_inner(r)] }.to_h - end - - # @api private - # Packs `[Mode]` into the value-buffer wire format. - def self._wv_write_list_mode(w, v) - w.write_len(v.length) - v.each { |e| w.write_i32(e) } - end - - # @api private - # Unpacks `[Mode]` from the value-buffer wire format. - def self._wv_read_list_mode(r) - Array.new(r.read_len) { r.read_i32 } - end - - # @api private - # Packs `[[i32]]` into the value-buffer wire format. - def self._wv_write_list_list_i32(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_list_i32(w, e) } - end - - # @api private - # Unpacks `[[i32]]` from the value-buffer wire format. - def self._wv_read_list_list_i32(r) - Array.new(r.read_len) { _wv_read_list_i32(r) } - end - - # @api private - # Packs `[i32]` into the value-buffer wire format. - def self._wv_write_list_i32(w, v) - w.write_len(v.length) - v.each { |e| w.write_i32(e) } - end - - # @api private - # Unpacks `[i32]` from the value-buffer wire format. - def self._wv_read_list_i32(r) - Array.new(r.read_len) { r.read_i32 } - end - - # @api private - # Packs `[i64?]` into the value-buffer wire format. - def self._wv_write_list_opt_i64(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_opt_i64(w, e) } - end - - # @api private - # Unpacks `[i64?]` from the value-buffer wire format. - def self._wv_read_list_opt_i64(r) - Array.new(r.read_len) { _wv_read_opt_i64(r) } - end - - # @api private - # Packs `{i64:string}` into the value-buffer wire format. - def self._wv_write_map_i64_string(w, v) - w.write_len(v.length) - v.each do |k, e| - w.write_i64(k) - w.write_string(e) - end - end - - # @api private - # Unpacks `{i64:string}` from the value-buffer wire format. - def self._wv_read_map_i64_string(r) - Array.new(r.read_len) { [r.read_i64, r.read_string] }.to_h - end -end - -# END edge_cases.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__README_md.snap deleted file mode 100644 index a338208c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__README_md.snap +++ /dev/null @@ -1,33 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink (Ruby) - -Ruby bindings for `kitchen_sink` using the [ffi](https://github.com/ffi/ffi) gem. - -## Prerequisites - -- Ruby 2.7 or newer -- The native library (`libkitchen_sink.so`, `libkitchen_sink.dylib`, or `kitchen_sink.dll`) on the library - search path, or its path in the `KITCHEN_SINK_LIBRARY` environment variable. - -## Install - -```bash -gem build kitchen_sink.gemspec -gem install kitchen_sink-0.1.0.gem -``` - -## Usage - -```ruby -require 'kitchen_sink' - -KitchenSink::ABI_VERSION -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__kitchen_sink_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__kitchen_sink_gemspec.snap deleted file mode 100644 index 4fbc646f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__kitchen_sink_gemspec.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -Gem::Specification.new do |s| - s.name = 'kitchen_sink' - s.version = '0.1.0' - s.summary = 'Native bindings for kitchen_sink' - s.authors = ['kitchen_sink'] - s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - s.required_ruby_version = '>= 2.7' - - s.add_dependency 'ffi', '~> 1.15' -end - -# END kitchen_sink.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap index 0cdde38e..44318da2 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap @@ -12,154 +12,259 @@ require_relative 'kitchen_sink/runtime' # Ruby bindings for kitchen_sink. module KitchenSink # The declarations these bindings were generated with, per top-level - # module, as [id, hash, path]. Loading fails, naming the path, unless - # the library's contract table holds each one with an equal hash. - _wv_check_contract!( + # module, as [id, hash, path]. Loading raises LoadError, naming the path, + # unless the library's contract table holds each one with an equal hash. + Bridge.check_contract!( kitchen_sink_shared_contract: [ + # function ping() -> string [0x709e234caa962446, 0x3894f2ca452e9f98, 'shared.ping'], - [0xcfb306fa5801ee5b, 0x1cbd9e10983cac2c, 'shared.Token'], + # record Token {i64, string} + [0xcfb306fa5801ee5b, 0x18e9fc204a13df6f, 'shared.Token'], ], kitchen_sink_kitchen_contract: [ - [0x01d4f09bbddc986f, 0x29eb88fecc66544d, 'kitchen.Gadget.new'], - [0x10472d8fe748c2ee, 0xb265832b6a759fc4, 'kitchen.maybe_gadget'], + # constructor new(i64) -> Gadget + [0x01d4f09bbddc986f, 0xffcf2bd3b04aa002, 'kitchen.Gadget.new'], + # callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + [0x02bbd5f31c953704, 0x2cfa679ea1373e09, 'kitchen.Sampler.samples'], + # function maybe_gadget(Gadget?) -> Gadget? + [0x10472d8fe748c2ee, 0xcc570a2649bbf7dc, 'kitchen.maybe_gadget'], + # function gadgets() -> [Gadget] [0x11368eec07a74078, 0xfa049f78b77e146a, 'kitchen.gadgets'], + # function stream_gadgets() -> iter [0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, 'kitchen.stream_gadgets'], - [0x2111b54980826dd7, 0x452783f3e6ca6e1d, 'kitchen.do_cancellable'], + # function ids_u64([u64]) -> [u64] + [0x184bfc4ab0e3fe45, 0xb64587477022696b, 'kitchen.ids_u64'], + # function do_cancellable(string) -> string async cancellable + [0x2111b54980826dd7, 0x627bf01ab365b209, 'kitchen.do_cancellable'], + # callback_method #4 favorite() -> Gadget + [0x21c70f9371374d74, 0x25d5d88a30fe0a79, 'kitchen.ReadyListener.favorite'], + # function stream_chunks() -> iter<[i32]> + [0x23de5bd597372f46, 0xcf1bc6bb37872798, 'kitchen.stream_chunks'], + # function take_from_pantry(string) -> i32 throws PantryError + [0x249f1ddaec6a6cbe, 0x0710d6c9a4de93da, 'kitchen.take_from_pantry'], + # enum Priority {Low = 0, Normal = 1, High = 2} [0x29dddbc60bf8d7db, 0x452a3e573d57f6db, 'kitchen.Priority'], - [0x2b227003b74a0d16, 0x12332957470e9782, 'kitchen.maybe_subscribe'], - [0x2dd66cb0064f0494, 0x60197363fefd92dd, 'kitchen.Item'], - [0x33c265bbf079b0e9, 0xc3dcd0927e6c1deb, 'kitchen.u32_id'], - [0x33e6381d01660dfe, 0x077c63c31fd7f90a, 'kitchen.KitchenErrors'], - [0x3f5fc99d73504b76, 0x2df88d710f1ea10e, 'kitchen.Gadget.poke'], - [0x4c5764cf109528d1, 0x2a65529af4338a58, 'kitchen.maybe_item'], - [0x4de17dfb93fd494d, 0xba0ad0dd52ed74af, 'kitchen.i32_id'], + # function maybe_subscribe(ReadyListener?) -> bool + [0x2b227003b74a0d16, 0x5dae036b8a6aed30, 'kitchen.maybe_subscribe'], + # record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + [0x2dd66cb0064f0494, 0x74b131724dd0d867, 'kitchen.Item'], + # function u32_id(u32) -> u32 + [0x33c265bbf079b0e9, 0x97c32b764693f2cb, 'kitchen.u32_id'], + # errors KitchenErrors + [0x33e6381d01660dfe, 0x05e35a563089bf7d, 'kitchen.KitchenErrors'], + # callback_method #3 latest() -> Item? + [0x3d969ae76db6cb23, 0x65a325cfc772296b, 'kitchen.ReadyListener.latest'], + # method poke(i32) -> i32 throws KitchenErrors + [0x3f5fc99d73504b76, 0x417e4a2acd3210b3, 'kitchen.Gadget.poke'], + # callback Sampler + [0x4082f0085db28781, 0x18848e701962912e, 'kitchen.Sampler'], + # callback_method #2 describe() -> string throws any + [0x4340a6b1e7ec8c50, 0xa59d0f32a66dc7a2, 'kitchen.Sampler.describe'], + # function maybe_item(i64) -> Item? throws KitchenErrors + [0x4c5764cf109528d1, 0xb374accf14676d90, 'kitchen.maybe_item'], + # function i32_id(i32) -> i32 + [0x4de17dfb93fd494d, 0x78be3203bcd10727, 'kitchen.i32_id'], + # function stream_items() -> iter [0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, 'kitchen.stream_items'], - [0x68cdb9a4bd3e3dd5, 0xc678b9164348d497, 'kitchen.bool_id'], - [0x69e7cd703d16eb85, 0xf3b28dd22ae9e689, 'kitchen.do_async'], + # code Spoiled = 2 + [0x5dafabb7775fcb2d, 0x6c4b78c7d3f858c1, 'kitchen.PantryError.Spoiled'], + # callback_method #0 threshold(u32?) -> f64? + [0x5e0a8f6be4b00600, 0x18ea1a42036c96a0, 'kitchen.Sampler.threshold'], + # function async_samples(u32) -> [f32] async cancellable + [0x5ec0af1d4ecc6fed, 0x6540bdb74246420c, 'kitchen.async_samples'], + # function async_maybe_count() -> i64? async + [0x61a358b0571e0a4e, 0x11a24c4b50d11996, 'kitchen.async_maybe_count'], + # function bool_id(bool) -> bool + [0x68cdb9a4bd3e3dd5, 0xcde022fe6bc3b1bf, 'kitchen.bool_id'], + # function do_async(string) -> string async + [0x69e7cd703d16eb85, 0xb13cb8d39e702c79, 'kitchen.do_async'], + # method describe() -> string [0x70865079ce41be82, 0xbe3b69464672af46, 'kitchen.Gadget.describe'], - [0x71c5a1116cb7c49a, 0xcba1837108a89b84, 'kitchen.echo_bytes'], - [0x735ee1e0049ee91a, 0xca74320bc53eddab, 'kitchen.echo_string'], - [0x76d7fc58f4e41d84, 0x7373a0e47d30e690, 'kitchen.i64_id'], + # function echo_bytes(bytes) -> bytes + [0x71c5a1116cb7c49a, 0x1506cfe6f548fac4, 'kitchen.echo_bytes'], + # function echo_string(string) -> string + [0x735ee1e0049ee91a, 0xa713dbd1be707752, 'kitchen.echo_string'], + # function i64_id(i64) -> i64 + [0x76d7fc58f4e41d84, 0x6bdb4ccea2841d5e, 'kitchen.i64_id'], + # function stream_maybe() -> iter + [0x78d91fd617e162b8, 0xd88e2753503b36df, 'kitchen.stream_maybe'], + # function hello() -> string [0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, 'kitchen.nested.hello'], + # function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + [0x7b3865b536da998c, 0x1c4e79d60a20e74f, 'kitchen.mixed_slices'], + # code InvalidInput = 2 + [0x7ba266dc26a58989, 0x0f86001cac0c21e8, 'kitchen.KitchenErrors.InvalidInput'], + # function new_op() -> i32 [0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, 'kitchen.new_op'], - [0x89a5e664bc25c67b, 0x5764a95e81ecfd31, 'kitchen.f64_id'], + # function scale_i32([i32], i32) -> [i32] + [0x81ded69adee0abf2, 0x8c71141a931906f6, 'kitchen.scale_i32'], + # callback_method #2 label() -> string throws KitchenErrors + [0x81fa72769330fed6, 0x89681e305b55a4f4, 'kitchen.ReadyListener.label'], + # function f64_id(f64) -> f64 + [0x89a5e664bc25c67b, 0x357fd9b65ad879a9, 'kitchen.f64_id'], + # static version() -> string [0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, 'kitchen.Gadget.version'], + # function opt_priority(Priority?) -> Priority? + [0x8dc42397e47614b9, 0xcddf6fede09b2e7f, 'kitchen.opt_priority'], + # callback_method #1 on_item(Item, Gadget) -> bool + [0x92f060ef68961fc1, 0x4af6ce5b4f9fa508, 'kitchen.ReadyListener.on_item'], + # function cross_module_token() -> Token [0x974b9f3e823c1738, 0xb81b91e975695f27, 'kitchen.cross_module_token'], + # function sum_f64([f64]) -> f64 + [0x9f0f001aa85f97d5, 0x55815247f895bd45, 'kitchen.sum_f64'], + # code NotFound = 1 + [0xab2a29fd1ef5458b, 0x471a34aa4c09ae2b, 'kitchen.KitchenErrors.NotFound'], + # function list_items() -> [Item] [0xaca0655fac15d230, 0x11c4d2b28abe07bb, 'kitchen.list_items'], + # errors PantryError + [0xb57c8e6ae3fb4383, 0x3f857851ab75366c, 'kitchen.PantryError'], + # function opt_mixed(f64, f32?, i64?, string?) -> u16? + [0xbdc795b195edd428, 0x36127462f8b0969f, 'kitchen.opt_mixed'], + # callback_method #0 on_ready(i32, string) -> void + [0xbe4821ab3c340e71, 0xe1d32e38b766e623, 'kitchen.ReadyListener.on_ready'], + # function opt_i32_id(i32?) -> i32? + [0xc2ad53f2a6a36323, 0x071e29abee823f17, 'kitchen.opt_i32_id'], + # function parse_any(string) -> i64 throws any + [0xc7ab409554fd3d7b, 0xfea12b6bc219fc46, 'kitchen.parse_any'], + # function sample_with(Sampler, u32?) -> [i16] throws PantryError + [0xc82a7f2d85f99f4e, 0xa9bcb8bacedb036c, 'kitchen.sample_with'], + # function legacy_op() -> i32 [0xc883b4afae20543e, 0xc40560e5411ff392, 'kitchen.legacy_op'], + # code OutOfStock = 1 {string} + [0xd180bf646bff59e2, 0xa4515a6d00d7c600, 'kitchen.PantryError.OutOfStock'], + # function opt_bool_id(bool?) -> bool? + [0xe6778df0f5c1b82b, 0x38eaafdb350559e1, 'kitchen.opt_bool_id'], + # function get_attrs() -> {string:i32} [0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, 'kitchen.get_attrs'], - [0xfae25d3391922ead, 0x1c7f95739f3210b8, 'kitchen.subscribe'], + # function subscribe(ReadyListener) -> i32 + [0xfae25d3391922ead, 0xcc034cf506cae182, 'kitchen.subscribe'], + # interface Gadget [0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, 'kitchen.Gadget'], - [0xfed9c551fb9fd2d6, 0x59aec980ef3f3291, 'kitchen.ReadyListener'], + # callback ReadyListener + [0xfed9c551fb9fd2d6, 0xe1020573f084dc1d, 'kitchen.ReadyListener'], + # method readings(u32?) -> [f64] throws any + [0xff3a99b52597dee2, 0x14c4ecac97bdfc9b, 'kitchen.Gadget.readings'], ], ) - # === Module: shared === - - # A reusable token referenced across modules - class Token - attr_reader :id - - attr_reader :label - - def initialize(id:, label:) - @id = id - @label = label - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Token) && - id == other.id && - label == other.label - end - end - - # @api private - # Packs a Token into the value-buffer wire format. - def self._wv_write_token(w, v) - w.write_i64(v.id) - w.write_string(v.label) + # The C functions the wrappers below call (private). + module Native + # shared + attach_function :kitchen_sink_shared_ping, [:pointer, :pointer], :pointer, blocking: true + + # kitchen + attach_function :kitchen_sink_kitchen_Gadget_clone, [:pointer], :pointer + attach_function :kitchen_sink_kitchen_Gadget_destroy, [:pointer], :void + attach_function :kitchen_sink_kitchen_Gadget_new, [:int64, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_describe, [:pointer, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_poke, [:pointer, :int32, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_readings, [:pointer, :bool, :uint32, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_version, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_bool_id, [:bool, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_i32_id, [:int32, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_u32_id, [:uint32, :pointer], :uint32, blocking: true + attach_function :kitchen_sink_kitchen_i64_id, [:int64, :pointer], :int64, blocking: true + attach_function :kitchen_sink_kitchen_f64_id, [:double, :pointer], :double, blocking: true + attach_function :kitchen_sink_kitchen_echo_string, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_echo_bytes, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_subscribe, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_maybe_subscribe, [:pointer, :pointer, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_maybe_gadget, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_gadgets, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_stream_gadgets, [:pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_StreamGadgetsIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_StreamGadgetsIterator_destroy, [:pointer], :void + attach_function :kitchen_sink_kitchen_maybe_item, [:int64, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_list_items, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_get_attrs, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_stream_items, [:pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_StreamItemsIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_StreamItemsIterator_destroy, [:pointer], :void + attach_function :kitchen_sink_kitchen_cross_module_token, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_do_async, [:pointer, :size_t, :pointer, :pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_do_cancellable, [:pointer, :size_t, :pointer, :pointer, :pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_legacy_op, [:pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_new_op, [:pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_opt_i32_id, [:bool, :int32, :pointer, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_opt_bool_id, [:bool, :bool, :pointer, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_opt_priority, [:bool, :int32, :pointer, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_opt_mixed, [:double, :bool, :float, :bool, :int64, :pointer, :size_t, :pointer, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_sum_f64, [:pointer, :size_t, :pointer], :double, blocking: true + attach_function :kitchen_sink_kitchen_scale_i32, [:pointer, :size_t, :int32, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_ids_u64, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_mixed_slices, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_async_maybe_count, [:pointer, :pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_async_samples, [:uint32, :pointer, :pointer, :pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_stream_maybe, [:pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_StreamMaybeIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_StreamMaybeIterator_destroy, [:pointer], :void + attach_function :kitchen_sink_kitchen_stream_chunks, [:pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_StreamChunksIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_StreamChunksIterator_destroy, [:pointer], :void + attach_function :kitchen_sink_kitchen_sample_with, [:pointer, :pointer, :bool, :uint32, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_take_from_pantry, [:pointer, :size_t, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_parse_any, [:pointer, :size_t, :pointer], :int64, blocking: true + + # kitchen.nested + attach_function :kitchen_sink_kitchen_nested_hello, [:pointer, :pointer], :pointer, blocking: true end - # @api private - # Unpacks a Token from the value-buffer wire format. - def self._wv_read_token(r) - Token.new(id: r.read_i64, label: r.read_string) - end + # === Module: shared === - attach_function :kitchen_sink_shared_ping, [:pointer, :pointer], :pointer, blocking: true + # A reusable token referenced across modules + # + # @!attribute [r] id + # @return [Integer] + # @!attribute [r] label + # @return [String] + Token = Data.define(:id, :label) # Trivial cross-module helper + # # @return [String] def self.ping - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_shared_ping(out_len, err) - _wv_check!(err) - _wv_take_string(result, out_len.read(:size_t)) + Bridge.call(nil, :string) do |out_len, out_err| + Native.kitchen_sink_shared_ping(out_len, out_err) + end end # === Module: kitchen === - # Base error for the `kitchen` module's error domain. - class KitchenErrorsError < Error + # The `KitchenErrors` error domain of the `kitchen` module. A code these + # bindings don't know (from a newer library) raises this class itself, with + # its code and message. + class KitchenError < Error # Item not found - class NotFound < KitchenErrorsError + class NotFound < KitchenError CODE = 1 - - def initialize(message = nil) - super(CODE, message || 'Item not found') - end + MESSAGE = 'Item not found' end # Invalid input - class InvalidInput < KitchenErrorsError + class InvalidInput < KitchenError CODE = 2 - - def initialize(message = nil) - super(CODE, message || 'Invalid input') - end + MESSAGE = 'Invalid input' end end - # @api private - # The KitchenErrorsError for a domain `code`, with its payload fields decoded; - # the root Error (or Cancelled) for any other code. - def self._wv_kitchen_errors_error_from(code, message, payload = nil) - message = nil if message.empty? - r = WvBufferReader.new(payload) - error = - case code - when 1 - KitchenErrorsError::NotFound.new(message) - when 2 - KitchenErrorsError::InvalidInput.new(message) - else - return _wv_error(code, message.to_s) - end - r.expect_end! - error - end - - # @api private - # Raises the KitchenErrorsError for a non-zero error slot. - def self._wv_check_kitchen_errors_error!(err) - taken = _wv_take_error(err) - raise _wv_kitchen_errors_error_from(*taken) unless taken.nil? - end - - # @api private - # The code and value-buffer payload of a KitchenErrorsError a callback - # implementation raised, or nil for an error of no declared code. - def self._wv_kitchen_errors_error_payload(error) - w = WvBufferWriter.new - code = - case error - when KitchenErrorsError::NotFound then 1 - when KitchenErrorsError::InvalidInput then 2 - end - code && [code, w.bytes] + # The `PantryError` error domain of the `kitchen` module. A code these + # bindings don't know (from a newer library) raises this class itself, with + # its code and message. + class PantryError < Error + # Out of stock + class OutOfStock < PantryError + CODE = 1 + MESSAGE = 'Out of stock' + + # @return [String] + attr_reader :item + end + + # Spoiled + class Spoiled < PantryError + CODE = 2 + MESSAGE = 'Spoiled' + end end # Task priority levels @@ -173,757 +278,763 @@ module KitchenSink end # Kitchen sink struct exercising every field feature - class Item - # Stable identifier - attr_reader :id - - # Display name - attr_reader :name - - # Initial count - attr_reader :count - - # Whether the item is enabled - attr_reader :enabled - - # Scaling ratio - attr_reader :ratio - - attr_reader :nick - - attr_reader :payload - - attr_reader :tags - - attr_reader :attrs - - attr_reader :parent - - # Cross-module struct reference - attr_reader :token - - attr_reader :priority - - # An optional object inside a record - attr_reader :gadget - - def initialize( - id:, - name:, - count:, - enabled:, - ratio:, - nick:, - payload:, - tags:, - attrs:, - parent:, - token:, - priority:, - gadget: - ) - @id = id - @name = name - @count = count - @enabled = enabled - @ratio = ratio - @nick = nick - @payload = payload - @tags = tags - @attrs = attrs - @parent = parent - @token = token - @priority = priority - @gadget = gadget - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Item) && - id == other.id && - name == other.name && - count == other.count && - enabled == other.enabled && - ratio == other.ratio && - nick == other.nick && - payload == other.payload && - tags == other.tags && - attrs == other.attrs && - parent == other.parent && - token == other.token && - priority == other.priority && - gadget == other.gadget - end - end - - # @api private - # Packs an Item into the value-buffer wire format. - def self._wv_write_item(w, v) - w.write_i64(v.id) - w.write_string(v.name) - w.write_i32(v.count) - w.write_bool(v.enabled) - w.write_f64(v.ratio) - w.write_u32(v.nick) - w.write_bytes(v.payload) - _wv_write_list_string(w, v.tags) - _wv_write_map_string_string(w, v.attrs) - _wv_write_opt_i64(w, v.parent) - _wv_write_token(w, v.token) - w.write_i32(v.priority) - _wv_write_opt_gadget(w, v.gadget) - end - - # @api private - # Unpacks an Item from the value-buffer wire format. - def self._wv_read_item(r) - Item.new( - id: r.read_i64, - name: r.read_string, - count: r.read_i32, - enabled: r.read_bool, - ratio: r.read_f64, - nick: r.read_u32, - payload: r.read_bytes, - tags: _wv_read_list_string(r), - attrs: _wv_read_map_string_string(r), - parent: _wv_read_opt_i64(r), - token: _wv_read_token(r), - priority: r.read_i32, - gadget: _wv_read_opt_gadget(r), - ) - end - - attach_function :kitchen_sink_kitchen_Gadget_clone, [:pointer], :pointer - attach_function :kitchen_sink_kitchen_Gadget_destroy, [:pointer], :void - attach_function :kitchen_sink_kitchen_Gadget_new, [:int64, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_Gadget_describe, [:pointer, :pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_Gadget_poke, [:pointer, :int32, :pointer], :int32, blocking: true - attach_function :kitchen_sink_kitchen_Gadget_version, [:pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_bool_id, [:bool, :pointer], :bool, blocking: true - attach_function :kitchen_sink_kitchen_i32_id, [:int32, :pointer], :int32, blocking: true - attach_function :kitchen_sink_kitchen_u32_id, [:uint32, :pointer], :uint32, blocking: true - attach_function :kitchen_sink_kitchen_i64_id, [:int64, :pointer], :int64, blocking: true - attach_function :kitchen_sink_kitchen_f64_id, [:double, :pointer], :double, blocking: true - attach_function :kitchen_sink_kitchen_echo_string, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_echo_bytes, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_subscribe, [:pointer, :pointer, :pointer], :int32, blocking: true - attach_function :kitchen_sink_kitchen_maybe_subscribe, [:pointer, :pointer, :pointer], :bool, blocking: true - attach_function :kitchen_sink_kitchen_maybe_gadget, [:pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_gadgets, [:pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_stream_gadgets, [:pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_StreamGadgetsIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true - attach_function :kitchen_sink_kitchen_StreamGadgetsIterator_destroy, [:pointer], :void - attach_function :kitchen_sink_kitchen_maybe_item, [:int64, :pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_list_items, [:pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_get_attrs, [:pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_stream_items, [:pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_StreamItemsIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true - attach_function :kitchen_sink_kitchen_StreamItemsIterator_destroy, [:pointer], :void - attach_function :kitchen_sink_kitchen_cross_module_token, [:pointer, :pointer], :pointer, blocking: true - attach_function :kitchen_sink_kitchen_do_async, [:pointer, :size_t, :pointer, :pointer], :void, blocking: true - attach_function :kitchen_sink_kitchen_do_cancellable, [:pointer, :size_t, :pointer, :pointer, :pointer], :void, blocking: true - attach_function :kitchen_sink_kitchen_legacy_op, [:pointer], :int32, blocking: true - attach_function :kitchen_sink_kitchen_new_op, [:pointer], :int32, blocking: true - - # @api private - # Completion trampoline for do_async. - KITCHEN_SINK_KITCHEN_DO_ASYNC_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = _wv_take_string(result_ptr, result_len) - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end - - # @api private - # Completion trampoline for do_cancellable. - KITCHEN_SINK_KITCHEN_DO_CANCELLABLE_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| - queue = _wv_async_finish(ctx) - begin - taken = _wv_take_boxed_error(err) - if taken - queue << _wv_trap(*taken) - else - value = _wv_take_string(result_ptr, result_len) - queue << value - end - rescue Exception => e # rubocop:disable Lint/RescueException - queue << e - end - end + # + # @!attribute [r] id + # @return [Integer] Stable identifier + # @!attribute [r] name + # @return [String] Display name + # @!attribute [r] count + # @return [Integer] Initial count + # @!attribute [r] enabled + # @return [Boolean] Whether the item is enabled + # @!attribute [r] ratio + # @return [Float] Scaling ratio + # @!attribute [r] nick + # @return [Integer] + # @!attribute [r] payload + # @return [String] + # @!attribute [r] tags + # @return [Array] + # @!attribute [r] attrs + # @return [Hash{String => String}] + # @!attribute [r] parent + # @return [Integer, nil] + # @!attribute [r] token + # @return [Token] Cross-module struct reference + # @!attribute [r] priority + # @return [Integer] + # @!attribute [r] gadget + # @return [Gadget, nil] An optional object inside a record + Item = Data.define( + :id, + :name, + :count, + :enabled, + :ratio, + :nick, + :payload, + :tags, + :attrs, + :parent, + :token, + :priority, + :gadget, + ) # Consumer-implemented observer of ready items # - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a ReadyListener is expected; include - # this module to inherit NotImplementedError defaults. The library - # may call the methods from any thread until it releases the - # implementation. + # A callback interface: pass any object that responds to the methods below + # wherever a ReadyListener is expected. Including this module documents the + # intent and supplies NotImplementedError defaults. The library may call the + # methods from any thread until it releases the implementation. module ReadyListener # Fires when an item is ready + # + # @param code [Integer] + # @param msg [String] + # @return [void] def on_ready(code, msg) raise NotImplementedError, "#{self.class}#on_ready is not implemented" end # Receives the item itself and says whether to keep listening + # + # @param item [Item] + # @param gadget [Gadget] # @return [Boolean] def on_item(item, gadget) raise NotImplementedError, "#{self.class}#on_item is not implemented" end # A display label, failing with a kitchen error + # # @return [String] - # @raise [KitchenErrorsError] reported to the library with its code and fields + # @raise [KitchenError] reported to the library with its code and fields def label raise NotImplementedError, "#{self.class}#label is not implemented" end # The most recent item the listener kept, if any + # # @return [Item, nil] def latest raise NotImplementedError, "#{self.class}#latest is not implemented" end # The listener's favorite gadget + # # @return [Gadget] def favorite raise NotImplementedError, "#{self.class}#favorite is not implemented" end end - # @api private - # The C vtable layout the library calls a ReadyListener through. - class WvReadyListenerVtable < FFI::Struct - layout :wv_size, :uint32, - :wv_flags, :uint32, - :wv_free, :pointer, - :on_ready, :pointer, - :on_item, :pointer, - :label, :pointer, - :latest, :pointer, - :favorite, :pointer - end - - # @api private - # Releases a ReadyListener implementation when the library drops its last - # reference. May run on any library thread. - WV_READY_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # @api private - # Trampoline for ReadyListener#on_ready. - WV_READY_LISTENER_ON_READY = FFI::Function.new(:void, [:pointer, :int32, :pointer, :size_t, :pointer]) do |wv_ctx, code, msg_ptr, msg_len, wv_err| - msg = _wv_borrow_string(msg_ptr, msg_len) - _wv_cb_lookup(wv_ctx).on_ready(code, msg) - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for ReadyListener#on_item. - WV_READY_LISTENER_ON_ITEM = FFI::Function.new(:bool, [:pointer, :pointer, :size_t, :pointer, :pointer]) do |wv_ctx, item_ptr, item_len, gadget_ptr, wv_err| - gadget = Gadget._from_ptr(gadget_ptr) - item = _wv_decode_borrowed(item_ptr, item_len) { |r| _wv_read_item(r) } - _wv_cb_lookup(wv_ctx).on_item(item, gadget) ? true : false - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - false - end - - # @api private - # Trampoline for ReadyListener#label. - WV_READY_LISTENER_LABEL = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer]) do |wv_ctx, wv_out_ptr, wv_out_len, wv_err| - _wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_str(_wv_cb_lookup(wv_ctx).label)) - rescue KitchenErrorsError => e - _wv_cb_throw(wv_err, e) { _wv_kitchen_errors_error_payload(e) } - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for ReadyListener#latest. - WV_READY_LISTENER_LATEST = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer]) do |wv_ctx, wv_out_ptr, wv_out_len, wv_err| - result = _wv_cb_lookup(wv_ctx).latest - writer = WvBufferWriter.new - _wv_write_opt_item(writer, result) - _wv_cb_return_bytes(wv_out_ptr, wv_out_len, _wv_seal(writer).first) - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - nil - end - - # @api private - # Trampoline for ReadyListener#favorite. - WV_READY_LISTENER_FAVORITE = FFI::Function.new(:pointer, [:pointer, :pointer]) do |wv_ctx, wv_err| - _wv_cb_return_object(_wv_cb_lookup(wv_ctx).favorite, Gadget) - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(wv_err, e) - FFI::Pointer::NULL - end - - # The one process-wide ReadyListener vtable every registration hands the - # library; its entries live for the process lifetime. - WV_READY_LISTENER_VTABLE = WvReadyListenerVtable.new - WV_READY_LISTENER_VTABLE[:wv_size] = WvReadyListenerVtable.size - WV_READY_LISTENER_VTABLE[:wv_flags] = 0 - WV_READY_LISTENER_VTABLE[:wv_free] = WV_READY_LISTENER_FREE - WV_READY_LISTENER_VTABLE[:on_ready] = WV_READY_LISTENER_ON_READY - WV_READY_LISTENER_VTABLE[:on_item] = WV_READY_LISTENER_ON_ITEM - WV_READY_LISTENER_VTABLE[:label] = WV_READY_LISTENER_LABEL - WV_READY_LISTENER_VTABLE[:latest] = WV_READY_LISTENER_LATEST - WV_READY_LISTENER_VTABLE[:favorite] = WV_READY_LISTENER_FAVORITE - - # @api private - # Owns one strong reference to a Gadget; releases it exactly once. - class GadgetPtr < FFI::AutoPointer - def self.release(ptr) - KitchenSink.kitchen_sink_kitchen_Gadget_destroy(ptr) + # Consumer-implemented source of scalar samples + # + # A callback interface: pass any object that responds to the methods below + # wherever a Sampler is expected. Including this module documents the intent + # and supplies NotImplementedError defaults. The library may call the + # methods from any thread until it releases the implementation. + module Sampler + # The cutoff for a level, if the sampler has one + # + # @param level [Integer, nil] + # @return [Float, nil] + def threshold(level) + raise NotImplementedError, "#{self.class}#threshold is not implemented" end - end - # A pocket-sized interface exercising the object surface - class Gadget < WvObject - WV_PTR = GadgetPtr + # Up to `limit` samples, failing when the pantry is empty + # + # @param limit [Integer, nil] + # @param weights [Array] + # @return [Array] + # @raise [PantryError] reported to the library with its code and fields + def samples(limit, weights) + raise NotImplementedError, "#{self.class}#samples is not implemented" + end - # @api private - def self._wv_clone(ptr) - KitchenSink.kitchen_sink_kitchen_Gadget_clone(ptr) + # A free-form description, failing with an untyped error + # + # @return [String] + # @raise [StandardError] reported to the library with its message + def describe + raise NotImplementedError, "#{self.class}#describe is not implemented" end + end + # A pocket-sized interface exercising the object surface + class Gadget < Bridge::Handle # Create a gadget with the given id + # + # @param id [Integer] def initialize(id) - err = ErrorStruct.new - result = KitchenSink.kitchen_sink_kitchen_Gadget_new(id, err) - KitchenSink._wv_check!(err) - _wv_init(KitchenSink._wv_nonnull(result)) + id = Bridge.scalar(id, :i64) + ptr = Bridge.call(nil, :pointer) do |out_err| + Native.kitchen_sink_kitchen_Gadget_new(id, out_err) + end + _wv_init(ptr) end # Render the gadget as a human-readable string + # # @return [String] def describe - KitchenSink._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = KitchenSink.kitchen_sink_kitchen_Gadget_describe(_wv_self, out_len, err) - KitchenSink._wv_check!(err) - KitchenSink._wv_take_string(result, out_len.read(:size_t)) + Bridge.pin(self) do |self_| + Bridge.call(nil, :string) do |out_len, out_err| + Native.kitchen_sink_kitchen_Gadget_describe(self_, out_len, out_err) + end end end # Poke the gadget a number of times, failing on a negative count + # + # @param times [Integer] # @return [Integer] + # @raise [KitchenError] for a domain error def poke(times) - KitchenSink._wv_pin(self) do |_wv_self| - err = ErrorStruct.new - result = KitchenSink.kitchen_sink_kitchen_Gadget_poke(_wv_self, times, err) - KitchenSink._wv_check_kitchen_errors_error!(err) - result + times = Bridge.scalar(times, :i32) + Bridge.pin(self) do |self_| + Bridge.call(KitchenError, :i32) do |out_err| + Native.kitchen_sink_kitchen_Gadget_poke(self_, times, out_err) + end + end + end + + # The gadget's sensor readings, optionally limited + # + # @param limit [Integer, nil] + # @return [Array] + # @raise [Error] when the call fails + def readings(limit = nil) + has_limit, limit = Bridge.opt_arg(limit, :u32) + Bridge.pin(self) do |self_| + Bridge.call(Error, [:slice, :f64]) do |out_len, out_err| + Native.kitchen_sink_kitchen_Gadget_readings(self_, has_limit, limit, out_len, out_err) + end end end # The gadget subsystem version string + # # @return [String] def self.version - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = KitchenSink.kitchen_sink_kitchen_Gadget_version(out_len, err) - KitchenSink._wv_check!(err) - KitchenSink._wv_take_string(result, out_len.read(:size_t)) + Bridge.call(nil, :string) do |out_len, out_err| + Native.kitchen_sink_kitchen_Gadget_version(out_len, out_err) + end end end # Identity for bool + # + # @param x [Boolean] # @return [Boolean] def self.bool_id(x) - err = ErrorStruct.new - result = kitchen_sink_kitchen_bool_id((x ? true : false), err) - _wv_check!(err) - result + Bridge.call(nil, :bool) do |out_err| + Native.kitchen_sink_kitchen_bool_id((x ? true : false), out_err) + end end + # @param x [Integer] # @return [Integer] def self.i32_id(x) - err = ErrorStruct.new - result = kitchen_sink_kitchen_i32_id(x, err) - _wv_check!(err) - result + x = Bridge.scalar(x, :i32) + Bridge.call(nil, :i32) do |out_err| + Native.kitchen_sink_kitchen_i32_id(x, out_err) + end end + # @param x [Integer] # @return [Integer] def self.u32_id(x) - err = ErrorStruct.new - result = kitchen_sink_kitchen_u32_id(x, err) - _wv_check!(err) - result + x = Bridge.scalar(x, :u32) + Bridge.call(nil, :u32) do |out_err| + Native.kitchen_sink_kitchen_u32_id(x, out_err) + end end + # @param x [Integer] # @return [Integer] def self.i64_id(x) - err = ErrorStruct.new - result = kitchen_sink_kitchen_i64_id(x, err) - _wv_check!(err) - result + x = Bridge.scalar(x, :i64) + Bridge.call(nil, :i64) do |out_err| + Native.kitchen_sink_kitchen_i64_id(x, out_err) + end end + # @param x [Float] # @return [Float] def self.f64_id(x) - err = ErrorStruct.new - result = kitchen_sink_kitchen_f64_id(x, err) - _wv_check!(err) - result + Bridge.call(nil, :f64) do |out_err| + Native.kitchen_sink_kitchen_f64_id(x, out_err) + end end + # @param s [String] # @return [String] def self.echo_string(s) - s_s = _wv_str(s) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_echo_string(s_s, s_s.bytesize, out_len, err) - _wv_check!(err) - _wv_take_string(result, out_len.read(:size_t)) + s_ptr = Bridge.string_arg(s) + Bridge.call(nil, :string) do |out_len, out_err| + Native.kitchen_sink_kitchen_echo_string(s_ptr, s_ptr.bytesize, out_len, out_err) + end end + # @param b [String] # @return [String] def self.echo_bytes(b) - b_s = _wv_bytes(b) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_echo_bytes(b_s, b_s.bytesize, out_len, err) - _wv_check!(err) - _wv_take_bytes(result, out_len.read(:size_t)) + b_ptr = Bridge.bytes_arg(b) + Bridge.call(nil, :bytes) do |out_len, out_err| + Native.kitchen_sink_kitchen_echo_bytes(b_ptr, b_ptr.bytesize, out_len, out_err) + end end # Register a listener, returning how many are attached + # + # @param listener [ReadyListener] # @return [Integer] def self.subscribe(listener) - _wv_present!(listener, 'listener') - err = ErrorStruct.new - listener_ctx, listener_vtable = _wv_cb_register(listener, WV_READY_LISTENER_VTABLE) - result = kitchen_sink_kitchen_subscribe(listener_ctx, listener_vtable, err) - _wv_check!(err) - result + Bridge.present!(listener, 'listener') + Bridge.call(nil, :i32) do |out_err| + listener_ctx, listener_vtable = Bridge.register(listener, ReadyListener) + Native.kitchen_sink_kitchen_subscribe(listener_ctx, listener_vtable, out_err) + end end # Register a listener if one is given + # + # @param listener [ReadyListener, nil] # @return [Boolean] def self.maybe_subscribe(listener = nil) - err = ErrorStruct.new - listener_ctx, listener_vtable = _wv_cb_register(listener, WV_READY_LISTENER_VTABLE) - result = kitchen_sink_kitchen_maybe_subscribe(listener_ctx, listener_vtable, err) - _wv_check!(err) - result + Bridge.call(nil, :bool) do |out_err| + listener_ctx, listener_vtable = Bridge.register(listener, ReadyListener) + Native.kitchen_sink_kitchen_maybe_subscribe(listener_ctx, listener_vtable, out_err) + end end # Optional object in both directions + # + # @param fallback [Gadget, nil] # @return [Gadget, nil] def self.maybe_gadget(fallback = nil) - _wv_object!(fallback, Gadget, 'fallback', true) - _wv_pin(fallback) do |_wv_fallback| - err = ErrorStruct.new - result = kitchen_sink_kitchen_maybe_gadget(_wv_fallback, err) - _wv_check!(err) - result.null? ? nil : Gadget._from_ptr(result) + Bridge.check_object(fallback, Gadget, 'fallback', nullable: true) + Bridge.pin(fallback) do |fallback| + Bridge.call(nil, [:object?, Gadget]) do |out_err| + Native.kitchen_sink_kitchen_maybe_gadget(fallback, out_err) + end end end # A list of objects + # # @return [Array] def self.gadgets - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_gadgets(out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_list_gadget(r) } + Bridge.call(nil, [:buffer, [:list, Gadget]]) do |out_len, out_err| + Native.kitchen_sink_kitchen_gadgets(out_len, out_err) + end end # An iterator over objects - # Returns a lazy Enumerator that pulls one element per step. The - # producer iterator starts on the first pull and is released when - # iteration finishes or is abandoned early. + # + # Returns a lazy Enumerator: each enumeration starts its own native iterator + # on the first pull and releases it when iteration finishes or stops early. + # # @return [Enumerator] def self.stream_gadgets - Enumerator.new do |y| - err = ErrorStruct.new - iter = kitchen_sink_kitchen_stream_gadgets(err) - begin - _wv_check!(err) - out_item = FFI::MemoryPointer.new(:pointer) - loop do - has_item = kitchen_sink_kitchen_StreamGadgetsIterator_next(iter, out_item, err) - _wv_check!(err) - break if has_item.zero? - item = out_item.read_pointer - value = Gadget._from_ptr(_wv_nonnull(item)) - y << value - end - ensure - kitchen_sink_kitchen_StreamGadgetsIterator_destroy(iter) unless iter.null? - end + Bridge.iterate( + nil, + [:object, Gadget], + :kitchen_sink_kitchen_StreamGadgetsIterator_next, + :kitchen_sink_kitchen_StreamGadgetsIterator_destroy, + ) do |out_err| + Native.kitchen_sink_kitchen_stream_gadgets(out_err) end end # Optional struct return + # + # @param id [Integer] # @return [Item, nil] + # @raise [KitchenError] for a domain error def self.maybe_item(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_maybe_item(id, out_len, err) - _wv_check_kitchen_errors_error!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_opt_item(r) } + id = Bridge.scalar(id, :i64) + Bridge.call(KitchenError, [:buffer, [:opt, Item]]) do |out_len, out_err| + Native.kitchen_sink_kitchen_maybe_item(id, out_len, out_err) + end end # List of structs + # # @return [Array] def self.list_items - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_list_items(out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_list_item(r) } + Bridge.call(nil, [:buffer, [:list, Item]]) do |out_len, out_err| + Native.kitchen_sink_kitchen_list_items(out_len, out_err) + end end # Map return type + # # @return [Hash{String => Integer}] def self.get_attrs - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_get_attrs(out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_map_string_i32(r) } + Bridge.call(nil, [:buffer, [:map, :string, :i32]]) do |out_len, out_err| + Native.kitchen_sink_kitchen_get_attrs(out_len, out_err) + end end # Iterator return type - # Returns a lazy Enumerator that pulls one element per step. The - # producer iterator starts on the first pull and is released when - # iteration finishes or is abandoned early. + # + # Returns a lazy Enumerator: each enumeration starts its own native iterator + # on the first pull and releases it when iteration finishes or stops early. + # # @return [Enumerator] def self.stream_items - Enumerator.new do |y| - err = ErrorStruct.new - iter = kitchen_sink_kitchen_stream_items(err) - begin - _wv_check!(err) - out_item = FFI::MemoryPointer.new(:pointer) - out_len = FFI::MemoryPointer.new(:size_t) - loop do - has_item = kitchen_sink_kitchen_StreamItemsIterator_next(iter, out_item, out_len, err) - _wv_check!(err) - break if has_item.zero? - item = out_item.read_pointer - value = _wv_decode(item, out_len.read(:size_t)) { |r| _wv_read_item(r) } - y << value - end - ensure - kitchen_sink_kitchen_StreamItemsIterator_destroy(iter) unless iter.null? - end + Bridge.iterate( + nil, + [:buffer, Item], + :kitchen_sink_kitchen_StreamItemsIterator_next, + :kitchen_sink_kitchen_StreamItemsIterator_destroy, + ) do |out_err| + Native.kitchen_sink_kitchen_stream_items(out_err) end end # Returns the shared Token type from another module + # # @return [Token] def self.cross_module_token - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_cross_module_token(out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_token(r) } + Bridge.call(nil, [:buffer, Token]) do |out_len, out_err| + Native.kitchen_sink_kitchen_cross_module_token(out_len, out_err) + end end # Async operation - # Blocks until the call completes on a producer thread. + # + # Blocks until the call completes on a library thread (under a Fiber + # scheduler, only the calling Fiber waits). + # + # @param input [String] # @return [String] def self.do_async(input) - input_s = _wv_str(input) - ctx, queue = _wv_async_begin - kitchen_sink_kitchen_do_async(input_s, input_s.bytesize, KITCHEN_SINK_KITCHEN_DO_ASYNC_CALLBACK, ctx) - _wv_async_wait(queue) + input_ptr = Bridge.string_arg(input) + Bridge.await(nil, :string) do |callback, context| + Native.kitchen_sink_kitchen_do_async(input_ptr, input_ptr.bytesize, callback, context) + end end # Cancellable async operation - # Blocks until the call completes on a producer thread. - # @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled + # + # Blocks until the call completes on a library thread (under a Fiber + # scheduler, only the calling Fiber waits). + # + # @param input [String] + # @param cancel [CancelToken, nil] cancels the call, which then raises Cancelled # @return [String] + # @raise [Cancelled] when `cancel` fires first def self.do_cancellable(input, cancel: nil) - input_s = _wv_str(input) - own_token = CancelToken.new if cancel.nil? - token = cancel || own_token - ctx, queue = _wv_async_begin - kitchen_sink_kitchen_do_cancellable(input_s, input_s.bytesize, token._wv_ptr, KITCHEN_SINK_KITCHEN_DO_CANCELLABLE_CALLBACK, ctx) - _wv_async_wait(queue, token) - ensure - own_token&.close + input_ptr = Bridge.string_arg(input) + Bridge.await_cancellable(nil, :string, cancel) do |cancel_token, callback, context| + Native.kitchen_sink_kitchen_do_cancellable( + input_ptr, + input_ptr.bytesize, + cancel_token, + callback, + context, + ) + end end # Legacy operation kept for compatibility + # # @return [Integer] - # @deprecated Use new_op instead + # @deprecated Use `new_op` instead def self.legacy_op - warn('KitchenSink.legacy_op is deprecated: Use new_op instead', uplevel: 1) - err = ErrorStruct.new - result = kitchen_sink_kitchen_legacy_op(err) - _wv_check!(err) - result + warn('KitchenSink.legacy_op is deprecated: Use `new_op` instead', uplevel: 1, category: :deprecated) + Bridge.call(nil, :i32) do |out_err| + Native.kitchen_sink_kitchen_legacy_op(out_err) + end end - # Replacement for legacy_op + # Replacement for `legacy_op` + # # @return [Integer] def self.new_op - err = ErrorStruct.new - result = kitchen_sink_kitchen_new_op(err) - _wv_check!(err) - result + Bridge.call(nil, :i32) do |out_err| + Native.kitchen_sink_kitchen_new_op(out_err) + end end - # === Module: kitchen.nested === - - attach_function :kitchen_sink_kitchen_nested_hello, [:pointer, :pointer], :pointer, blocking: true - - # Trivial nested-module function - # @return [String] - def self.hello - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = kitchen_sink_kitchen_nested_hello(out_len, err) - _wv_check!(err) - _wv_take_string(result, out_len.read(:size_t)) + # Optional scalar in both directions + # + # @param x [Integer, nil] + # @return [Integer, nil] + def self.opt_i32_id(x = nil) + has_x, x = Bridge.opt_arg(x, :i32) + Bridge.call(nil, [:opt, :i32]) do |out_value, out_err| + Native.kitchen_sink_kitchen_opt_i32_id(has_x, x, out_value, out_err) + end end - # === Value-buffer codecs of the composite types === - - # @api private - # Packs `[Gadget]` into the value-buffer wire format. - def self._wv_write_list_gadget(w, v) - w.write_len(v.length) - v.each { |e| w.write_object(e, Gadget) } + # @param x [Boolean, nil] + # @return [Boolean, nil] + def self.opt_bool_id(x = nil) + has_x, x = Bridge.opt_arg(x, :bool) + Bridge.call(nil, [:opt, :bool]) do |out_value, out_err| + Native.kitchen_sink_kitchen_opt_bool_id(has_x, x, out_value, out_err) + end end - # @api private - # Unpacks `[Gadget]` from the value-buffer wire format. - def self._wv_read_list_gadget(r) - Array.new(r.read_len) { r.read_object(Gadget) } + # Optional C-style enum in both directions + # + # @param p [Integer, nil] + # @return [Integer, nil] + def self.opt_priority(p = nil) + has_p, p = Bridge.opt_arg(p, :i32) + Bridge.call(nil, [:opt, :i32]) do |out_value, out_err| + Native.kitchen_sink_kitchen_opt_priority(has_p, p, out_value, out_err) + end end - # @api private - # Packs `Item?` into the value-buffer wire format. - def self._wv_write_opt_item(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - _wv_write_item(w, v) + # Optional scalars next to ordinary parameters + # + # @param base [Float] + # @param scale [Float, nil] + # @param offset [Integer, nil] + # @param label [String, nil] + # @return [Integer, nil] + def self.opt_mixed(base, scale = nil, offset = nil, label = nil) + has_scale, scale = Bridge.opt_arg(scale, :f32) + has_offset, offset = Bridge.opt_arg(offset, :i64) + label_ptr = Bridge.encode([:opt, :string], label) + Bridge.call(nil, [:opt, :u16], label_ptr) do |label_ptr, out_value, out_err| + Native.kitchen_sink_kitchen_opt_mixed( + base, + has_scale, + scale, + has_offset, + offset, + label_ptr, + label_ptr.bytesize, + out_value, + out_err, + ) end end - # @api private - # Unpacks `Item?` from the value-buffer wire format. - def self._wv_read_opt_item(r) - r.read_flag ? _wv_read_item(r) : nil + # A typed-array parameter + # + # @param xs [Array] + # @return [Float] + def self.sum_f64(xs) + xs_ptr, xs_len = Bridge.slice_arg(xs, :f64) + Bridge.call(nil, :f64) do |out_err| + Native.kitchen_sink_kitchen_sum_f64(xs_ptr, xs_len, out_err) + end end - # @api private - # Packs `[Item]` into the value-buffer wire format. - def self._wv_write_list_item(w, v) - w.write_len(v.length) - v.each { |e| _wv_write_item(w, e) } + # Typed arrays in both directions + # + # @param xs [Array] + # @param factor [Integer] + # @return [Array] + def self.scale_i32(xs, factor) + xs_ptr, xs_len = Bridge.slice_arg(xs, :i32) + factor = Bridge.scalar(factor, :i32) + Bridge.call(nil, [:slice, :i32]) do |out_len, out_err| + Native.kitchen_sink_kitchen_scale_i32(xs_ptr, xs_len, factor, out_len, out_err) + end end - # @api private - # Unpacks `[Item]` from the value-buffer wire format. - def self._wv_read_list_item(r) - Array.new(r.read_len) { _wv_read_item(r) } + # @param ids [Array] + # @return [Array] + def self.ids_u64(ids) + ids_ptr, ids_len = Bridge.slice_arg(ids, :u64) + Bridge.call(nil, [:slice, :u64]) do |out_len, out_err| + Native.kitchen_sink_kitchen_ids_u64(ids_ptr, ids_len, out_len, out_err) + end end - # @api private - # Packs `{string:i32}` into the value-buffer wire format. - def self._wv_write_map_string_i32(w, v) - w.write_len(v.length) - v.each do |k, e| - w.write_string(k) - w.write_i32(e) + # Every slice element type + # + # @param a [Array] + # @param b [Array] + # @param c [Array] + # @param d [Array] + # @param e [Array] + # @param f [Array] + # @return [Array] + def self.mixed_slices(a, b, c, d, e, f) + a_ptr, a_len = Bridge.slice_arg(a, :i8) + b_ptr, b_len = Bridge.slice_arg(b, :i16) + c_ptr, c_len = Bridge.slice_arg(c, :u16) + d_ptr, d_len = Bridge.slice_arg(d, :u32) + e_ptr, e_len = Bridge.slice_arg(e, :i64) + f_ptr, f_len = Bridge.slice_arg(f, :f32) + Bridge.call(nil, [:slice, :u32]) do |out_len, out_err| + Native.kitchen_sink_kitchen_mixed_slices( + a_ptr, + a_len, + b_ptr, + b_len, + c_ptr, + c_len, + d_ptr, + d_len, + e_ptr, + e_len, + f_ptr, + f_len, + out_len, + out_err, + ) end end - # @api private - # Unpacks `{string:i32}` from the value-buffer wire format. - def self._wv_read_map_string_i32(r) - Array.new(r.read_len) { [r.read_string, r.read_i32] }.to_h + # Async returning an optional scalar + # + # Blocks until the call completes on a library thread (under a Fiber + # scheduler, only the calling Fiber waits). + # + # @return [Integer, nil] + def self.async_maybe_count + Bridge.await(nil, [:opt, :i64]) do |callback, context| + Native.kitchen_sink_kitchen_async_maybe_count(callback, context) + end end - # @api private - # Packs `[string]` into the value-buffer wire format. - def self._wv_write_list_string(w, v) - w.write_len(v.length) - v.each { |e| w.write_string(e) } + # Async returning a typed array + # + # Blocks until the call completes on a library thread (under a Fiber + # scheduler, only the calling Fiber waits). + # + # @param n [Integer] + # @param cancel [CancelToken, nil] cancels the call, which then raises Cancelled + # @return [Array] + # @raise [Cancelled] when `cancel` fires first + def self.async_samples(n, cancel: nil) + n = Bridge.scalar(n, :u32) + Bridge.await_cancellable(nil, [:slice, :f32], cancel) do |cancel_token, callback, context| + Native.kitchen_sink_kitchen_async_samples(n, cancel_token, callback, context) + end end - # @api private - # Unpacks `[string]` from the value-buffer wire format. - def self._wv_read_list_string(r) - Array.new(r.read_len) { r.read_string } + # Iterator of optional scalars + # + # Returns a lazy Enumerator: each enumeration starts its own native iterator + # on the first pull and releases it when iteration finishes or stops early. + # + # @return [Enumerator] + def self.stream_maybe + Bridge.iterate( + nil, + [:opt, :i32], + :kitchen_sink_kitchen_StreamMaybeIterator_next, + :kitchen_sink_kitchen_StreamMaybeIterator_destroy, + ) do |out_err| + Native.kitchen_sink_kitchen_stream_maybe(out_err) + end end - # @api private - # Packs `{string:string}` into the value-buffer wire format. - def self._wv_write_map_string_string(w, v) - w.write_len(v.length) - v.each do |k, e| - w.write_string(k) - w.write_string(e) + # Iterator of typed arrays + # + # Returns a lazy Enumerator: each enumeration starts its own native iterator + # on the first pull and releases it when iteration finishes or stops early. + # + # @return [Enumerator>] + def self.stream_chunks + Bridge.iterate( + nil, + [:slice, :i32], + :kitchen_sink_kitchen_StreamChunksIterator_next, + :kitchen_sink_kitchen_StreamChunksIterator_destroy, + ) do |out_err| + Native.kitchen_sink_kitchen_stream_chunks(out_err) end end - # @api private - # Unpacks `{string:string}` from the value-buffer wire format. - def self._wv_read_map_string_string(r) - Array.new(r.read_len) { [r.read_string, r.read_string] }.to_h + # Drive a consumer-implemented sampler + # + # @param sampler [Sampler] + # @param level [Integer, nil] + # @return [Array] + # @raise [PantryError] for a domain error + def self.sample_with(sampler, level = nil) + Bridge.present!(sampler, 'sampler') + has_level, level = Bridge.opt_arg(level, :u32) + Bridge.call(PantryError, [:slice, :i16]) do |out_len, out_err| + sampler_ctx, sampler_vtable = Bridge.register(sampler, Sampler) + Native.kitchen_sink_kitchen_sample_with( + sampler_ctx, + sampler_vtable, + has_level, + level, + out_len, + out_err, + ) + end end - # @api private - # Packs `i64?` into the value-buffer wire format. - def self._wv_write_opt_i64(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_i64(v) + # Fails with the second domain + # + # @param item [String] + # @return [Integer] + # @raise [PantryError] for a domain error + def self.take_from_pantry(item) + item_ptr = Bridge.string_arg(item) + Bridge.call(PantryError, :i32) do |out_err| + Native.kitchen_sink_kitchen_take_from_pantry(item_ptr, item_ptr.bytesize, out_err) end end - # @api private - # Unpacks `i64?` from the value-buffer wire format. - def self._wv_read_opt_i64(r) - r.read_flag ? r.read_i64 : nil + # Fails with an untyped error + # + # @param text [String] + # @return [Integer] + # @raise [Error] when the call fails + def self.parse_any(text) + text_ptr = Bridge.string_arg(text) + Bridge.call(Error, :i64) do |out_err| + Native.kitchen_sink_kitchen_parse_any(text_ptr, text_ptr.bytesize, out_err) + end end - # @api private - # Packs `Gadget?` into the value-buffer wire format. - def self._wv_write_opt_gadget(w, v) - if v.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_object(v, Gadget) + # === Module: kitchen.nested === + + # Trivial nested-module function + # + # @return [String] + def self.hello + Bridge.call(nil, :string) do |out_len, out_err| + Native.kitchen_sink_kitchen_nested_hello(out_len, out_err) end end - # @api private - # Unpacks `Gadget?` from the value-buffer wire format. - def self._wv_read_opt_gadget(r) - r.read_flag ? r.read_object(Gadget) : nil - end + # === Private registrations === + + # How the library clones and releases each interface's objects, and how + # each record, variant, and error payload is laid out in a value buffer. + Bridge.interface(Gadget, :kitchen_sink_kitchen_Gadget_clone, :kitchen_sink_kitchen_Gadget_destroy) + Bridge.record(Token, id: :i64, label: :string) + Bridge.record( + Item, + id: :i64, + name: :string, + count: :i32, + enabled: :bool, + ratio: :f64, + nick: :u32, + payload: :bytes, + tags: [:list, :string], + attrs: [:map, :string, :string], + parent: [:opt, :i64], + token: Token, + priority: :i32, + gadget: [:opt, Gadget], + ) + Bridge.record(PantryError::OutOfStock, item: :string) + + # The ReadyListener vtable. + Bridge.vtable( + ReadyListener, + # ReadyListener#on_ready + FFI::Function.new(:void, [:pointer, :int32, :pointer, :size_t, :pointer]) do |ctx, code, msg_ptr, msg_len, out_err| + Bridge.callback(out_err, nil) do + msg = Bridge.borrow_string(msg_ptr, msg_len) + Bridge.impl(ctx).on_ready(code, msg) + nil + end + end, + # ReadyListener#on_item + FFI::Function.new(:bool, [:pointer, :pointer, :size_t, :pointer, :pointer]) do |ctx, item_ptr, item_len, gadget, out_err| + Bridge.callback(out_err, nil, false) do + gadget = Bridge.adopt(Gadget, gadget) + item = Bridge.decode_borrowed(item_ptr, item_len, Item) + Bridge.impl(ctx).on_item(item, gadget) ? true : false + end + end, + # ReadyListener#label + FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer]) do |ctx, out_ptr, out_len, out_err| + Bridge.callback(out_err, KitchenError) do + Bridge.string_return(Bridge.impl(ctx).label, out_ptr, out_len) + end + end, + # ReadyListener#latest + FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer]) do |ctx, out_ptr, out_len, out_err| + Bridge.callback(out_err, nil) do + Bridge.buffer_return([:opt, Item], Bridge.impl(ctx).latest, out_ptr, out_len) + end + end, + # ReadyListener#favorite + FFI::Function.new(:pointer, [:pointer, :pointer]) do |ctx, out_err| + Bridge.callback(out_err, nil) do + Bridge.object_return(Bridge.impl(ctx).favorite, Gadget) + end + end, + ) + + # The Sampler vtable. + Bridge.vtable( + Sampler, + # Sampler#threshold + FFI::Function.new(:bool, [:pointer, :bool, :uint32, :pointer, :pointer]) do |ctx, has_level, level, out_value, out_err| + Bridge.callback(out_err, nil, false) do + level = has_level ? level : nil + Bridge.opt_return(Bridge.impl(ctx).threshold(level), :f64, out_value) + end + end, + # Sampler#samples + FFI::Function.new(:void, [:pointer, :bool, :uint32, :pointer, :size_t, :pointer, :pointer, :pointer]) do |ctx, has_limit, limit, weights_ptr, weights_len, out_ptr, out_len, out_err| + Bridge.callback(out_err, PantryError) do + limit = has_limit ? limit : nil + weights = Bridge.borrow_slice(weights_ptr, weights_len, :f64) + Bridge.slice_return(Bridge.impl(ctx).samples(limit, weights), :i16, out_ptr, out_len) + end + end, + # Sampler#describe + FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer]) do |ctx, out_ptr, out_len, out_err| + Bridge.callback(out_err, Error) do + Bridge.string_return(Bridge.impl(ctx).describe, out_ptr, out_len) + end + end, + ) end # END kitchen_sink.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_runtime_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_runtime_rb.snap deleted file mode 100644 index e1a525a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_runtime_rb.snap +++ /dev/null @@ -1,900 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -# frozen_string_literal: true - -require 'ffi' -require 'monitor' - -# The fixed runtime every generated KitchenSink binding builds on: the native -# library loader, the load-time ABI and contract checks, the error classes, -# the value-buffer reader and writer, cancel tokens, the handle table behind -# callback interfaces and async completions, and the object wrapper base. -module KitchenSink - extend FFI::Library - - # An explicit path in KITCHEN_SINK_LIBRARY wins. Otherwise a library bundled - # under lib/native/ (a platform gem) is preferred over the system search - # path. - _wv_override = ENV['KITCHEN_SINK_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - _wv_name = - case FFI::Platform::OS - when /darwin/ then 'libkitchen_sink.dylib' - when /mswin|mingw|windows/ then 'kitchen_sink.dll' - else 'libkitchen_sink.so' - end - _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) - ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) - end - - # The C ABI revision these bindings speak. Checked before anything else is - # attached, so a mismatched library fails at require time instead of - # misreading an error struct or a value buffer later. - ABI_VERSION = 4 - begin - attach_function :kitchen_sink_abi_version, [], :uint32 - rescue FFI::NotFoundError - raise LoadError, "the loaded kitchen_sink library does not export kitchen_sink_abi_version " \ - "(these bindings expect ABI revision #{ABI_VERSION})" - end - _wv_abi = kitchen_sink_abi_version - unless _wv_abi == ABI_VERSION - raise LoadError, "kitchen_sink ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ - "but the loaded library reports revision #{_wv_abi}" - end - - # The `kitchen_sink_error` out-parameter every call reports through. - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - # Codes reserved by the runtime. Positive codes belong to a module's error - # domain; these negative codes report runtime failures. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - # The call was cancelled through its CancelToken. - CANCELLED_ERROR_CODE = -5 - - # The base class of every error the library raises. - class Error < StandardError - # The ABI error code: positive for a domain error, negative for a runtime - # failure. - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # Raised when a call that declares no errors fails anyway. That's a bug in - # the library (a panic, an argument it couldn't accept, a malformed value - # it returned, a callback failure it let through), never an outcome to - # handle; the message names the code and the library's message. - class NativeBugError < Error - def initialize(code, message) - super(code, "native call failed with code #{code}: #{message}") - end - end - - # Raised when a cancellable call completes because its CancelToken fired. - class Cancelled < Error - def initialize(message = 'cancelled') - super(CANCELLED_ERROR_CODE, message) - end - end - - attach_function :kitchen_sink_error_set, [:pointer, :int32, :string], :void - attach_function :kitchen_sink_error_set_payload, [:pointer, :pointer, :size_t], :void - attach_function :kitchen_sink_error_clear, [:pointer], :void - attach_function :kitchen_sink_error_free, [:pointer], :void - attach_function :kitchen_sink_alloc, [:size_t], :pointer - attach_function :kitchen_sink_free_bytes, [:pointer, :size_t], :void - attach_function :kitchen_sink_cancel_token_create, [], :pointer - attach_function :kitchen_sink_cancel_token_cancel, [:pointer], :void - attach_function :kitchen_sink_cancel_token_is_cancelled, [:pointer], :bool - attach_function :kitchen_sink_cancel_token_destroy, [:pointer], :void - - # @api private - # One entry of a module's contract table. - class ContractEntry < FFI::Struct - layout :id, :uint64, - :hash, :uint64 - end - - # @api private - # Checks, for every top-level module, that the library's contract table - # (`kitchen_sink_{module}_contract`) holds each declaration these bindings - # were generated with, as `[id, hash, path]`, with an equal signature - # hash. Declarations the library adds are fine. - def self._wv_check_contract!(tables) - tables.each do |symbol, expected| - begin - attach_function symbol, [:pointer], :pointer unless respond_to?(symbol) - rescue FFI::NotFoundError - raise LoadError, "the loaded kitchen_sink library does not export #{symbol}" - end - out_len = FFI::MemoryPointer.new(:size_t) - table = public_send(symbol, out_len) - count = table.null? ? 0 : out_len.read(:size_t) - actual = Array.new(count) do |i| - entry = ContractEntry.new(table + (i * ContractEntry.size)) - [entry[:id], entry[:hash]] - end.to_h - expected.each do |id, hash, path| - found = actual[id] - raise LoadError, "kitchen_sink: #{path} is missing from the library" if found.nil? - next if found == hash - - raise LoadError, "kitchen_sink: #{path} changed since these bindings were generated" - end - end - end - - # @api private - # The error a throwing call raises for a code outside its domain: - # Cancelled for -5, otherwise the root Error. - def self._wv_error(code, message, _payload = nil) - return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE - - Error.new(code, message) - end - - # @api private - # The error a call that declares no errors raises when it fails anyway: - # Cancelled for -5, otherwise NativeBugError. - def self._wv_trap(code, message, _payload = nil) - return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE - - NativeBugError.new(code, message) - end - - # @api private - # The error for a malformed value the library handed over. - def self._wv_malformed(what) - NativeBugError.new(MARSHAL_ERROR_CODE, "malformed value buffer: #{what}") - end - - # @api private - # Copies the code, message, and payload out of a reported error and clears - # it. Returns nil when the struct reports success. - def self._wv_take_error(err) - code = err[:code] - return nil if code.zero? - - msg_ptr = err[:message] - message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - kitchen_sink_error_clear(err.to_ptr) - [code, message, payload] - end - - # @api private - # Like _wv_take_error for the heap-boxed error an async completion - # receives; the box is released with kitchen_sink_error_free. - def self._wv_take_boxed_error(err_ptr) - return nil if err_ptr.null? - - taken = _wv_take_error(ErrorStruct.new(err_ptr)) - kitchen_sink_error_free(err_ptr) - taken - end - - # @api private - # Raises NativeBugError for a failed call that declares no errors. - def self._wv_check!(err) - taken = _wv_take_error(err) - raise _wv_trap(*taken) unless taken.nil? - end - - # @api private - # The UTF-8 bytes of a String argument, as a private binary copy the - # library borrows for the call. - def self._wv_str(value) - str = String.try_convert(value) - raise TypeError, "expected a String, got #{value.class}" if str.nil? - - utf8 = str.encode(Encoding::UTF_8) - raise ArgumentError, 'string is not valid UTF-8' unless utf8.valid_encoding? - - utf8.b - end - - # @api private - # A private binary copy of a bytes argument. - def self._wv_bytes(value) - str = String.try_convert(value) - raise TypeError, "expected a String of bytes, got #{value.class}" if str.nil? - - str.b - end - - # The inclusive range of each fixed-width integer type. - INT_RANGES = { - i8: -0x80..0x7f, - u8: 0..0xff, - i16: -0x8000..0x7fff, - u16: 0..0xffff, - i32: -0x8000_0000..0x7fff_ffff, - u32: 0..0xffff_ffff, - i64: -0x8000_0000_0000_0000..0x7fff_ffff_ffff_ffff, - u64: 0..0xffff_ffff_ffff_ffff - }.freeze - - # @api private - # `value`, checked to be an Integer in the range of `kind` (`:i32`, ...): - # out-of-range values raise RangeError rather than wrap. - def self._wv_int(value, kind) - raise TypeError, "expected an Integer for #{kind}, got #{value.class}" unless value.is_a?(Integer) - raise RangeError, "#{value} is out of range for #{kind}" unless INT_RANGES[kind].cover?(value) - - value - end - - # @api private - # Raises TypeError unless `value` is a `cls` wrapper (or nil, when - # `optional`), so an object of the wrong interface never reaches the - # library. - def self._wv_object!(value, cls, name, optional = false) - return if value.is_a?(cls) || (optional && value.nil?) - - expected = optional ? "a #{cls.name} or nil" : "a #{cls.name}" - raise TypeError, "#{name} must be #{expected}, got #{value.class}" - end - - # @api private - # Raises TypeError for a nil callback implementation where one is required. - def self._wv_present!(value, name) - raise TypeError, "#{name} must not be nil" if value.nil? - end - - # @api private - # Copies a borrowed (ptr, len) run without releasing it. - def self._wv_borrow_bytes(ptr, len) - ptr.null? || len.zero? ? ''.b : ptr.read_string(len) - end - - # @api private - # Copies a borrowed (ptr, len) UTF-8 run without releasing it. - def self._wv_borrow_string(ptr, len) - _wv_borrow_bytes(ptr, len).force_encoding(Encoding::UTF_8) - end - - # @api private - # Copies a returned (ptr, len) allocation, then releases it. - def self._wv_take_bytes(ptr, len) - return ''.b if ptr.null? - - data = len.zero? ? ''.b : ptr.read_string(len) - kitchen_sink_free_bytes(ptr, len) - data - end - - # @api private - # Copies a returned UTF-8 string, then releases it. - def self._wv_take_string(ptr, len) - _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) - end - - # A cancellation signal for a cancellable async call. Pass one as the - # `cancel:` keyword, then call #cancel from any thread; the call raises - # Cancelled unless it already completed. A token may be shared by several - # calls and stays cancelled once cancelled. - class CancelToken - # @api private - # Owns the consumer's reference to the native token. - class Ptr < FFI::AutoPointer - def self.release(ptr) - KitchenSink.kitchen_sink_cancel_token_destroy(ptr) - end - end - - def initialize - @ptr = Ptr.new(KitchenSink.kitchen_sink_cancel_token_create) - end - - # Requests cancellation. Idempotent. - def cancel - KitchenSink.kitchen_sink_cancel_token_cancel(@ptr) unless @ptr.nil? - nil - end - - # Whether #cancel has been called. - def cancelled? - !@ptr.nil? && KitchenSink.kitchen_sink_cancel_token_is_cancelled(@ptr) - end - - # Releases the native token now rather than at GC time. Calls already - # launched with it keep their own reference. - def close - return if @ptr.nil? - - @ptr.free - @ptr = nil - end - - # @api private - def _wv_ptr - raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? - - @ptr - end - end - - # @api private - # Appends values in the value-buffer wire format: little-endian, packed, - # no alignment. Integers out of their type's range raise RangeError rather - # than wrap. - class WvBufferWriter - def initialize - @buf = +''.b - @objects = [] - end - - # Reserves an object token for a `cls` wrapper. The strong reference it - # carries is minted by #seal, just before the buffer is handed to the - # library, so an encoding abandoned on the way never strands a - # reference. - def write_object(object, cls) - raise TypeError, "expected a #{cls.name}, got #{object.class}" unless object.is_a?(cls) - - @objects << [@buf.bytesize, object] - @buf << [0].pack('Q<') - end - - # Mints the reference each reserved object token carries, recording it - # in `minted`, and returns the finished encoding. - def seal(minted) - @objects.each do |offset, object| - ptr = object._wv_clone_ptr - minted << [object.class, ptr] - @buf[offset, 8] = [ptr.address].pack('Q<') - end - @buf - end - - # The finished encoding of a buffer that carries no objects. - def bytes - raise TypeError, 'this encoding cannot carry objects' unless @objects.empty? - - @buf - end - - def write_bool(value) - @buf << (value ? "\x01" : "\x00").b - end - - def write_flag(value) - write_bool(value) - end - - def write_i8(value) - @buf << [int(value, :i8)].pack('c') - end - - def write_u8(value) - @buf << [int(value, :u8)].pack('C') - end - - def write_i16(value) - @buf << [int(value, :i16)].pack('s<') - end - - def write_u16(value) - @buf << [int(value, :u16)].pack('S<') - end - - def write_i32(value) - @buf << [int(value, :i32)].pack('l<') - end - - def write_u32(value) - @buf << [int(value, :u32)].pack('L<') - end - - def write_len(value) - write_u32(value) - end - - def write_i64(value) - @buf << [int(value, :i64)].pack('q<') - end - - def write_u64(value) - @buf << [int(value, :u64)].pack('Q<') - end - - def write_f32(value) - @buf << [value].pack('e') - end - - def write_f64(value) - @buf << [value].pack('E') - end - - def write_string(value) - write_bytes(KitchenSink._wv_str(value)) - end - - def write_bytes(value) - bytes = KitchenSink._wv_bytes(value) - write_u32(bytes.bytesize) - @buf << bytes - end - - private - - def int(value, kind) - KitchenSink._wv_int(value, kind) - end - end - - # @api private - # Reads values in the value-buffer wire format, raising NativeBugError on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(count, what) - raise KitchenSink._wv_malformed("truncated #{what}") if @pos + count > @data.bytesize - - bytes = @data.byteslice(@pos, count) - @pos += count - bytes - end - - def read_bool - byte = take(1, 'bool').unpack1('C') - raise KitchenSink._wv_malformed('bool byte out of range') if byte > 1 - - byte == 1 - end - - def read_flag - byte = take(1, 'option flag').unpack1('C') - raise KitchenSink._wv_malformed('option flag out of range') if byte > 1 - - byte == 1 - end - - def read_i8 - take(1, 'i8').unpack1('c') - end - - def read_u8 - take(1, 'u8').unpack1('C') - end - - def read_i16 - take(2, 'i16').unpack1('s<') - end - - def read_u16 - take(2, 'u16').unpack1('S<') - end - - def read_i32 - take(4, 'i32').unpack1('l<') - end - - def read_u32 - take(4, 'u32').unpack1('L<') - end - - # A u32 element count. Counts are not checked against the remaining - # bytes, since an element can encode to zero bytes; a short buffer still - # fails on the first truncated read. - def read_len - read_u32 - end - - def read_i64 - take(8, 'i64').unpack1('q<') - end - - def read_u64 - take(8, 'u64').unpack1('Q<') - end - - def read_f32 - take(4, 'f32').unpack1('e') - end - - def read_f64 - take(8, 'f64').unpack1('E') - end - - def read_string - text = read_bytes.force_encoding(Encoding::UTF_8) - raise KitchenSink._wv_malformed('string is not valid UTF-8') unless text.valid_encoding? - - text - end - - def read_bytes - take(read_u32, 'byte run') - end - - # An object token: a non-zero u64 carrying one strong reference, adopted - # into a new `cls` wrapper. - def read_object(cls) - addr = read_u64 - raise KitchenSink._wv_malformed('null object token') if addr.zero? - - cls._from_ptr(FFI::Pointer.new(addr)) - end - - def expect_end! - return if @pos == @data.bytesize - - raise KitchenSink._wv_malformed('trailing bytes after value') - end - end - - # @api private - # Seals the writers of one call's object-carrying buffers (see - # WvBufferWriter#seal) and returns their encodings. If any reference can't - # be minted (a closed wrapper), the ones already minted are released before - # the error propagates. The library adopts every minted reference once the - # call is made. - def self._wv_seal(*writers) - minted = [] - writers.map { |writer| writer.seal(minted) } - rescue Exception # rubocop:disable Lint/RescueException - minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } - raise - end - - # @api private - # Encodes an argument that carries no objects: yields a writer and - # returns the finished encoding. - def self._wv_encode - writer = WvBufferWriter.new - yield writer - writer.bytes - end - - # @api private - # A returned object pointer that must not be NULL. - def self._wv_nonnull(ptr) - raise NativeBugError.new(MARSHAL_ERROR_CODE, 'null object pointer') if ptr.null? - - ptr - end - - # @api private - # Decodes a returned value buffer: copies and releases it, yields a reader - # to the block, and checks that the block consumed every byte. - def self._wv_decode(ptr, len) - reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) - value = yield reader - reader.expect_end! - value - end - - # @api private - # Decodes a borrowed value buffer (a callback argument) without releasing - # it. - def self._wv_decode_borrowed(ptr, len) - reader = WvBufferReader.new(_wv_borrow_bytes(ptr, len)) - value = yield reader - reader.expect_end! - value - end - - # Ruby objects the library refers to by an integer key: callback-interface - # implementations (the key is the vtable `ctx`) and pending async - # completions (the key is the completion `context`). The library never - # sees a Ruby object address. A Monitor rather than a Mutex, so a - # finalizer that runs while this thread holds the lock can't deadlock it. - @wv_handles = {} - @wv_next_handle = 0 - @wv_handles_lock = Monitor.new - - # @api private - def self._wv_handle_put(value) - @wv_handles_lock.synchronize do - @wv_next_handle += 1 - @wv_handles[@wv_next_handle] = value - FFI::Pointer.new(@wv_next_handle) - end - end - - # @api private - def self._wv_handle_get(key) - @wv_handles_lock.synchronize { @wv_handles[key.address] } - end - - # @api private - def self._wv_handle_take(key) - @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } - end - - # @api private - # Registers a callback-interface implementation and returns its `ctx` and - # `vtable` arguments: two NULLs for a nil (optional) implementation. Never - # raises, so nothing can strand the registration between this and the - # call that hands it to the library (an unusable implementation fails - # when the library calls it, with FOREIGN_ERROR_CODE). - def self._wv_cb_register(impl, vtable) - return [nil, nil] if impl.nil? - - [_wv_handle_put([:callback, impl]), vtable.to_ptr] - end - - # @api private - # Resolves a trampoline's `ctx` back to the registered implementation. - def self._wv_cb_lookup(ctx) - kind, impl = _wv_handle_get(ctx) - raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback - - impl - end - - # @api private - # Drops an implementation when the library releases its last reference. - def self._wv_cb_free(ctx) - _wv_handle_take(ctx) - nil - end - - # @api private - # The message a callback failure reports: the exception's message (its - # class name when that's empty), as UTF-8 without NULs. - def self._wv_cb_message(exception) - message = exception.message.to_s - message = exception.class.name.to_s if message.empty? - message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") - end - - # @api private - # Reports a callback implementation's exception through `out_err` as - # FOREIGN_ERROR_CODE. The library copies the message. - def self._wv_cb_fail(out_err, exception) - kitchen_sink_error_set(out_err, FOREIGN_ERROR_CODE, _wv_cb_message(exception)) - nil - end - - # @api private - # Reports a domain error raised by a callback method declared to throw: - # the block returns its `[code, payload]`, or nil when it isn't one of the - # domain's codes, which then fails as FOREIGN_ERROR_CODE like any other - # exception. - def self._wv_cb_throw(out_err, exception) - code, payload = yield - return _wv_cb_fail(out_err, exception) if code.nil? - - kitchen_sink_error_set(out_err, code, _wv_cb_message(exception)) - kitchen_sink_error_set_payload(out_err, payload, payload.bytesize) unless payload.empty? - nil - rescue Exception => e # rubocop:disable Lint/RescueException - _wv_cb_fail(out_err, e) - end - - # @api private - # A callback method's floating-point return, checked to be numeric. - def self._wv_float(value) - raise TypeError, "expected a Float, got #{value.class}" unless value.is_a?(Numeric) - - value.to_f - end - - # @api private - # Hands a callback method's string, bytes, or buffer return to the - # library: a run allocated with kitchen_sink_alloc, written to the out slots. - # The library adopts and frees it. - def self._wv_cb_return_bytes(out_ptr, out_len, data) - len = data.bytesize - run = FFI::Pointer::NULL - unless len.zero? - run = kitchen_sink_alloc(len) - raise NoMemoryError, 'kitchen_sink_alloc failed' if run.null? - - run.put_bytes(0, data) - end - out_ptr.write_pointer(run) - out_len.write(:size_t, len) - nil - end - - # @api private - # A callback method's object return: a fresh strong reference the library - # adopts, or NULL for nil (which the library rejects where the return - # isn't optional). - def self._wv_cb_return_object(value, cls) - return FFI::Pointer::NULL if value.nil? - raise TypeError, "expected a #{cls.name}, got #{value.class}" unless value.is_a?(cls) - - value._wv_clone_ptr - end - - # @api private - # Registers a pending async call; returns its completion context and the - # queue its completion is delivered on. - def self._wv_async_begin - queue = Queue.new - [_wv_handle_put([:async, queue]), queue] - end - - # @api private - # The queue of the pending call a completion belongs to. Each context - # completes exactly once, so its entry is removed here. - def self._wv_async_finish(ctx) - kind, queue = _wv_handle_take(ctx) - kind == :async ? queue : Queue.new - end - - # @api private - # Blocks until a pending call completes, raising its error. If the wait is - # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is - # cancelled before the interruption propagates. - def self._wv_async_wait(queue, token = nil) - value = - begin - queue.pop - rescue Exception # rubocop:disable Lint/RescueException - token&.cancel - raise - end - raise value if value.is_a?(Exception) - - value - end - - # @api private - # The base of every interface wrapper. A wrapper owns one strong reference - # to its native object and releases it exactly once, from #close or (as a - # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the - # pointer through #_wv_call, so a #close that races an in-flight call on - # another thread only releases the reference once the call returns. - class WvObject - # @api private - # Adopts one strong reference the library handed over. - def self._from_ptr(ptr) - obj = allocate - obj._wv_init(ptr) - obj - end - - # @api private - def _wv_init(ptr) - @wv_handle = self.class::WV_PTR.new(ptr) - @wv_lock = Mutex.new - @wv_calls = 0 - @wv_closing = false - end - - # The native object pointer (borrowed; valid until #close). - def handle - handle = @wv_handle - raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing - - handle - end - - # Whether #close has released this wrapper's reference. - def closed? - @wv_handle.nil? || @wv_closing - end - - # Releases this wrapper's reference now rather than at GC time. - # Idempotent. The object itself lives until its last reference anywhere - # (another wrapper, a record field, the library) goes. - def close - handle = @wv_lock.synchronize do - next nil if @wv_handle.nil? || @wv_closing - - if @wv_calls.zero? - taken = @wv_handle - @wv_handle = nil - taken - else - @wv_closing = true - nil - end - end - handle&.free - nil - end - - # @api private - # Yields the native pointer, keeping the reference alive until the block - # returns. - def _wv_call - handle = @wv_lock.synchronize do - raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing - - @wv_calls += 1 - @wv_handle - end - begin - yield handle - ensure - _wv_leave - end - end - - # @api private - # Mints a new strong reference the caller owns (an object written into a - # value buffer or returned from a callback carries its own reference). - def _wv_clone_ptr - _wv_call { |ptr| self.class._wv_clone(ptr) } - end - - # dup and clone produce an independent wrapper with its own reference to - # the same object. - def initialize_copy(other) - super - _wv_init(other._wv_clone_ptr) - end - - private - - def _wv_leave - release = @wv_lock.synchronize do - @wv_calls -= 1 - next nil unless @wv_closing && @wv_calls.zero? - - taken = @wv_handle - @wv_handle = nil - @wv_closing = false - taken - end - release&.free - end - end - - # @api private - # Yields the native pointers of `objects` (NULL for nil), each kept alive - # until the block returns. - def self._wv_pin(*objects, &block) - _wv_pin_from(objects, 0, [], &block) - end - - # @api private - def self._wv_pin_from(objects, index, pointers, &block) - return yield(*pointers) if index == objects.length - - object = objects[index] - if object.nil? - _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) - else - object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } - end - end - - # @api private - # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, - # 3 cancel tokens, 4 byte runs; -1 reports whether the library counts at - # all). Only libraries built with the `leak-check` feature count. - def self._wv_debug_live(kind) - unless respond_to?(:kitchen_sink_debug_live) - begin - attach_function :kitchen_sink_debug_live, [:int32], :uint64 - rescue FFI::NotFoundError - raise Error.new(GENERIC_ERROR_CODE, 'kitchen_sink does not export kitchen_sink_debug_live') - end - end - kitchen_sink_debug_live(kind) - end -end - -# END runtime.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_rb.snap deleted file mode 100644 index 7d74d436..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_rb.snap +++ /dev/null @@ -1,227 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -# frozen_string_literal: true - -require_relative 'nested_modules/runtime' - -# Ruby bindings for nested_modules. -module NestedModules - # The declarations these bindings were generated with, per top-level - # module, as [id, hash, path]. Loading fails, naming the path, unless - # the library's contract table holds each one with an equal hash. - _wv_check_contract!( - nested_modules_graphics_contract: [ - [0x2c29eef9327a6bd4, 0x12983d5271651389, 'graphics.Unit'], - [0x304088bbc28f0015, 0x97e6e809913be6b6, 'graphics.new_canvas'], - [0x49f8837532df9ac6, 0x32e5b27e81ee0957, 'graphics.Canvas'], - [0x55717fa027336b47, 0xb2bd3a6bdcce48c8, 'graphics.shapes.describe'], - [0x7626b91b8f1f53cd, 0x867bbf7cf0f41c74, 'graphics.shapes.geometry.compute'], - [0xb3efcb50d9a61ab7, 0x3cb0207dfad2a149, 'graphics.GraphicsError'], - [0xc62b16a9591b744b, 0x7895610c8487eb1b, 'graphics.shapes.Shape'], - [0xe13001f4983ccd35, 0x16e7de070b4a4cfc, 'graphics.shapes.geometry.Detail'], - [0xf3f658bcfc5dba62, 0x2665ccb47356e746, 'graphics.shapes.make_shape'], - ], - ) - - # === Module: graphics === - - # Base error for the `graphics` module's error domain. - class GraphicsError < Error - # coordinate out of bounds - class OutOfBounds < GraphicsError - CODE = 1 - - def initialize(message = nil) - super(CODE, message || 'coordinate out of bounds') - end - end - end - - # @api private - # The GraphicsError for a domain `code`, with its payload fields decoded; - # the root Error (or Cancelled) for any other code. - def self._wv_graphics_error_from(code, message, payload = nil) - message = nil if message.empty? - r = WvBufferReader.new(payload) - error = - case code - when 1 - GraphicsError::OutOfBounds.new(message) - else - return _wv_error(code, message.to_s) - end - r.expect_end! - error - end - - # @api private - # Raises the GraphicsError for a non-zero error slot. - def self._wv_check_graphics_error!(err) - taken = _wv_take_error(err) - raise _wv_graphics_error_from(*taken) unless taken.nil? - end - - # A measurement unit shared with nested submodules. - module Unit - PIXELS = 0 - POINTS = 1 - end - - # Top-level struct that references a struct defined one level deeper. - class Canvas - attr_reader :width - - # Reference down into a nested module - attr_reader :root - - def initialize(width:, root:) - @width = width - @root = root - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Canvas) && - width == other.width && - root == other.root - end - end - - # @api private - # Packs a Canvas into the value-buffer wire format. - def self._wv_write_canvas(w, v) - w.write_i32(v.width) - _wv_write_shape(w, v.root) - end - - # @api private - # Unpacks a Canvas from the value-buffer wire format. - def self._wv_read_canvas(r) - Canvas.new(width: r.read_i32, root: _wv_read_shape(r)) - end - - attach_function :nested_modules_graphics_new_canvas, [:int32, :pointer, :pointer], :pointer, blocking: true - - # Construct a Canvas. - # @return [Canvas] - def self.new_canvas(width) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = nested_modules_graphics_new_canvas(width, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_canvas(r) } - end - - # === Module: graphics.shapes === - - # A struct one level deep that references both an ancestor enum and a deeper struct. - class Shape - attr_reader :sides - - # Reference up to the parent module's enum - attr_reader :unit - - # Reference down into a deeper module - attr_reader :detail - - def initialize(sides:, unit:, detail:) - @sides = sides - @unit = unit - @detail = detail - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Shape) && - sides == other.sides && - unit == other.unit && - detail == other.detail - end - end - - # @api private - # Packs a Shape into the value-buffer wire format. - def self._wv_write_shape(w, v) - w.write_i32(v.sides) - w.write_i32(v.unit) - _wv_write_detail(w, v.detail) - end - - # @api private - # Unpacks a Shape from the value-buffer wire format. - def self._wv_read_shape(r) - Shape.new(sides: r.read_i32, unit: r.read_i32, detail: _wv_read_detail(r)) - end - - attach_function :nested_modules_graphics_shapes_make_shape, [:int32, :pointer, :pointer], :pointer, blocking: true - attach_function :nested_modules_graphics_shapes_describe, [:pointer, :pointer], :pointer, blocking: true - - # Construct a Shape. - # @return [Shape] - def self.make_shape(sides) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = nested_modules_graphics_shapes_make_shape(sides, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_shape(r) } - end - - # Nested function returning a struct owned by an ancestor module. - # @return [Canvas] - def self.describe - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = nested_modules_graphics_shapes_describe(out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_canvas(r) } - end - - # === Module: graphics.shapes.geometry === - - # A struct two levels deep. - class Detail - attr_reader :area - - def initialize(area:) - @area = area - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Detail) && - area == other.area - end - end - - # @api private - # Packs a Detail into the value-buffer wire format. - def self._wv_write_detail(w, v) - w.write_f64(v.area) - end - - # @api private - # Unpacks a Detail from the value-buffer wire format. - def self._wv_read_detail(r) - Detail.new(area: r.read_f64) - end - - attach_function :nested_modules_graphics_shapes_geometry_compute, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - - # Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - # @return [Detail] - def self.compute(shape) - shape_s = _wv_encode { |w| _wv_write_shape(w, shape) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = nested_modules_graphics_shapes_geometry_compute(shape_s, shape_s.bytesize, out_len, err) - _wv_check_graphics_error!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_detail(r) } - end -end - -# END nested_modules.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_rb.snap deleted file mode 100644 index 05df05ba..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_rb.snap +++ /dev/null @@ -1,319 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate - -# frozen_string_literal: true - -require_relative 'shapes/runtime' - -# Ruby bindings for shapes. -module Shapes - # The declarations these bindings were generated with, per top-level - # module, as [id, hash, path]. Loading fails, naming the path, unless - # the library's contract table holds each one with an equal hash. - _wv_check_contract!( - shapes_shapes_contract: [ - [0x0d7b3eec49243e7a, 0x5f232311808e1b1f, 'shapes.Channel'], - [0x183fa8198100860e, 0xe40c6993cb31d359, 'shapes.describe'], - [0x2d1395eb837b848e, 0x42202a0c12f3a16e, 'shapes.sum_bytes'], - [0x7f9a5b87ba09b86b, 0xc5b32fb0efbbaa17, 'shapes.scale'], - [0xc8edd779240fda17, 0x68e1be78fddb20e4, 'shapes.Numerics'], - [0xd3d3881b2fd18a62, 0xf24d0eb76a99d918, 'shapes.make_numerics'], - [0xf010844afff5ac3c, 0x50de28ae9b41ff15, 'shapes.Shape'], - ], - ) - - # === Module: shapes === - - # An algebraic shape (sum type with associated data) - class Shape - # The active variant's integer tag. - def tag - self.class::TAG - end - - # The empty shape (a unit variant) - class Empty < Shape - TAG = 0 - - # Structural equality over every field. - def ==(other) - other.is_a?(Empty) - end - end - - # A circle with a radius - class Circle < Shape - TAG = 1 - - # Radius in points - attr_reader :radius - - def initialize(radius:) - @radius = radius - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Circle) && - radius == other.radius - end - end - - # An axis-aligned rectangle - class Rectangle < Shape - TAG = 2 - - attr_reader :width - - attr_reader :height - - def initialize(width:, height:) - @width = width - @height = height - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Rectangle) && - width == other.width && - height == other.height - end - end - - # An integer-bounded shape (exercises narrow + wide ints) - class Bounded < Shape - TAG = 3 - - attr_reader :min - - attr_reader :max - - attr_reader :weight - - def initialize(min:, max:, weight:) - @min = min - @max = max - @weight = weight - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Bounded) && - min == other.min && - max == other.max && - weight == other.weight - end - end - - # A labeled shape with a small count - class Labeled < Shape - TAG = 4 - - attr_reader :label - - attr_reader :count - - def initialize(label:, count:) - @label = label - @count = count - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Labeled) && - label == other.label && - count == other.count - end - end - end - - # A plain C-style enum (no payloads) - module Channel - RED = 0 - GREEN = 1 - BLUE = 2 - end - - # One field per supported numeric primitive - class Numerics - attr_reader :a - - attr_reader :b - - attr_reader :c - - attr_reader :d - - attr_reader :e - - attr_reader :f - - attr_reader :g - - attr_reader :h - - attr_reader :i - - attr_reader :j - - def initialize(a:, b:, c:, d:, e:, f:, g:, h:, i:, j:) - @a = a - @b = b - @c = c - @d = d - @e = e - @f = f - @g = g - @h = h - @i = i - @j = j - end - - # Structural equality over every field. - def ==(other) - other.is_a?(Numerics) && - a == other.a && - b == other.b && - c == other.c && - d == other.d && - e == other.e && - f == other.f && - g == other.g && - h == other.h && - i == other.i && - j == other.j - end - end - - # @api private - # Packs a Numerics into the value-buffer wire format. - def self._wv_write_numerics(w, v) - w.write_i8(v.a) - w.write_i16(v.b) - w.write_i32(v.c) - w.write_i64(v.d) - w.write_u8(v.e) - w.write_u16(v.f) - w.write_u32(v.g) - w.write_u64(v.h) - w.write_f32(v.i) - w.write_f64(v.j) - end - - # @api private - # Unpacks a Numerics from the value-buffer wire format. - def self._wv_read_numerics(r) - Numerics.new( - a: r.read_i8, - b: r.read_i16, - c: r.read_i32, - d: r.read_i64, - e: r.read_u8, - f: r.read_u16, - g: r.read_u32, - h: r.read_u64, - i: r.read_f32, - j: r.read_f64, - ) - end - - # @api private - # Packs a Shape into the value-buffer wire format. - def self._wv_write_shape(w, v) - case v - when Shape::Empty - w.write_i32(0) - when Shape::Circle - w.write_i32(1) - w.write_f64(v.radius) - when Shape::Rectangle - w.write_i32(2) - w.write_f32(v.width) - w.write_f32(v.height) - when Shape::Bounded - w.write_i32(3) - w.write_i16(v.min) - w.write_i16(v.max) - w.write_u64(v.weight) - when Shape::Labeled - w.write_i32(4) - w.write_string(v.label) - w.write_u8(v.count) - else - raise TypeError, "expected a Shape, got #{v.class}" - end - end - - # @api private - # Unpacks a Shape from the value-buffer wire format. - def self._wv_read_shape(r) - tag = r.read_i32 - case tag - when 0 - Shape::Empty.new - when 1 - Shape::Circle.new(radius: r.read_f64) - when 2 - Shape::Rectangle.new(width: r.read_f32, height: r.read_f32) - when 3 - Shape::Bounded.new(min: r.read_i16, max: r.read_i16, weight: r.read_u64) - when 4 - Shape::Labeled.new(label: r.read_string, count: r.read_u8) - else - raise _wv_malformed("unknown Shape tag #{tag}") - end - end - - attach_function :shapes_shapes_describe, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true - attach_function :shapes_shapes_scale, [:pointer, :size_t, :double, :pointer, :pointer], :pointer, blocking: true - attach_function :shapes_shapes_sum_bytes, [:pointer, :size_t, :pointer], :uint64, blocking: true - attach_function :shapes_shapes_make_numerics, [:int8, :int16, :int32, :int64, :uint8, :uint16, :uint32, :uint64, :float, :double, :pointer, :pointer], :pointer, blocking: true - - # Render a shape to a string - # @return [String] - def self.describe(shape) - shape_s = _wv_encode { |w| _wv_write_shape(w, shape) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = shapes_shapes_describe(shape_s, shape_s.bytesize, out_len, err) - _wv_check!(err) - _wv_take_string(result, out_len.read(:size_t)) - end - - # Scale a shape by a factor, returning a new shape - # @return [Shape] - def self.scale(shape, factor) - shape_s = _wv_encode { |w| _wv_write_shape(w, shape) } - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = shapes_shapes_scale(shape_s, shape_s.bytesize, factor, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_shape(r) } - end - - # Sum a list of bytes into a wide integer (numerics smoke) - # @return [Integer] - def self.sum_bytes(values) - values_s = _wv_bytes(values) - err = ErrorStruct.new - result = shapes_shapes_sum_bytes(values_s, values_s.bytesize, err) - _wv_check!(err) - result - end - - # Build a Numerics struct from every numeric primitive - # @return [Numerics] - def self.make_numerics(a, b, c, d, e, f, g, h, i, j) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, out_len, err) - _wv_check!(err) - _wv_decode(result, out_len.read(:size_t)) { |r| _wv_read_numerics(r) } - end -end - -# END shapes.rb diff --git a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_DocsEverywhere_DocsEverywhere_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_DocsEverywhere_DocsEverywhere_swift.snap deleted file mode 100644 index e36f0457..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_DocsEverywhere_DocsEverywhere_swift.snap +++ /dev/null @@ -1,944 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import CDocsEverywhere -import Foundation - -// MARK: - Runtime support - -/// A failure the native library reports outside any declared error domain: -/// an unknown error code or a runtime failure. Throwing wrappers raise it; -/// non-throwing wrappers stop the process with `fatalError` instead, since -/// Swift has no unchecked errors. -public struct DocsEverywhereRuntimeError: Error, LocalizedError, Hashable, Sendable { - /// The code of a generic runtime failure. - public static let genericCode: Int32 = -1 - /// The code of a panic caught at the library boundary. - public static let panicCode: Int32 = -2 - /// The code of an argument or result that failed to marshal. - public static let marshalCode: Int32 = -3 - /// The code of a callback implementation that threw. - public static let foreignCode: Int32 = -4 - - /// The numeric code the native library reported. - public let errorCode: Int32 - /// The message the native library reported. - public let message: String - - /// Creates an error carrying a native code and message. - public init(errorCode: Int32, message: String) { - self.errorCode = errorCode - self.message = message - } - - public var errorDescription: String? { message } -} - -/// The error slot every native call reports through. -typealias WvError = docs_everywhere_error - -/// The C ABI revision these bindings were generated against. -let wvAbiVersion: UInt32 = 4 - -/// Runs the load-time checks if they haven't run yet. -@inline(__always) -func wvLoad() { - _ = wvContract -} - -/// Stops the process unless the library implements the ABI revision these -/// bindings were generated against. -func wvCheckAbiVersion() { - let abi = docs_everywhere_abi_version() - guard abi == wvAbiVersion else { - fatalError("DocsEverywhere: the native library 'docs_everywhere' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") - } -} - -/// Stops the process unless every declaration these bindings were generated -/// with is in a top-level module's contract table with an equal signature -/// hash. Declarations the library adds are fine. -func wvCheckContract( - _ table: (UnsafeMutablePointer?) -> UnsafePointer?, - _ expected: [(id: UInt64, hash: UInt64, path: StaticString)] -) { - var len = 0 - let entries = UnsafeBufferPointer(start: table(&len), count: len) - var hashes: [UInt64: UInt64] = [:] - for entry in entries { - hashes[entry.id] = entry.hash - } - for (id, hash, path) in expected { - guard let actual = hashes[id] else { - fatalError("DocsEverywhere: \(path) is missing from the library 'docs_everywhere'; regenerate the bindings or rebuild the library") - } - guard actual == hash else { - fatalError("DocsEverywhere: \(path) changed since these bindings were generated; regenerate the bindings or rebuild the library 'docs_everywhere'") - } - } -} - -/// The message carried by an error slot. -func wvErrorMessage(_ err: WvError) -> String { - err.message.map { String(cString: $0) } ?? "" -} - -/// The Swift error for a code outside any declared domain: cancellation -/// (`-5`) becomes `CancellationError`, anything else the runtime error. -func wvRuntimeError(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - return DocsEverywhereRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) -} - -/// Stops the process for a failed call that can't throw, naming the code and -/// message the library reported. -func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { - fatalError("DocsEverywhere.\(function) failed with code \(code): \(message)") -} - -/// Throws the runtime error for a filled error slot, clearing it. -@inline(__always) -func wvCheck(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvRuntimeError(err) - docs_everywhere_error_clear(&err) - throw error -} - -/// Stops the process for a filled error slot of a call that can't throw. -@inline(__always) -func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { - guard err.code != 0 else { return } - let code = err.code - let message = wvErrorMessage(err) - docs_everywhere_error_clear(&err) - wvFatal(code, message, function) -} - -/// Maps and releases the heap-boxed error an async completion received. -func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { - let error = map(err.pointee) - docs_everywhere_error_free(err) - return error -} - -/// The error mapping of a cancellable call that declares no error domain: -/// cancellation throws `CancellationError`, anything else stops the process. -func wvCancelledOrTrap(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - wvFatal(err.code, wvErrorMessage(err), "async call") -} - -/// Stops the process for the boxed error of an async call that can't throw. -func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { - wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) -} - -/// Unwraps a pointer the library promised is non-null. -@inline(__always) -func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { - guard let ptr = ptr else { - wvFatal(DocsEverywhereRuntimeError.marshalCode, "the native library returned a null pointer", function) - } - return ptr -} - -/// The Swift enum case for a C-style enum discriminant. -func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { - guard let value = E(rawValue: raw) else { - wvDecodeFailure("unknown \(E.self) discriminant \(raw)") - } - return value -} - -// MARK: Strings, bytes, and buffers - -/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. -@inline(__always) -func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var s = s - return try s.withUTF8 { try body($0.baseAddress, $0.count) } -} - -/// Lends the bytes of `d` for one call. -@inline(__always) -func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } -} - -/// Encodes `value` as a value buffer and lends it for one call. -@inline(__always) -func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var w = WvWriter() - w.write(value) - return try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } -} - -/// Copies a returned string, then releases the library's allocation. -func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - defer { docs_everywhere_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies returned bytes, then releases the library's allocation. -func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - defer { docs_everywhere_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return Data(bytes: ptr, count: len) -} - -/// Decodes a returned value buffer in place, then releases it. -func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - defer { - if let ptr = ptr { docs_everywhere_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - } - return wvBorrowBuffer(ptr, len, as: type) -} - -/// Copies a borrowed string. -func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies borrowed bytes. -func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - return Data(bytes: ptr, count: len) -} - -/// Decodes a borrowed value buffer in place. -func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - var r = WvReader(UnsafeRawPointer(ptr), len) - let value: T = r.read() - r.finish() - return value -} - -/// Stops the process on a malformed value buffer, which the wire format -/// treats like a marshalling failure. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("DocsEverywhere: malformed value buffer: \(context)") -} - -/// Serializes values in the value-buffer wire format: little-endian, packed, -/// no alignment. -struct WvWriter { - var bytes: [UInt8] = [] - - init() { - bytes.reserveCapacity(64) - } - - @inline(__always) - private mutating func append(_ v: T) { - withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } - } - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeI8(_ v: Int8) { append(v) } - mutating func writeU8(_ v: UInt8) { append(v) } - mutating func writeI16(_ v: Int16) { append(v) } - mutating func writeU16(_ v: UInt16) { append(v) } - mutating func writeI32(_ v: Int32) { append(v) } - mutating func writeU32(_ v: UInt32) { append(v) } - mutating func writeI64(_ v: Int64) { append(v) } - mutating func writeU64(_ v: UInt64) { append(v) } - mutating func writeF32(_ v: Float) { append(v.bitPattern) } - mutating func writeF64(_ v: Double) { append(v.bitPattern) } - - /// Writes a `u32` length or element count. - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") - append(UInt32(n)) - } - - mutating func writeString(_ v: String) { - let utf8 = v.utf8 - writeLen(utf8.count) - bytes.append(contentsOf: utf8) - } - - mutating func writeBytes(_ v: Data) { - writeLen(v.count) - bytes.append(contentsOf: v) - } - - mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } - - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } - - /// Writes any value that crosses inside a value buffer. - @inline(__always) - mutating func write(_ value: T) { - value.wvWrite(&self) - } -} - -/// Deserializes values from the value-buffer wire format directly from the -/// library's memory, rejecting truncated buffers, invalid flag bytes, -/// oversized length prefixes, and trailing bytes. -struct WvReader { - private let base: UnsafeRawPointer? - private let count: Int - private var pos = 0 - - init(_ base: UnsafeRawPointer?, _ count: Int) { - self.base = base - self.count = base == nil ? 0 : count - } - - /// The number of unread bytes. - var remaining: Int { count - pos } - - private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { - guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } - defer { pos += n } - return base + pos - } - - @inline(__always) - private mutating func load(_ type: T.Type, _ what: StaticString) -> T { - T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) - } - - mutating func readBool() -> Bool { - switch load(UInt8.self, "bool") { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readI8() -> Int8 { load(Int8.self, "i8") } - mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } - mutating func readI16() -> Int16 { load(Int16.self, "i16") } - mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } - mutating func readI32() -> Int32 { load(Int32.self, "i32") } - mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } - mutating func readI64() -> Int64 { load(Int64.self, "i64") } - mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } - mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } - mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } - - /// Reads a byte length, which can't exceed the unread bytes. - mutating func readLen() -> Int { - let n = Int(load(UInt32.self, "length")) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - - /// Reads an element count. Elements may encode to zero bytes, so the - /// count isn't bounded by the unread bytes; callers cap preallocation. - mutating func readCount() -> Int { - Int(load(UInt32.self, "count")) - } - - mutating func readString() -> String { - let n = readLen() - if n == 0 { return "" } - let p = take(n, "string") - guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - - mutating func readBytes() -> Data { - let n = readLen() - if n == 0 { return Data() } - return Data(bytes: take(n, "bytes"), count: n) - } - - mutating func readOptionFlag() -> Bool { - switch load(UInt8.self, "option flag") { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - - /// Reads an object token: one strong reference the caller adopts into a - /// wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { - wvDecodeFailure("null object token") - } - return p - } - - /// Reads any value that crosses inside a value buffer, typed by context. - @inline(__always) - mutating func read() -> T { - T.wvRead(&self) - } - - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -// MARK: Value-buffer codecs - -/// A type that crosses the C ABI inside a value buffer. Every primitive, -/// optional, list, and map conforms here; each record, enum, and interface -/// conforms where it's declared. -protocol WvCodable { - /// Reads one value, adopting the reference of any object token. - static func wvRead(_ r: inout WvReader) -> Self - /// Writes this value, writing a fresh reference for any object. - func wvWrite(_ w: inout WvWriter) -} - -/// A C-style enum, which crosses as its `i32` discriminant. -protocol WvCEnum: WvCodable, RawRepresentable where RawValue == Int32 {} - -extension WvCEnum { - static func wvRead(_ r: inout WvReader) -> Self { wvEnumCase(Self.self, r.readI32()) } - func wvWrite(_ w: inout WvWriter) { w.writeI32(rawValue) } -} - -extension Bool: WvCodable { - static func wvRead(_ r: inout WvReader) -> Bool { r.readBool() } - func wvWrite(_ w: inout WvWriter) { w.writeBool(self) } -} - -extension Int8: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int8 { r.readI8() } - func wvWrite(_ w: inout WvWriter) { w.writeI8(self) } -} - -extension UInt8: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt8 { r.readU8() } - func wvWrite(_ w: inout WvWriter) { w.writeU8(self) } -} - -extension Int16: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int16 { r.readI16() } - func wvWrite(_ w: inout WvWriter) { w.writeI16(self) } -} - -extension UInt16: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt16 { r.readU16() } - func wvWrite(_ w: inout WvWriter) { w.writeU16(self) } -} - -extension Int32: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int32 { r.readI32() } - func wvWrite(_ w: inout WvWriter) { w.writeI32(self) } -} - -extension UInt32: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt32 { r.readU32() } - func wvWrite(_ w: inout WvWriter) { w.writeU32(self) } -} - -extension Int64: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int64 { r.readI64() } - func wvWrite(_ w: inout WvWriter) { w.writeI64(self) } -} - -extension UInt64: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt64 { r.readU64() } - func wvWrite(_ w: inout WvWriter) { w.writeU64(self) } -} - -extension Float: WvCodable { - static func wvRead(_ r: inout WvReader) -> Float { r.readF32() } - func wvWrite(_ w: inout WvWriter) { w.writeF32(self) } -} - -extension Double: WvCodable { - static func wvRead(_ r: inout WvReader) -> Double { r.readF64() } - func wvWrite(_ w: inout WvWriter) { w.writeF64(self) } -} - -extension String: WvCodable { - static func wvRead(_ r: inout WvReader) -> String { r.readString() } - func wvWrite(_ w: inout WvWriter) { w.writeString(self) } -} - -extension Data: WvCodable { - static func wvRead(_ r: inout WvReader) -> Data { r.readBytes() } - func wvWrite(_ w: inout WvWriter) { w.writeBytes(self) } -} - -extension Optional: WvCodable where Wrapped: WvCodable { - static func wvRead(_ r: inout WvReader) -> Wrapped? { - r.readOptionFlag() ? .some(r.read()) : nil - } - - func wvWrite(_ w: inout WvWriter) { - w.writeOptionFlag(self != nil) - if let value = self { w.write(value) } - } -} - -extension Array: WvCodable where Element: WvCodable { - static func wvRead(_ r: inout WvReader) -> [Element] { - let n = r.readCount() - var items: [Element] = [] - items.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. [Key: Value] { - let n = r.readCount() - var map: [Key: Value] = [:] - map.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. { - let value: CheckedContinuation - - init(_ value: CheckedContinuation) { - self.value = value - } -} - -/// One process-wide callback vtable at a stable address the library may hold -/// for the life of the process. -final class WvVtable: @unchecked Sendable { - let pointer: UnsafePointer - - init(_ value: T) { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - pointer = UnsafePointer(cell) - } -} - -/// Holds one callback implementation behind the `ctx` the library keeps. -final class WvBox: @unchecked Sendable { - let value: T - - init(_ value: T) { - self.value = value - } -} - -/// Retains `impl` for the library: the `ctx` of a callback parameter, which -/// the vtable's `free` entry releases with ``wvRelease(_:as:)``. -func wvRetain(_ impl: T) -> UnsafeMutableRawPointer { - Unmanaged.passRetained(WvBox(impl)).toOpaque() -} - -/// Releases the implementation behind `ctx`, from the vtable's `free` entry. -func wvRelease(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) { - Unmanaged>.fromOpaque(ctx!).release() -} - -/// Runs one callback method on the implementation behind `ctx`. A thrown -/// error never unwinds through the library: `domain` reports it when it's -/// the method's declared error, anything else is reported as a callback -/// failure (`-4`), and the method returns `fallback`. -func wvInvoke( - _ ctx: UnsafeMutableRawPointer?, - _ outErr: UnsafeMutablePointer?, - as type: T.Type, - fallback: R, - domain: (Error, UnsafeMutablePointer?) -> Bool = { _, _ in false }, - _ body: (T) throws -> R -) -> R { - let impl = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value - do { - return try body(impl) - } catch { - if !domain(error, outErr) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { docs_everywhere_error_set(outErr, DocsEverywhereRuntimeError.foreignCode, $0) } - } - return fallback - } -} - -/// Reports a declared domain error from a callback method: its code, its -/// message, and its fields as the payload. -func wvSetError(_ outErr: UnsafeMutablePointer?, _ code: Int32, _ message: String, _ payload: WvWriter? = nil) { - message.withCString { docs_everywhere_error_set(outErr, code, $0) } - if let payload = payload { - payload.bytes.withUnsafeBufferPointer { docs_everywhere_error_set_payload(outErr, $0.baseAddress, $0.count) } - } -} - -/// Hands `bytes` to the library as a callback method's return: a run -/// allocated with `docs_everywhere_alloc` that the library adopts. -func wvHandOver(_ bytes: UnsafeRawBufferPointer, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - let run = docs_everywhere_alloc(bytes.count) - if let run = run, let base = bytes.baseAddress { - run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - outPtr?.pointee = run - outLen?.pointee = run == nil ? 0 : bytes.count -} - -/// Returns a string from a callback method through its out slots. -func wvReturnString(_ value: String, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var value = value - value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0), outPtr, outLen) } -} - -/// Returns bytes from a callback method through its out slots. -func wvReturnBytes(_ value: Data, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - value.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -/// Returns a value buffer from a callback method through its out slots. -func wvReturnBuffer(_ value: T, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var w = WvWriter() - w.write(value) - w.bytes.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -// MARK: - Load-time checks - -/// The load-time checks, run once before the first native call: the library -/// must implement C ABI revision 4 and carry every declaration these -/// bindings were generated with, unchanged. -let wvContract: Void = { - wvCheckAbiVersion() - wvCheckContract(docs_everywhere_docs_contract, [ - (0x00b2187fd75634ec, 0x2263c6f787bf6081, "docs.publish_async"), - (0x12fc242b9dc6444c, 0x96405f484a642b31, "docs.Notebook.insert"), - (0x2213822ed05cd5bd, 0x26c5833392aa63ce, "docs.Notebook.new"), - (0x2bd762bcdaa207ff, 0x1b0ebab2d04c49b0, "docs.SavedListener"), - (0x2e086c45e9d617de, 0x4d43bf32358c01ff, "docs.Notebook.find"), - (0x4817dc98197ad960, 0xf8017b5a7d58b2c7, "docs.Notebook.size"), - (0x614de47452d5a55c, 0xd9aae1472494ffef, "docs.get_document"), - (0x67c5cb9fc119bcd5, 0x08d5b25a1a22a7fa, "docs.Document"), - (0x8d484bf2d78c9b70, 0xe9093cf96912194c, "docs.Notebook.max_documents"), - (0xaea61d6451691c67, 0xe4704128b3524b3d, "docs.watch"), - (0xd68d6ad1540cdec5, 0x01afeb40b0a8bfe1, "docs.Notebook"), - (0xdb0601444a4760cc, 0xe56c69629b444797, "docs.create_document"), - (0xe12d6f4838347b3e, 0xa9bab86646d84f51, "docs.Status"), - (0xee2f8d25b3313b50, 0x9691232f35439a2b, "docs.DocsErrors"), - ]) -}() - -// MARK: - API - -/// Typed errors reported by the `docs` module. -public enum DocsErrorsError: Error, LocalizedError, Sendable { - /// Returned when no document matches the requested id - case notFound(message: String) - /// Raised when attempting to move into a forbidden state - case invalidStatus(message: String) - - /// The message the library reported, or the code's documented default. - public var errorDescription: String? { - switch self { - case let .notFound(message): return message - case let .invalidStatus(message): return message - } - } - - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .notFound: return 1 - case .invalidStatus: return 2 - } - } -} - -func wvMapDocs(_ err: WvError) -> Error { - let message = wvErrorMessage(err) - switch err.code { - case 1: return DocsErrorsError.notFound(message: message.isEmpty ? "Document not found" : message) - case 2: return DocsErrorsError.invalidStatus(message: message.isEmpty ? "Invalid status transition" : message) - default: return wvRuntimeError(err) - } -} - -func wvCheckDocs(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvMapDocs(err) - docs_everywhere_error_clear(&err) - throw error -} - -/// Lifecycle status of a document. -/// -/// Used to drive UI badges and filters. -public enum Status: Int32, Sendable { - /// Document is being edited - case draft = 0 - /// Document is publicly visible - case published = 1 - /// Document is hidden from listings - case archived = 2 -} - -extension Status: WvCEnum {} - -/// Represents a single document tracked by the system. -/// -/// Documents are persisted to disk and exposed via the public API. -public struct Document: Hashable, Sendable { - /// Stable opaque identifier - public var id: Int64 - /// Human-readable title shown in lists - public var title: String - /// Current lifecycle status - public var status: Status - /// Free-form tags for grouping - public var tags: [String] - - /// Creates a value from its fields. - public init(id: Int64, title: String, status: Status, tags: [String]) { - self.id = id - self.title = title - self.status = status - self.tags = tags - } -} - -extension Document: WvCodable { - static func wvRead(_ r: inout WvReader) -> Document { - Document(id: r.read(), title: r.read(), status: r.read(), tags: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.id) - w.write(self.title) - w.write(self.status) - w.write(self.tags) - } -} - -/// Consumer-implemented observer of document-saved notifications. -/// -/// Implementations must be safe to call from any thread. -/// -/// Implement this protocol with a class and pass an instance where the API -/// expects it. The library keeps the instance until it no longer needs it and -/// may call any method from any thread. A method that throws fails the -/// library's call in progress: a method documented to throw a module error -/// reports that error, with its code and fields, and any other error reaches -/// the library as a callback failure (code -4). -public protocol SavedListener: AnyObject, Sendable { - /// Fires after a document has been persisted successfully - /// - /// - Parameter docId: Identifier of the saved document - /// - Parameter revision: Monotonically increasing revision - func onSaved(docId: Int64, revision: Int32) throws -} - -/// The process-wide vtable the library calls `SavedListener` implementations -/// through (`docs_everywhere_docs_SavedListener_vtable`). Every entry recovers the implementation -/// from `ctx`, copies or adopts its arguments, hands its result back, and -/// reports a thrown error through `out_err` instead of unwinding. -enum WvSavedListenerVtable { - static let shared = WvVtable(docs_everywhere_docs_SavedListener_vtable( - size: UInt32(MemoryLayout.stride), - flags: 0, - free: { ctx in - wvRelease(ctx, as: (any SavedListener).self) - }, - on_saved: { ctx, doc_id, revision, out_err in - wvInvoke(ctx, out_err, as: (any SavedListener).self, fallback: ()) { wvImpl in - try wvImpl.onSaved(docId: doc_id, revision: revision) - } - } - )) -} - -/// An ordered collection of documents. -/// -/// Notebooks own their documents and release them when destroyed. -public final class Notebook: @unchecked Sendable { - let ptr: OpaquePointer - - init(ptr: OpaquePointer) { - self.ptr = ptr - } - - deinit { - docs_everywhere_docs_Notebook_destroy(ptr) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it. - func clonePtr() -> OpaquePointer { - wvNonNull(docs_everywhere_docs_Notebook_clone(ptr)) - } - - /// Create an empty notebook - public init() { - wvLoad() - var err = WvError() - let rv = docs_everywhere_docs_Notebook_new(&err) - wvTrap(&err) - self.ptr = wvNonNull(rv) - } - - /// Add a document, returning its position in the notebook - /// - /// - Parameter document: The document to add - public func insert(document: Document) -> Int32 { - var err = WvError() - let rv: Int32 = wvWithEncoded(document) { document_ptr, document_len in - docs_everywhere_docs_Notebook_insert(ptr, document_ptr, document_len, &err) - } - wvTrap(&err) - return rv - } - - /// Look up a document by identifier, failing when none matches - /// - /// - Parameter id: Identifier of the document to find - /// - Throws: ``DocsErrorsError`` for a declared failure, ``DocsEverywhereRuntimeError`` otherwise. - public func find(id: Int64) throws -> Document { - var err = WvError() - var outLen = 0 - let rv = docs_everywhere_docs_Notebook_find(ptr, id, &outLen, &err) - try wvCheckDocs(&err) - return wvTakeBuffer(rv, outLen, as: Document.self) - } - - /// The number of documents currently held - public func size() -> Int32 { - var err = WvError() - let rv = docs_everywhere_docs_Notebook_size(ptr, &err) - wvTrap(&err) - return rv - } - - /// The largest number of documents one notebook will hold - public static func maxDocuments() -> Int32 { - wvLoad() - var err = WvError() - let rv = docs_everywhere_docs_Notebook_max_documents(&err) - wvTrap(&err) - return rv - } -} - -extension Notebook: WvCodable { - static func wvRead(_ r: inout WvReader) -> Notebook { Notebook(ptr: r.readObject()) } - func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) } -} - -/// Document storage: create, publish, and observe documents. -public enum Docs { - /// Create a brand new document. - /// - /// The document starts in `Draft` status; callers must publish - /// it explicitly to make it visible. - /// - /// - Parameter title: Human-readable title for the new document - /// - Parameter tags: Initial set of tags (may be empty) - public static func createDocument(title: String, tags: [String]) -> Document { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithUTF8(title) { title_ptr, title_len in - wvWithEncoded(tags) { tags_ptr, tags_len in - docs_everywhere_docs_create_document(title_ptr, title_len, tags_ptr, tags_len, &outLen, &err) - } - } - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Document.self) - } - - /// Look up a document by identifier - /// - /// - Parameter id: Identifier returned from create_document - public static func getDocument(id: Int64) -> Document? { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = docs_everywhere_docs_get_document(id, &outLen, &err) - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Document?.self) - } - - /// Publish a document in the background - /// - /// - Parameter id: Identifier of the document to publish - public static func publishAsync(id: Int64) async -> Bool { - wvLoad() - return await withCheckedContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - docs_everywhere_docs_publish_async(id, { context, err, result in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - wvTrapBoxed(err) - } - cont.resume(returning: result) - }, context) - } - } - - /// Register a listener for saved notifications - /// - /// - Parameter listener: Receiver of saved notifications - public static func watch(listener: any SavedListener) { - wvLoad() - var err = WvError() - let listener_ctx = wvRetain(listener) - docs_everywhere_docs_watch(listener_ctx, WvSavedListenerVtable.shared.pointer, &err) - wvTrap(&err) - } -} - -// END DocsEverywhere.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_EdgeCases_EdgeCases_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_EdgeCases_EdgeCases_swift.snap deleted file mode 100644 index d7dbab81..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_EdgeCases_EdgeCases_swift.snap +++ /dev/null @@ -1,1822 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import CEdgeCases -import Foundation - -// MARK: - Runtime support - -/// A failure the native library reports outside any declared error domain: -/// an unknown error code or a runtime failure. Throwing wrappers raise it; -/// non-throwing wrappers stop the process with `fatalError` instead, since -/// Swift has no unchecked errors. -public struct EdgeCasesRuntimeError: Error, LocalizedError, Hashable, Sendable { - /// The code of a generic runtime failure. - public static let genericCode: Int32 = -1 - /// The code of a panic caught at the library boundary. - public static let panicCode: Int32 = -2 - /// The code of an argument or result that failed to marshal. - public static let marshalCode: Int32 = -3 - /// The code of a callback implementation that threw. - public static let foreignCode: Int32 = -4 - - /// The numeric code the native library reported. - public let errorCode: Int32 - /// The message the native library reported. - public let message: String - - /// Creates an error carrying a native code and message. - public init(errorCode: Int32, message: String) { - self.errorCode = errorCode - self.message = message - } - - public var errorDescription: String? { message } -} - -/// The error slot every native call reports through. -typealias WvError = edge_cases_error - -/// The C ABI revision these bindings were generated against. -let wvAbiVersion: UInt32 = 4 - -/// Runs the load-time checks if they haven't run yet. -@inline(__always) -func wvLoad() { - _ = wvContract -} - -/// Stops the process unless the library implements the ABI revision these -/// bindings were generated against. -func wvCheckAbiVersion() { - let abi = edge_cases_abi_version() - guard abi == wvAbiVersion else { - fatalError("EdgeCases: the native library 'edge_cases' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") - } -} - -/// Stops the process unless every declaration these bindings were generated -/// with is in a top-level module's contract table with an equal signature -/// hash. Declarations the library adds are fine. -func wvCheckContract( - _ table: (UnsafeMutablePointer?) -> UnsafePointer?, - _ expected: [(id: UInt64, hash: UInt64, path: StaticString)] -) { - var len = 0 - let entries = UnsafeBufferPointer(start: table(&len), count: len) - var hashes: [UInt64: UInt64] = [:] - for entry in entries { - hashes[entry.id] = entry.hash - } - for (id, hash, path) in expected { - guard let actual = hashes[id] else { - fatalError("EdgeCases: \(path) is missing from the library 'edge_cases'; regenerate the bindings or rebuild the library") - } - guard actual == hash else { - fatalError("EdgeCases: \(path) changed since these bindings were generated; regenerate the bindings or rebuild the library 'edge_cases'") - } - } -} - -/// The message carried by an error slot. -func wvErrorMessage(_ err: WvError) -> String { - err.message.map { String(cString: $0) } ?? "" -} - -/// The Swift error for a code outside any declared domain: cancellation -/// (`-5`) becomes `CancellationError`, anything else the runtime error. -func wvRuntimeError(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - return EdgeCasesRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) -} - -/// Stops the process for a failed call that can't throw, naming the code and -/// message the library reported. -func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { - fatalError("EdgeCases.\(function) failed with code \(code): \(message)") -} - -/// Throws the runtime error for a filled error slot, clearing it. -@inline(__always) -func wvCheck(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvRuntimeError(err) - edge_cases_error_clear(&err) - throw error -} - -/// Stops the process for a filled error slot of a call that can't throw. -@inline(__always) -func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { - guard err.code != 0 else { return } - let code = err.code - let message = wvErrorMessage(err) - edge_cases_error_clear(&err) - wvFatal(code, message, function) -} - -/// Maps and releases the heap-boxed error an async completion received. -func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { - let error = map(err.pointee) - edge_cases_error_free(err) - return error -} - -/// The error mapping of a cancellable call that declares no error domain: -/// cancellation throws `CancellationError`, anything else stops the process. -func wvCancelledOrTrap(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - wvFatal(err.code, wvErrorMessage(err), "async call") -} - -/// Stops the process for the boxed error of an async call that can't throw. -func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { - wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) -} - -/// Unwraps a pointer the library promised is non-null. -@inline(__always) -func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { - guard let ptr = ptr else { - wvFatal(EdgeCasesRuntimeError.marshalCode, "the native library returned a null pointer", function) - } - return ptr -} - -/// The Swift enum case for a C-style enum discriminant. -func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { - guard let value = E(rawValue: raw) else { - wvDecodeFailure("unknown \(E.self) discriminant \(raw)") - } - return value -} - -// MARK: Strings, bytes, and buffers - -/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. -@inline(__always) -func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var s = s - return try s.withUTF8 { try body($0.baseAddress, $0.count) } -} - -/// Lends the bytes of `d` for one call. -@inline(__always) -func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } -} - -/// Encodes `value` as a value buffer and lends it for one call. -@inline(__always) -func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var w = WvWriter() - w.write(value) - return try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } -} - -/// Copies a returned string, then releases the library's allocation. -func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - defer { edge_cases_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies returned bytes, then releases the library's allocation. -func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - defer { edge_cases_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return Data(bytes: ptr, count: len) -} - -/// Decodes a returned value buffer in place, then releases it. -func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - defer { - if let ptr = ptr { edge_cases_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - } - return wvBorrowBuffer(ptr, len, as: type) -} - -/// Copies a borrowed string. -func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies borrowed bytes. -func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - return Data(bytes: ptr, count: len) -} - -/// Decodes a borrowed value buffer in place. -func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - var r = WvReader(UnsafeRawPointer(ptr), len) - let value: T = r.read() - r.finish() - return value -} - -/// Stops the process on a malformed value buffer, which the wire format -/// treats like a marshalling failure. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("EdgeCases: malformed value buffer: \(context)") -} - -/// Serializes values in the value-buffer wire format: little-endian, packed, -/// no alignment. -struct WvWriter { - var bytes: [UInt8] = [] - - init() { - bytes.reserveCapacity(64) - } - - @inline(__always) - private mutating func append(_ v: T) { - withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } - } - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeI8(_ v: Int8) { append(v) } - mutating func writeU8(_ v: UInt8) { append(v) } - mutating func writeI16(_ v: Int16) { append(v) } - mutating func writeU16(_ v: UInt16) { append(v) } - mutating func writeI32(_ v: Int32) { append(v) } - mutating func writeU32(_ v: UInt32) { append(v) } - mutating func writeI64(_ v: Int64) { append(v) } - mutating func writeU64(_ v: UInt64) { append(v) } - mutating func writeF32(_ v: Float) { append(v.bitPattern) } - mutating func writeF64(_ v: Double) { append(v.bitPattern) } - - /// Writes a `u32` length or element count. - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") - append(UInt32(n)) - } - - mutating func writeString(_ v: String) { - let utf8 = v.utf8 - writeLen(utf8.count) - bytes.append(contentsOf: utf8) - } - - mutating func writeBytes(_ v: Data) { - writeLen(v.count) - bytes.append(contentsOf: v) - } - - mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } - - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } - - /// Writes any value that crosses inside a value buffer. - @inline(__always) - mutating func write(_ value: T) { - value.wvWrite(&self) - } -} - -/// Deserializes values from the value-buffer wire format directly from the -/// library's memory, rejecting truncated buffers, invalid flag bytes, -/// oversized length prefixes, and trailing bytes. -struct WvReader { - private let base: UnsafeRawPointer? - private let count: Int - private var pos = 0 - - init(_ base: UnsafeRawPointer?, _ count: Int) { - self.base = base - self.count = base == nil ? 0 : count - } - - /// The number of unread bytes. - var remaining: Int { count - pos } - - private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { - guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } - defer { pos += n } - return base + pos - } - - @inline(__always) - private mutating func load(_ type: T.Type, _ what: StaticString) -> T { - T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) - } - - mutating func readBool() -> Bool { - switch load(UInt8.self, "bool") { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readI8() -> Int8 { load(Int8.self, "i8") } - mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } - mutating func readI16() -> Int16 { load(Int16.self, "i16") } - mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } - mutating func readI32() -> Int32 { load(Int32.self, "i32") } - mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } - mutating func readI64() -> Int64 { load(Int64.self, "i64") } - mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } - mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } - mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } - - /// Reads a byte length, which can't exceed the unread bytes. - mutating func readLen() -> Int { - let n = Int(load(UInt32.self, "length")) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - - /// Reads an element count. Elements may encode to zero bytes, so the - /// count isn't bounded by the unread bytes; callers cap preallocation. - mutating func readCount() -> Int { - Int(load(UInt32.self, "count")) - } - - mutating func readString() -> String { - let n = readLen() - if n == 0 { return "" } - let p = take(n, "string") - guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - - mutating func readBytes() -> Data { - let n = readLen() - if n == 0 { return Data() } - return Data(bytes: take(n, "bytes"), count: n) - } - - mutating func readOptionFlag() -> Bool { - switch load(UInt8.self, "option flag") { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - - /// Reads an object token: one strong reference the caller adopts into a - /// wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { - wvDecodeFailure("null object token") - } - return p - } - - /// Reads any value that crosses inside a value buffer, typed by context. - @inline(__always) - mutating func read() -> T { - T.wvRead(&self) - } - - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -// MARK: Value-buffer codecs - -/// A type that crosses the C ABI inside a value buffer. Every primitive, -/// optional, list, and map conforms here; each record, enum, and interface -/// conforms where it's declared. -protocol WvCodable { - /// Reads one value, adopting the reference of any object token. - static func wvRead(_ r: inout WvReader) -> Self - /// Writes this value, writing a fresh reference for any object. - func wvWrite(_ w: inout WvWriter) -} - -/// A C-style enum, which crosses as its `i32` discriminant. -protocol WvCEnum: WvCodable, RawRepresentable where RawValue == Int32 {} - -extension WvCEnum { - static func wvRead(_ r: inout WvReader) -> Self { wvEnumCase(Self.self, r.readI32()) } - func wvWrite(_ w: inout WvWriter) { w.writeI32(rawValue) } -} - -extension Bool: WvCodable { - static func wvRead(_ r: inout WvReader) -> Bool { r.readBool() } - func wvWrite(_ w: inout WvWriter) { w.writeBool(self) } -} - -extension Int8: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int8 { r.readI8() } - func wvWrite(_ w: inout WvWriter) { w.writeI8(self) } -} - -extension UInt8: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt8 { r.readU8() } - func wvWrite(_ w: inout WvWriter) { w.writeU8(self) } -} - -extension Int16: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int16 { r.readI16() } - func wvWrite(_ w: inout WvWriter) { w.writeI16(self) } -} - -extension UInt16: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt16 { r.readU16() } - func wvWrite(_ w: inout WvWriter) { w.writeU16(self) } -} - -extension Int32: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int32 { r.readI32() } - func wvWrite(_ w: inout WvWriter) { w.writeI32(self) } -} - -extension UInt32: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt32 { r.readU32() } - func wvWrite(_ w: inout WvWriter) { w.writeU32(self) } -} - -extension Int64: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int64 { r.readI64() } - func wvWrite(_ w: inout WvWriter) { w.writeI64(self) } -} - -extension UInt64: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt64 { r.readU64() } - func wvWrite(_ w: inout WvWriter) { w.writeU64(self) } -} - -extension Float: WvCodable { - static func wvRead(_ r: inout WvReader) -> Float { r.readF32() } - func wvWrite(_ w: inout WvWriter) { w.writeF32(self) } -} - -extension Double: WvCodable { - static func wvRead(_ r: inout WvReader) -> Double { r.readF64() } - func wvWrite(_ w: inout WvWriter) { w.writeF64(self) } -} - -extension String: WvCodable { - static func wvRead(_ r: inout WvReader) -> String { r.readString() } - func wvWrite(_ w: inout WvWriter) { w.writeString(self) } -} - -extension Data: WvCodable { - static func wvRead(_ r: inout WvReader) -> Data { r.readBytes() } - func wvWrite(_ w: inout WvWriter) { w.writeBytes(self) } -} - -extension Optional: WvCodable where Wrapped: WvCodable { - static func wvRead(_ r: inout WvReader) -> Wrapped? { - r.readOptionFlag() ? .some(r.read()) : nil - } - - func wvWrite(_ w: inout WvWriter) { - w.writeOptionFlag(self != nil) - if let value = self { w.write(value) } - } -} - -extension Array: WvCodable where Element: WvCodable { - static func wvRead(_ r: inout WvReader) -> [Element] { - let n = r.readCount() - var items: [Element] = [] - items.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. [Key: Value] { - let n = r.readCount() - var map: [Key: Value] = [:] - map.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. { - let value: CheckedContinuation - - init(_ value: CheckedContinuation) { - self.value = value - } -} - -/// One process-wide callback vtable at a stable address the library may hold -/// for the life of the process. -final class WvVtable: @unchecked Sendable { - let pointer: UnsafePointer - - init(_ value: T) { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - pointer = UnsafePointer(cell) - } -} - -/// Holds one callback implementation behind the `ctx` the library keeps. -final class WvBox: @unchecked Sendable { - let value: T - - init(_ value: T) { - self.value = value - } -} - -/// Retains `impl` for the library: the `ctx` of a callback parameter, which -/// the vtable's `free` entry releases with ``wvRelease(_:as:)``. -func wvRetain(_ impl: T) -> UnsafeMutableRawPointer { - Unmanaged.passRetained(WvBox(impl)).toOpaque() -} - -/// Releases the implementation behind `ctx`, from the vtable's `free` entry. -func wvRelease(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) { - Unmanaged>.fromOpaque(ctx!).release() -} - -/// Runs one callback method on the implementation behind `ctx`. A thrown -/// error never unwinds through the library: `domain` reports it when it's -/// the method's declared error, anything else is reported as a callback -/// failure (`-4`), and the method returns `fallback`. -func wvInvoke( - _ ctx: UnsafeMutableRawPointer?, - _ outErr: UnsafeMutablePointer?, - as type: T.Type, - fallback: R, - domain: (Error, UnsafeMutablePointer?) -> Bool = { _, _ in false }, - _ body: (T) throws -> R -) -> R { - let impl = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value - do { - return try body(impl) - } catch { - if !domain(error, outErr) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { edge_cases_error_set(outErr, EdgeCasesRuntimeError.foreignCode, $0) } - } - return fallback - } -} - -/// Reports a declared domain error from a callback method: its code, its -/// message, and its fields as the payload. -func wvSetError(_ outErr: UnsafeMutablePointer?, _ code: Int32, _ message: String, _ payload: WvWriter? = nil) { - message.withCString { edge_cases_error_set(outErr, code, $0) } - if let payload = payload { - payload.bytes.withUnsafeBufferPointer { edge_cases_error_set_payload(outErr, $0.baseAddress, $0.count) } - } -} - -/// Hands `bytes` to the library as a callback method's return: a run -/// allocated with `edge_cases_alloc` that the library adopts. -func wvHandOver(_ bytes: UnsafeRawBufferPointer, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - let run = edge_cases_alloc(bytes.count) - if let run = run, let base = bytes.baseAddress { - run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - outPtr?.pointee = run - outLen?.pointee = run == nil ? 0 : bytes.count -} - -/// Returns a string from a callback method through its out slots. -func wvReturnString(_ value: String, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var value = value - value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0), outPtr, outLen) } -} - -/// Returns bytes from a callback method through its out slots. -func wvReturnBytes(_ value: Data, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - value.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -/// Returns a value buffer from a callback method through its out slots. -func wvReturnBuffer(_ value: T, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var w = WvWriter() - w.write(value) - w.bytes.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -// MARK: - Load-time checks - -/// The load-time checks, run once before the first native call: the library -/// must implement C ABI revision 4 and carry every declaration these -/// bindings were generated with, unchanged. -let wvContract: Void = { - wvCheckAbiVersion() - wvCheckContract(edge_cases_edge_cases_contract, [ - (0x056a86511785ba2b, 0x5cba3f16d57c6cdb, "edge_cases.make_widget"), - (0x07bfa9ecb2b720d2, 0x3813b3f57c64bf35, "edge_cases.EdgeErrors"), - (0x0aaa11867e37f991, 0xf090dd52146ebdf0, "edge_cases.Widget.refresh"), - (0x0f9701493898ae4c, 0x6944f47a275c96e8, "edge_cases.Inner"), - (0x1226b1adac179f7d, 0x0e95a00ab0bef637, "edge_cases.async_keywords"), - (0x16d36bef1541136c, 0x5ea7021ec324298f, "edge_cases.find_widget"), - (0x1b7324c55e980feb, 0xabf87fe372358f42, "edge_cases.Widget.old_name"), - (0x1cbd731467c2bab8, 0x4915b9989c2c9074, "edge_cases.Vault"), - (0x2c2a6de67d894b0a, 0x50a4496b1e7191ca, "edge_cases.Vault.open"), - (0x322b4cfcd2d6861c, 0xca7527a379ae8465, "edge_cases.Keywords"), - (0x3f8e23a5f32906f4, 0x8d4c908d796f130d, "edge_cases.Leaf"), - (0x42ab8a685457b97c, 0xe5ce30500cadb32d, "edge_cases.Vault.handle"), - (0x4a68540e2836d3d0, 0xa20e44c71e420ed2, "edge_cases.mode_stream"), - (0x4e030580ac0b111d, 0xb68b7a18d910e175, "edge_cases.legacy_value"), - (0x4f5e4e5291864ef3, 0x55a1a88b21a08f8b, "edge_cases.Legacy"), - (0x4f84b06171b4213d, 0xbaedcfdebb6a2820, "edge_cases.scalar_stream"), - (0x5067a5ff2d9ef910, 0xbfc79b18fc4714fc, "edge_cases.Widget"), - (0x59aff159112fbf5b, 0x18e4def57a8cfa49, "edge_cases.optional_params"), - (0x5daf65b98fa77c48, 0xc817a8cb202e59c2, "edge_cases.keywords_roundtrip"), - (0x5de8a5c9726c2c5a, 0x7c50b5b65f7a896d, "edge_cases.register_batch"), - (0x618a620309994c3c, 0x826ad0b1d208d723, "edge_cases.async_bytes"), - (0x648285cdba0a7b8e, 0xcf6e80e783a4d8be, "edge_cases.WidgetHolder"), - (0x649c78079824b10d, 0x5b12dd26e0634b69, "edge_cases.async_widget"), - (0x7da7a7357d122a37, 0xf544e025ffaa3d20, "edge_cases.async_void"), - (0x7e4015bc3a472e2d, 0xfe22291d90b82e4a, "edge_cases.widget_stream"), - (0x805276e5d81fdba7, 0xd9b64e360bd03b2b, "edge_cases.Outer"), - (0x80e1183326542df4, 0xe8a57e12cc6ed276, "edge_cases.select"), - (0x923ce5e9bcd769a1, 0x0b973f5dc07dda8b, "edge_cases.Vault.to_string"), - (0x98ca8c01f5239404, 0xfe11c93eb6b303ef, "edge_cases.Widget.rename"), - (0xa13e22e98ea278d4, 0xf1fae215d7387800, "edge_cases.widgets_everywhere"), - (0xa84a8c9bf2a881e1, 0xbe1dc840bf221996, "edge_cases.async_optional"), - (0xad958c478fbf205d, 0xe386b0716d24f19b, "edge_cases.Retired"), - (0xb2fc3964ef56ab39, 0x73c325c8bc351236, "edge_cases.async_list"), - (0xb3403af521cf0006, 0x6abc30d48206a3ba, "edge_cases.Widget.close"), - (0xb794304a22a406b8, 0xf6fb98711c516105, "edge_cases.async_struct"), - (0xba3936106a49c47d, 0xb7025f2fadd6288e, "edge_cases.Vault.peek"), - (0xc155b0c3cfb164f4, 0xc777ae57d92ebc47, "edge_cases.noop"), - (0xcdc18607777019dd, 0xb81a31e5c20aa7a8, "edge_cases.inspect_widget"), - (0xd164a82aeb64b8cc, 0xded02c68005c4737, "edge_cases.nested_optional"), - (0xd978ccad2468038f, 0x30c39c65d6235153, "edge_cases.Mode"), - (0xdbe9d947431c9124, 0x39537fb848fdf980, "edge_cases.BatchListener"), - (0xeae8b7862c08bb07, 0xfe12eeaa830452ea, "edge_cases.Keyword"), - (0xf1e155a05ae262b8, 0xb575eb818a423d42, "edge_cases.async_enum"), - (0xf22e210bdbce7343, 0x2a5e0f801bc8c768, "edge_cases.Widget.dispose"), - (0xf3c7cf675563ee6c, 0x0ecffeef399facc8, "edge_cases.Vault.probe"), - (0xfd34bc67acac379e, 0x576caa14f3c0901a, "edge_cases.Widget.fetch"), - (0xff2e9d2bff941786, 0x7e96764ef1f5fbaa, "edge_cases.string_stream"), - ]) -}() - -// MARK: - API - -/// Typed errors reported by the `edge_cases` module. -public enum EdgeErrorsError: Error, LocalizedError, Sendable { - case busy(message: String) - - /// The message the library reported, or the code's documented default. - public var errorDescription: String? { - switch self { - case let .busy(message): return message - } - } - - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .busy: return 7 - } - } -} - -func wvMapEdgeCases(_ err: WvError) -> Error { - let message = wvErrorMessage(err) - switch err.code { - case 7: return EdgeErrorsError.busy(message: message.isEmpty ? "Resource is busy" : message) - default: return wvRuntimeError(err) - } -} - -func wvCheckEdgeCases(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvMapEdgeCases(err) - edge_cases_error_clear(&err) - throw error -} - -/// Deprecated enum with non-contiguous values and keyword-like variants -@available(*, deprecated, message: "Use Mode instead") -public enum Legacy: Int32, Sendable { - case none = 0 - case default_ = 5 - case class_ = 10 -} - -@available(*, deprecated, message: "Use Mode instead") -extension Legacy: WvCEnum {} - -public enum Mode: Int32, Sendable { - case fast = 1 - case safe = 2 -} - -extension Mode: WvCEnum {} - -public struct Leaf: Hashable, Sendable { - public var weight: Float - - /// Creates a value from its fields. - public init(weight: Float) { - self.weight = weight - } -} - -extension Leaf: WvCodable { - static func wvRead(_ r: inout WvReader) -> Leaf { - Leaf(weight: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.weight) - } -} - -/// Middle layer of a three-deep struct nesting -public struct Inner: Hashable, Sendable { - public var leaf: Leaf - public var leaves: [Leaf] - - /// Creates a value from its fields. - public init(leaf: Leaf, leaves: [Leaf]) { - self.leaf = leaf - self.leaves = leaves - } -} - -extension Inner: WvCodable { - static func wvRead(_ r: inout WvReader) -> Inner { - Inner(leaf: r.read(), leaves: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.leaf) - w.write(self.leaves) - } -} - -public struct Outer: Hashable, Sendable { - public var inner: Inner - public var maybeInner: Inner? - public var byName: [String: Inner] - public var modes: [Mode] - public var matrix: [[Int32]] - public var sparse: [Int64?] - public var blob: Data? - public var note: String? - public var byId: [Int64: String] - public var hidden: Bool - public var v2: UInt8 - - /// Creates a value from its fields. - public init(inner: Inner, maybeInner: Inner?, byName: [String: Inner], modes: [Mode], matrix: [[Int32]], sparse: [Int64?], blob: Data?, note: String?, byId: [Int64: String], hidden: Bool, v2: UInt8) { - self.inner = inner - self.maybeInner = maybeInner - self.byName = byName - self.modes = modes - self.matrix = matrix - self.sparse = sparse - self.blob = blob - self.note = note - self.byId = byId - self.hidden = hidden - self.v2 = v2 - } -} - -extension Outer: WvCodable { - static func wvRead(_ r: inout WvReader) -> Outer { - Outer(inner: r.read(), maybeInner: r.read(), byName: r.read(), modes: r.read(), matrix: r.read(), sparse: r.read(), blob: r.read(), note: r.read(), byId: r.read(), hidden: r.read(), v2: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.inner) - w.write(self.maybeInner) - w.write(self.byName) - w.write(self.modes) - w.write(self.matrix) - w.write(self.sparse) - w.write(self.blob) - w.write(self.note) - w.write(self.byId) - w.write(self.hidden) - w.write(self.v2) - } -} - -/// Every field is a reserved word somewhere -public struct Keywords: Hashable, Sendable { - public var class_: Int32 - public var def: Int32 - public var import_: Int32 - public var from: Int32 - public var func_: Int32 - public var range: Int32 - public var register: Int32 - public var volatile: Int32 - public var new: Int32 - public var default_: Int32 - public var end: Int32 - public var in_: Int32 - public var is_: Int32 - public var int: Int32 - public var var_: Int32 - public var val: Int32 - public var self_: Int32 - public var this: Int32 - public var object: Int32 - public var string: Int32 - public var case_: Int32 - public var select: Int32 - public var switch_: Int32 - public var void: Int32 - public var union: Int32 - public var namespace: Int32 - public var operator_: Int32 - public var template: Int32 - public var package: Int32 - public var protocol_: Int32 - public var guard_: Int32 - public var defer_: Int32 - public var inout_: Int32 - public var internal_: Int32 - public var when: Int32 - public var fun: Int32 - public var data: Int32 - public var late: Int32 - public var part: Int32 - public var out: Int32 - public var ref: Int32 - public var event: Int32 - public var base: Int32 - public var lambda: Int32 - public var yield: Int32 - public var nil_: Int32 - public var null: Int32 - public var begin: Int32 - public var rescue: Int32 - public var module: Int32 - public var chan: Int32 - public var go: Int32 - public var let_: Int32 - public var export: Int32 - public var delete: Int32 - public var typedef: Int32 - public var sizeof: Int32 - public var static_: Int32 - public var super_: Int32 - - /// Creates a value from its fields. - public init(class_: Int32, def: Int32, import_: Int32, from: Int32, func_: Int32, range: Int32, register: Int32, volatile: Int32, new: Int32, default_: Int32, end: Int32, in_: Int32, is_: Int32, int: Int32, var_: Int32, val: Int32, self_: Int32, this: Int32, object: Int32, string: Int32, case_: Int32, select: Int32, switch_: Int32, void: Int32, union: Int32, namespace: Int32, operator_: Int32, template: Int32, package: Int32, protocol_: Int32, guard_: Int32, defer_: Int32, inout_: Int32, internal_: Int32, when: Int32, fun: Int32, data: Int32, late: Int32, part: Int32, out: Int32, ref: Int32, event: Int32, base: Int32, lambda: Int32, yield: Int32, nil_: Int32, null: Int32, begin: Int32, rescue: Int32, module: Int32, chan: Int32, go: Int32, let_: Int32, export: Int32, delete: Int32, typedef: Int32, sizeof: Int32, static_: Int32, super_: Int32) { - self.class_ = class_ - self.def = def - self.import_ = import_ - self.from = from - self.func_ = func_ - self.range = range - self.register = register - self.volatile = volatile - self.new = new - self.default_ = default_ - self.end = end - self.in_ = in_ - self.is_ = is_ - self.int = int - self.var_ = var_ - self.val = val - self.self_ = self_ - self.this = this - self.object = object - self.string = string - self.case_ = case_ - self.select = select - self.switch_ = switch_ - self.void = void - self.union = union - self.namespace = namespace - self.operator_ = operator_ - self.template = template - self.package = package - self.protocol_ = protocol_ - self.guard_ = guard_ - self.defer_ = defer_ - self.inout_ = inout_ - self.internal_ = internal_ - self.when = when - self.fun = fun - self.data = data - self.late = late - self.part = part - self.out = out - self.ref = ref - self.event = event - self.base = base - self.lambda = lambda - self.yield = yield - self.nil_ = nil_ - self.null = null - self.begin = begin - self.rescue = rescue - self.module = module - self.chan = chan - self.go = go - self.let_ = let_ - self.export = export - self.delete = delete - self.typedef = typedef - self.sizeof = sizeof - self.static_ = static_ - self.super_ = super_ - } -} - -extension Keywords: WvCodable { - static func wvRead(_ r: inout WvReader) -> Keywords { - Keywords(class_: r.read(), def: r.read(), import_: r.read(), from: r.read(), func_: r.read(), range: r.read(), register: r.read(), volatile: r.read(), new: r.read(), default_: r.read(), end: r.read(), in_: r.read(), is_: r.read(), int: r.read(), var_: r.read(), val: r.read(), self_: r.read(), this: r.read(), object: r.read(), string: r.read(), case_: r.read(), select: r.read(), switch_: r.read(), void: r.read(), union: r.read(), namespace: r.read(), operator_: r.read(), template: r.read(), package: r.read(), protocol_: r.read(), guard_: r.read(), defer_: r.read(), inout_: r.read(), internal_: r.read(), when: r.read(), fun: r.read(), data: r.read(), late: r.read(), part: r.read(), out: r.read(), ref: r.read(), event: r.read(), base: r.read(), lambda: r.read(), yield: r.read(), nil_: r.read(), null: r.read(), begin: r.read(), rescue: r.read(), module: r.read(), chan: r.read(), go: r.read(), let_: r.read(), export: r.read(), delete: r.read(), typedef: r.read(), sizeof: r.read(), static_: r.read(), super_: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.class_) - w.write(self.def) - w.write(self.import_) - w.write(self.from) - w.write(self.func_) - w.write(self.range) - w.write(self.register) - w.write(self.volatile) - w.write(self.new) - w.write(self.default_) - w.write(self.end) - w.write(self.in_) - w.write(self.is_) - w.write(self.int) - w.write(self.var_) - w.write(self.val) - w.write(self.self_) - w.write(self.this) - w.write(self.object) - w.write(self.string) - w.write(self.case_) - w.write(self.select) - w.write(self.switch_) - w.write(self.void) - w.write(self.union) - w.write(self.namespace) - w.write(self.operator_) - w.write(self.template) - w.write(self.package) - w.write(self.protocol_) - w.write(self.guard_) - w.write(self.defer_) - w.write(self.inout_) - w.write(self.internal_) - w.write(self.when) - w.write(self.fun) - w.write(self.data) - w.write(self.late) - w.write(self.part) - w.write(self.out) - w.write(self.ref) - w.write(self.event) - w.write(self.base) - w.write(self.lambda) - w.write(self.yield) - w.write(self.nil_) - w.write(self.null) - w.write(self.begin) - w.write(self.rescue) - w.write(self.module) - w.write(self.chan) - w.write(self.go) - w.write(self.let_) - w.write(self.export) - w.write(self.delete) - w.write(self.typedef) - w.write(self.sizeof) - w.write(self.static_) - w.write(self.super_) - } -} - -/// Deprecated struct -@available(*, deprecated, message: "Use Outer instead") -public struct Retired: Hashable, Sendable { - public var id: Int64 - - /// Creates a value from its fields. - public init(id: Int64) { - self.id = id - } -} - -@available(*, deprecated, message: "Use Outer instead") -extension Retired: WvCodable { - static func wvRead(_ r: inout WvReader) -> Retired { - Retired(id: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.id) - } -} - -/// Record carrying objects in every buffered position -public struct WidgetHolder: Sendable { - public var primary: Widget - public var fallback: Widget? - public var extras: [Widget] - public var byName: [String: Widget] - - /// Creates a value from its fields. - public init(primary: Widget, fallback: Widget?, extras: [Widget], byName: [String: Widget]) { - self.primary = primary - self.fallback = fallback - self.extras = extras - self.byName = byName - } -} - -extension WidgetHolder: WvCodable { - static func wvRead(_ r: inout WvReader) -> WidgetHolder { - WidgetHolder(primary: r.read(), fallback: r.read(), extras: r.read(), byName: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.primary) - w.write(self.fallback) - w.write(self.extras) - w.write(self.byName) - } -} - -/// Callback interface carrying composite payloads and objects -/// -/// Implement this protocol with a class and pass an instance where the API -/// expects it. The library keeps the instance until it no longer needs it and -/// may call any method from any thread. A method that throws fails the -/// library's call in progress: a method documented to throw a module error -/// reports that error, with its code and fields, and any other error reaches -/// the library as a callback failure (code -4). -public protocol BatchListener: AnyObject, Sendable { - /// Composite payloads cross as value buffers - func onBatch(items: [Outer], raw: Data, first: Outer?, mode: Mode) throws - - /// Objects cross as strong references the consumer must release - func onWidget(widget: Widget, fallback: Widget?, children: [Widget]) throws - - func onTick() throws - - /// Scalar return travels back through the vtable - func shouldContinue(class_: Int32) throws -> Bool - - /// Enum return travels back as its integer discriminant - func pickMode() throws -> Mode -} - -/// The process-wide vtable the library calls `BatchListener` implementations -/// through (`edge_cases_edge_cases_BatchListener_vtable`). Every entry recovers the implementation -/// from `ctx`, copies or adopts its arguments, hands its result back, and -/// reports a thrown error through `out_err` instead of unwinding. -enum WvBatchListenerVtable { - static let shared = WvVtable(edge_cases_edge_cases_BatchListener_vtable( - size: UInt32(MemoryLayout.stride), - flags: 0, - free: { ctx in - wvRelease(ctx, as: (any BatchListener).self) - }, - on_batch: { ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err in - wvInvoke(ctx, out_err, as: (any BatchListener).self, fallback: ()) { wvImpl in - try wvImpl.onBatch(items: wvBorrowBuffer(items_ptr, items_len, as: [Outer].self), raw: wvBorrowBytes(raw_ptr, raw_len), first: wvBorrowBuffer(first_ptr, first_len, as: Outer?.self), mode: wvEnumCase(Mode.self, numericCast(mode.rawValue))) - } - }, - on_widget: { ctx, widget, fallback, children_ptr, children_len, out_err in - wvInvoke(ctx, out_err, as: (any BatchListener).self, fallback: ()) { wvImpl in - try wvImpl.onWidget(widget: Widget(ptr: wvNonNull(widget)), fallback: fallback.map { Widget(ptr: $0) }, children: wvBorrowBuffer(children_ptr, children_len, as: [Widget].self)) - } - }, - on_tick: { ctx, out_err in - wvInvoke(ctx, out_err, as: (any BatchListener).self, fallback: ()) { wvImpl in - try wvImpl.onTick() - } - }, - should_continue: { ctx, class_, out_err in - wvInvoke(ctx, out_err, as: (any BatchListener).self, fallback: false) { wvImpl in - try wvImpl.shouldContinue(class_: class_) - } - }, - pick_mode: { ctx, out_err in - wvInvoke(ctx, out_err, as: (any BatchListener).self, fallback: edge_cases_edge_cases_Mode(rawValue: 0)) { wvImpl in - try edge_cases_edge_cases_Mode(rawValue: numericCast(wvImpl.pickMode().rawValue)) - } - } - )) -} - -/// Callback interface whose name and methods are keywords -/// -/// Implement this protocol with a class and pass an instance where the API -/// expects it. The library keeps the instance until it no longer needs it and -/// may call any method from any thread. A method that throws fails the -/// library's call in progress: a method documented to throw a module error -/// reports that error, with its code and fields, and any other error reaches -/// the library as a callback failure (code -4). -public protocol Keyword: AnyObject, Sendable { - func delete(self_: Int32) throws - - func import_() throws -> Int32 -} - -/// The process-wide vtable the library calls `Keyword` implementations -/// through (`edge_cases_edge_cases_Keyword_vtable`). Every entry recovers the implementation -/// from `ctx`, copies or adopts its arguments, hands its result back, and -/// reports a thrown error through `out_err` instead of unwinding. -enum WvKeywordVtable { - static let shared = WvVtable(edge_cases_edge_cases_Keyword_vtable( - size: UInt32(MemoryLayout.stride), - flags: 0, - free: { ctx in - wvRelease(ctx, as: (any Keyword).self) - }, - delete_: { ctx, self_, out_err in - wvInvoke(ctx, out_err, as: (any Keyword).self, fallback: ()) { wvImpl in - try wvImpl.delete(self_: self_) - } - }, - import: { ctx, out_err in - wvInvoke(ctx, out_err, as: (any Keyword).self, fallback: 0) { wvImpl in - try wvImpl.import_() - } - } - )) -} - -/// Interface with no constructor; instances come from make_widget -public final class Widget: @unchecked Sendable { - let ptr: OpaquePointer - - init(ptr: OpaquePointer) { - self.ptr = ptr - } - - deinit { - edge_cases_edge_cases_Widget_destroy(ptr) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it. - func clonePtr() -> OpaquePointer { - wvNonNull(edge_cases_edge_cases_Widget_clone(ptr)) - } - - /// Void method that can fail - /// - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public func refresh() throws { - var err = WvError() - edge_cases_edge_cases_Widget_refresh(ptr, &err) - try wvCheckEdgeCases(&err) - } - - public func rename(class_: String, import_: String?) -> String { - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithUTF8(class_) { class__ptr, class__len in - wvWithEncoded(import_) { import__ptr, import__len in - edge_cases_edge_cases_Widget_rename(ptr, class__ptr, class__len, import__ptr, import__len, &outLen, &err) - } - } - wvTrap(&err) - return wvTakeString(rv, outLen) - } - - /// Async method with a struct result - /// - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public func fetch(id: Int64) async throws -> Outer { - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_Widget_fetch(self.ptr, id, { context, err, resultPtr, resultLen in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - cont.resume(throwing: wvTakeError(err, wvMapEdgeCases)) - return - } - cont.resume(returning: wvTakeBuffer(resultPtr, resultLen, as: Outer.self)) - }, context) - } - } - - @available(*, deprecated, message: "Use rename instead") - public func oldName() -> String { - var err = WvError() - var outLen = 0 - let rv = edge_cases_edge_cases_Widget_old_name(ptr, &outLen, &err) - wvTrap(&err) - return wvTakeString(rv, outLen) - } - - /// Spelled like the release method of several wrappers - public func close() -> Bool { - var err = WvError() - let rv = edge_cases_edge_cases_Widget_close(ptr, &err) - wvTrap(&err) - return rv - } - - /// Spelled like the release method of the Dart and .NET wrappers - public func dispose(force: Bool) { - var err = WvError() - edge_cases_edge_cases_Widget_dispose(ptr, force, &err) - wvTrap(&err) - } -} - -extension Widget: WvCodable { - static func wvRead(_ r: inout WvReader) -> Widget { Widget(ptr: r.readObject()) } - func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) } -} - -/// Deprecated interface with a throwing constructor -@available(*, deprecated, message: "Use Widget instead") -public final class Vault: @unchecked Sendable { - let ptr: OpaquePointer - - init(ptr: OpaquePointer) { - self.ptr = ptr - } - - deinit { - edge_cases_edge_cases_Vault_destroy(ptr) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it. - func clonePtr() -> OpaquePointer { - wvNonNull(edge_cases_edge_cases_Vault_clone(ptr)) - } - - /// Fails when the vault is busy - /// - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public static func open(path: String) throws -> Vault { - wvLoad() - var err = WvError() - let rv: OpaquePointer? = wvWithUTF8(path) { path_ptr, path_len in - edge_cases_edge_cases_Vault_open(path_ptr, path_len, &err) - } - try wvCheckEdgeCases(&err) - return Vault(ptr: wvNonNull(rv)) - } - - public func peek() -> Widget? { - var err = WvError() - let rv = edge_cases_edge_cases_Vault_peek(ptr, &err) - wvTrap(&err) - return rv.map { Widget(ptr: $0) } - } - - /// Spelled like the handle accessor of several wrappers - public func handle() -> Int64 { - var err = WvError() - let rv = edge_cases_edge_cases_Vault_handle(ptr, &err) - wvTrap(&err) - return rv - } - - /// Throwing static - /// - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public static func probe() throws -> Bool { - wvLoad() - var err = WvError() - let rv = edge_cases_edge_cases_Vault_probe(&err) - try wvCheckEdgeCases(&err) - return rv - } - - /// Static spelled like an inherited object member - public static func toString() -> String { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = edge_cases_edge_cases_Vault_to_string(&outLen, &err) - wvTrap(&err) - return wvTakeString(rv, outLen) - } -} - -@available(*, deprecated, message: "Use Widget instead") -extension Vault: WvCodable { - static func wvRead(_ r: inout WvReader) -> Vault { Vault(ptr: r.readObject()) } - func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) } -} - -/// A lazy sequence over the `Widget` elements streamed by `edge_cases_edge_cases_widget_stream`. -/// -/// Each `next()` call pulls exactly one element from the library. The -/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` -/// when iteration is abandoned early. -public final class EdgeCasesWidgetStreamIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - edge_cases_edge_cases_WidgetStreamIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the library, or returns `nil` once the - /// stream is exhausted (destroying the underlying iterator). - public func next() -> Widget? { - guard let handle = handle else { return nil } - var item: OpaquePointer? = nil - var err = WvError() - if edge_cases_edge_cases_WidgetStreamIterator_next(handle, &item, &err) == 0 { - wvTrap(&err) - destroyHandle() - return nil - } - return Widget(ptr: wvNonNull(item)) - } -} - -/// A lazy sequence over the `String` elements streamed by `edge_cases_edge_cases_string_stream`. -/// -/// Each `next()` call pulls exactly one element from the library. The -/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` -/// when iteration is abandoned early. -public final class EdgeCasesStringStreamIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - edge_cases_edge_cases_StringStreamIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the library, or returns `nil` once the - /// stream is exhausted (destroying the underlying iterator). - public func next() -> String? { - guard let handle = handle else { return nil } - var item: UnsafePointer? = nil - var itemLen = 0 - var err = WvError() - if edge_cases_edge_cases_StringStreamIterator_next(handle, &item, &itemLen, &err) == 0 { - wvTrap(&err) - destroyHandle() - return nil - } - return wvTakeString(item, itemLen) - } -} - -/// A lazy sequence over the `Int32` elements streamed by `edge_cases_edge_cases_scalar_stream`. -/// -/// Each `next()` call pulls exactly one element from the library. The -/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` -/// when iteration is abandoned early. -public final class EdgeCasesScalarStreamIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - edge_cases_edge_cases_ScalarStreamIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the library, or returns `nil` once the - /// stream is exhausted (destroying the underlying iterator). - public func next() -> Int32? { - guard let handle = handle else { return nil } - var item: Int32 = 0 - var err = WvError() - if edge_cases_edge_cases_ScalarStreamIterator_next(handle, &item, &err) == 0 { - wvTrap(&err) - destroyHandle() - return nil - } - return item - } -} - -/// A lazy sequence over the `Mode` elements streamed by `edge_cases_edge_cases_mode_stream`. -/// -/// Each `next()` call pulls exactly one element from the library. The -/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` -/// when iteration is abandoned early. -public final class EdgeCasesModeStreamIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - edge_cases_edge_cases_ModeStreamIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the library, or returns `nil` once the - /// stream is exhausted (destroying the underlying iterator). - public func next() -> Mode? { - guard let handle = handle else { return nil } - var item = edge_cases_edge_cases_Mode(rawValue: 0) - var err = WvError() - if edge_cases_edge_cases_ModeStreamIterator_next(handle, &item, &err) == 0 { - wvTrap(&err) - destroyHandle() - return nil - } - return wvEnumCase(Mode.self, numericCast(item.rawValue)) - } -} - -public enum EdgeCases { - /// No parameters and no result - public static func noop() { - wvLoad() - var err = WvError() - edge_cases_edge_cases_noop(&err) - wvTrap(&err) - } - - /// Function named after a keyword whose parameters are all keywords - public static func select(class_: Int32, from: String, func_: [String], range: Int32?, type: Bool) -> Int32 { - wvLoad() - var err = WvError() - let rv: Int32 = wvWithUTF8(from) { from_ptr, from_len in - wvWithEncoded(func_) { func__ptr, func__len in - wvWithEncoded(range) { range_ptr, range_len in - edge_cases_edge_cases_select(class_, from_ptr, from_len, func__ptr, func__len, range_ptr, range_len, type, &err) - } - } - } - wvTrap(&err) - return rv - } - - /// Optional list of optional structs, the deepest legal nesting - public static func nestedOptional(items: [Outer?]?) -> [Outer?]? { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(items) { items_ptr, items_len in - edge_cases_edge_cases_nested_optional(items_ptr, items_len, &outLen, &err) - } - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: [Outer?]?.self) - } - - public static func optionalParams(text: String?, count: Int32?, outer: Outer?, tags: [String]?, blob: Data?, mode: Mode?) -> Mode? { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(text) { text_ptr, text_len in - wvWithEncoded(count) { count_ptr, count_len in - wvWithEncoded(outer) { outer_ptr, outer_len in - wvWithEncoded(tags) { tags_ptr, tags_len in - wvWithEncoded(blob) { blob_ptr, blob_len in - wvWithEncoded(mode) { mode_ptr, mode_len in - edge_cases_edge_cases_optional_params(text_ptr, text_len, count_ptr, count_len, outer_ptr, outer_len, tags_ptr, tags_len, blob_ptr, blob_len, mode_ptr, mode_len, &outLen, &err) - } - } - } - } - } - } - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Mode?.self) - } - - /// Two callback interface parameters on one function - public static func registerBatch(listener: any BatchListener, keyword: any Keyword) { - wvLoad() - var err = WvError() - let listener_ctx = wvRetain(listener) - let keyword_ctx = wvRetain(keyword) - edge_cases_edge_cases_register_batch(listener_ctx, WvBatchListenerVtable.shared.pointer, keyword_ctx, WvKeywordVtable.shared.pointer, &err) - wvTrap(&err) - } - - /// Objects in every buffered position - public static func widgetsEverywhere(list: [Widget], byName: [String: Widget], sparse: [Widget?], holder: WidgetHolder) -> WidgetHolder { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(list) { list_ptr, list_len in - wvWithEncoded(byName) { byName_ptr, byName_len in - wvWithEncoded(sparse) { sparse_ptr, sparse_len in - wvWithEncoded(holder) { holder_ptr, holder_len in - edge_cases_edge_cases_widgets_everywhere(list_ptr, list_len, byName_ptr, byName_len, sparse_ptr, sparse_len, holder_ptr, holder_len, &outLen, &err) - } - } - } - } - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: WidgetHolder.self) - } - - /// Iterator of objects - /// - /// - Returns: A lazy sequence that pulls one element per step from the library. - public static func widgetStream() -> EdgeCasesWidgetStreamIterator { - wvLoad() - var err = WvError() - let rv = edge_cases_edge_cases_widget_stream(&err) - wvTrap(&err) - return EdgeCasesWidgetStreamIterator(handle: wvNonNull(rv)) - } - - /// Async returning an object - /// - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public static func asyncWidget() async throws -> Widget { - wvLoad() - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_widget({ context, err, result in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - cont.resume(throwing: wvTakeError(err, wvMapEdgeCases)) - return - } - cont.resume(returning: Widget(ptr: wvNonNull(result))) - }, context) - } - } - - /// Factory for an interface with no constructor - public static func makeWidget(label: String) -> Widget { - wvLoad() - var err = WvError() - let rv: OpaquePointer? = wvWithUTF8(label) { label_ptr, label_len in - edge_cases_edge_cases_make_widget(label_ptr, label_len, &err) - } - wvTrap(&err) - return Widget(ptr: wvNonNull(rv)) - } - - /// Optional interface return - /// - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public static func findWidget(id: Int64) throws -> Widget? { - wvLoad() - var err = WvError() - let rv = edge_cases_edge_cases_find_widget(id, &err) - try wvCheckEdgeCases(&err) - return rv.map { Widget(ptr: $0) } - } - - /// Interface parameter, including an optional one - public static func inspectWidget(widget: Widget, fallback: Widget?) -> String { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = edge_cases_edge_cases_inspect_widget(widget.ptr, fallback?.ptr, &outLen, &err) - wvTrap(&err) - return wvTakeString(rv, outLen) - } - - /// Iterator of strings - /// - /// - Returns: A lazy sequence that pulls one element per step from the library. - public static func stringStream() -> EdgeCasesStringStreamIterator { - wvLoad() - var err = WvError() - let rv = edge_cases_edge_cases_string_stream(&err) - wvTrap(&err) - return EdgeCasesStringStreamIterator(handle: wvNonNull(rv)) - } - - /// Iterator of scalars - /// - /// - Returns: A lazy sequence that pulls one element per step from the library. - public static func scalarStream() -> EdgeCasesScalarStreamIterator { - wvLoad() - var err = WvError() - let rv = edge_cases_edge_cases_scalar_stream(&err) - wvTrap(&err) - return EdgeCasesScalarStreamIterator(handle: wvNonNull(rv)) - } - - /// Iterator of enums - /// - /// - Returns: A lazy sequence that pulls one element per step from the library. - public static func modeStream() -> EdgeCasesModeStreamIterator { - wvLoad() - var err = WvError() - let rv = edge_cases_edge_cases_mode_stream(&err) - wvTrap(&err) - return EdgeCasesModeStreamIterator(handle: wvNonNull(rv)) - } - - /// Async with no result - public static func asyncVoid() async { - wvLoad() - return await withCheckedContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_void({ context, err in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - wvTrapBoxed(err) - } - cont.resume(returning: ()) - }, context) - } - } - - public static func asyncBytes(size: UInt32) async -> Data { - wvLoad() - return await withCheckedContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_bytes(size, { context, err, resultPtr, resultLen in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - wvTrapBoxed(err) - } - cont.resume(returning: wvTakeBytes(resultPtr, resultLen)) - }, context) - } - } - - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise. - public static func asyncStruct() async throws -> Outer { - wvLoad() - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_struct({ context, err, resultPtr, resultLen in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - cont.resume(throwing: wvTakeError(err, wvMapEdgeCases)) - return - } - cont.resume(returning: wvTakeBuffer(resultPtr, resultLen, as: Outer.self)) - }, context) - } - } - - /// - Throws: ``EdgeErrorsError`` for a declared failure, ``EdgeCasesRuntimeError`` otherwise, and `CancellationError` when the calling task is cancelled, which cancels the native call. - public static func asyncList() async throws -> [Outer] { - wvLoad() - let token = WvCancelToken() - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[Outer], Error>) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_list(token.raw, { context, err, resultPtr, resultLen in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - cont.resume(throwing: wvTakeError(err, wvMapEdgeCases)) - return - } - cont.resume(returning: wvTakeBuffer(resultPtr, resultLen, as: [Outer].self)) - }, context) - } - } onCancel: { - token.cancel() - } - } - - public static func asyncOptional() async -> Int64? { - wvLoad() - return await withCheckedContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_optional({ context, err, resultPtr, resultLen in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - wvTrapBoxed(err) - } - cont.resume(returning: wvTakeBuffer(resultPtr, resultLen, as: Int64?.self)) - }, context) - } - } - - public static func asyncEnum() async -> Mode { - wvLoad() - return await withCheckedContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - edge_cases_edge_cases_async_enum({ context, err, result in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - wvTrapBoxed(err) - } - cont.resume(returning: wvEnumCase(Mode.self, numericCast(result.rawValue))) - }, context) - } - } - - public static func asyncKeywords(class_: String, import_: Int32) async -> String { - wvLoad() - return await withCheckedContinuation { (continuation: CheckedContinuation) in - let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() - wvWithUTF8(class_) { class__ptr, class__len in - edge_cases_edge_cases_async_keywords(class__ptr, class__len, import_, { context, err, resultPtr, resultLen in - let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value - if let err = err { - wvTrapBoxed(err) - } - cont.resume(returning: wvTakeString(resultPtr, resultLen)) - }, context) - } - } - } - - public static func keywordsRoundtrip(value: Keywords) -> Keywords { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(value) { value_ptr, value_len in - edge_cases_edge_cases_keywords_roundtrip(value_ptr, value_len, &outLen, &err) - } - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Keywords.self) - } - - @available(*, deprecated, message: "Use Mode-based APIs") - public static func legacyValue(value: Legacy) -> Retired { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy(rawValue: numericCast(value.rawValue)), &outLen, &err) - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Retired.self) - } -} - -// END EdgeCases.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Package_swift.snap deleted file mode 100644 index 01417bcc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Package_swift.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// swift-tools-version:5.9 -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import Foundation -import PackageDescription - -// The C module `CKitchenSink` comes from one of two places. Next to this -// manifest, a prebuilt `CKitchenSink.xcframework` (from `weaveffi package`) wins. -// Otherwise `Sources/CKitchenSink` declares the generated header as a system -// library that links `kitchen_sink` from the linker search path, e.g. -// `swift build -Xlinker -L/path/to/lib`. -let xcframework = "CKitchenSink.xcframework" -let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") - ? .binaryTarget(name: "CKitchenSink", path: xcframework) - : .systemLibrary(name: "CKitchenSink") - -let package = Package( - name: "KitchenSink", - platforms: [.macOS("11.0"), .iOS("13.0")], - products: [ - .library(name: "KitchenSink", targets: ["KitchenSink"]), - ], - targets: [ - cModule, - .target(name: "KitchenSink", dependencies: ["CKitchenSink"]), - ] -) - -// END Package.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_module_modulemap.snap deleted file mode 100644 index 1eaaf986..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_module_modulemap.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -module CKitchenSink [system] { - header "kitchen_sink.h" - link "kitchen_sink" - export * -} - -// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap index 85e77d8c..a8c8bc75 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap @@ -8,695 +8,200 @@ source: crates/weaveffi-cli/tests/snapshots.rs import CKitchenSink import Foundation -// MARK: - Runtime support - -/// A failure the native library reports outside any declared error domain: -/// an unknown error code or a runtime failure. Throwing wrappers raise it; -/// non-throwing wrappers stop the process with `fatalError` instead, since -/// Swift has no unchecked errors. -public struct KitchenSinkRuntimeError: Error, LocalizedError, Hashable, Sendable { - /// The code of a generic runtime failure. - public static let genericCode: Int32 = -1 - /// The code of a panic caught at the library boundary. - public static let panicCode: Int32 = -2 - /// The code of an argument or result that failed to marshal. - public static let marshalCode: Int32 = -3 - /// The code of a callback implementation that threw. - public static let foreignCode: Int32 = -4 - - /// The numeric code the native library reported. - public let errorCode: Int32 - /// The message the native library reported. - public let message: String - - /// Creates an error carrying a native code and message. - public init(errorCode: Int32, message: String) { - self.errorCode = errorCode - self.message = message - } - - public var errorDescription: String? { message } -} - -/// The error slot every native call reports through. -typealias WvError = kitchen_sink_error - -/// The C ABI revision these bindings were generated against. -let wvAbiVersion: UInt32 = 4 - -/// Runs the load-time checks if they haven't run yet. -@inline(__always) -func wvLoad() { - _ = wvContract -} - -/// Stops the process unless the library implements the ABI revision these -/// bindings were generated against. -func wvCheckAbiVersion() { - let abi = kitchen_sink_abi_version() - guard abi == wvAbiVersion else { - fatalError("KitchenSink: the native library 'kitchen_sink' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") - } -} +// MARK: - Contract -/// Stops the process unless every declaration these bindings were generated -/// with is in a top-level module's contract table with an equal signature -/// hash. Declarations the library adds are fine. -func wvCheckContract( - _ table: (UnsafeMutablePointer?) -> UnsafePointer?, - _ expected: [(id: UInt64, hash: UInt64, path: StaticString)] -) { - var len = 0 - let entries = UnsafeBufferPointer(start: table(&len), count: len) - var hashes: [UInt64: UInt64] = [:] - for entry in entries { - hashes[entry.id] = entry.hash - } - for (id, hash, path) in expected { - guard let actual = hashes[id] else { - fatalError("KitchenSink: \(path) is missing from the library 'kitchen_sink'; regenerate the bindings or rebuild the library") - } - guard actual == hash else { - fatalError("KitchenSink: \(path) changed since these bindings were generated; regenerate the bindings or rebuild the library 'kitchen_sink'") - } - } -} - -/// The message carried by an error slot. -func wvErrorMessage(_ err: WvError) -> String { - err.message.map { String(cString: $0) } ?? "" -} - -/// The Swift error for a code outside any declared domain: cancellation -/// (`-5`) becomes `CancellationError`, anything else the runtime error. -func wvRuntimeError(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - return KitchenSinkRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) -} - -/// Stops the process for a failed call that can't throw, naming the code and -/// message the library reported. -func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { - fatalError("KitchenSink.\(function) failed with code \(code): \(message)") -} - -/// Throws the runtime error for a filled error slot, clearing it. -@inline(__always) -func wvCheck(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvRuntimeError(err) - kitchen_sink_error_clear(&err) - throw error -} - -/// Stops the process for a filled error slot of a call that can't throw. -@inline(__always) -func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { - guard err.code != 0 else { return } - let code = err.code - let message = wvErrorMessage(err) - kitchen_sink_error_clear(&err) - wvFatal(code, message, function) -} - -/// Maps and releases the heap-boxed error an async completion received. -func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { - let error = map(err.pointee) - kitchen_sink_error_free(err) - return error -} - -/// The error mapping of a cancellable call that declares no error domain: -/// cancellation throws `CancellationError`, anything else stops the process. -func wvCancelledOrTrap(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - wvFatal(err.code, wvErrorMessage(err), "async call") -} - -/// Stops the process for the boxed error of an async call that can't throw. -func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { - wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) -} - -/// Unwraps a pointer the library promised is non-null. -@inline(__always) -func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { - guard let ptr = ptr else { - wvFatal(KitchenSinkRuntimeError.marshalCode, "the native library returned a null pointer", function) - } - return ptr -} - -/// The Swift enum case for a C-style enum discriminant. -func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { - guard let value = E(rawValue: raw) else { - wvDecodeFailure("unknown \(E.self) discriminant \(raw)") - } - return value -} - -// MARK: Strings, bytes, and buffers - -/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. -@inline(__always) -func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var s = s - return try s.withUTF8 { try body($0.baseAddress, $0.count) } -} - -/// Lends the bytes of `d` for one call. -@inline(__always) -func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } -} - -/// Encodes `value` as a value buffer and lends it for one call. -@inline(__always) -func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var w = WvWriter() - w.write(value) - return try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } -} - -/// Copies a returned string, then releases the library's allocation. -func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - defer { kitchen_sink_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies returned bytes, then releases the library's allocation. -func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - defer { kitchen_sink_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return Data(bytes: ptr, count: len) -} - -/// Decodes a returned value buffer in place, then releases it. -func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - defer { - if let ptr = ptr { kitchen_sink_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - } - return wvBorrowBuffer(ptr, len, as: type) -} - -/// Copies a borrowed string. -func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies borrowed bytes. -func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - return Data(bytes: ptr, count: len) -} - -/// Decodes a borrowed value buffer in place. -func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - var r = WvReader(UnsafeRawPointer(ptr), len) - let value: T = r.read() - r.finish() - return value -} - -/// Stops the process on a malformed value buffer, which the wire format -/// treats like a marshalling failure. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("KitchenSink: malformed value buffer: \(context)") -} - -/// Serializes values in the value-buffer wire format: little-endian, packed, -/// no alignment. -struct WvWriter { - var bytes: [UInt8] = [] - - init() { - bytes.reserveCapacity(64) - } - - @inline(__always) - private mutating func append(_ v: T) { - withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } - } - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeI8(_ v: Int8) { append(v) } - mutating func writeU8(_ v: UInt8) { append(v) } - mutating func writeI16(_ v: Int16) { append(v) } - mutating func writeU16(_ v: UInt16) { append(v) } - mutating func writeI32(_ v: Int32) { append(v) } - mutating func writeU32(_ v: UInt32) { append(v) } - mutating func writeI64(_ v: Int64) { append(v) } - mutating func writeU64(_ v: UInt64) { append(v) } - mutating func writeF32(_ v: Float) { append(v.bitPattern) } - mutating func writeF64(_ v: Double) { append(v.bitPattern) } - - /// Writes a `u32` length or element count. - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") - append(UInt32(n)) - } - - mutating func writeString(_ v: String) { - let utf8 = v.utf8 - writeLen(utf8.count) - bytes.append(contentsOf: utf8) - } - - mutating func writeBytes(_ v: Data) { - writeLen(v.count) - bytes.append(contentsOf: v) - } - - mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } - - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } - - /// Writes any value that crosses inside a value buffer. - @inline(__always) - mutating func write(_ value: T) { - value.wvWrite(&self) - } +/// The first declaration these bindings were generated with that the library +/// lacks or declares differently, checked once by `KitchenSinkLibrary.check()`. +func wvCheckContracts() -> KitchenSinkLibrary.LoadError? { + if let failure = wvCheckContract(kitchen_sink_shared_contract, [ + (0x709e234caa962446, 0x3894f2ca452e9f98, "shared.ping"), + (0xcfb306fa5801ee5b, 0x18e9fc204a13df6f, "shared.Token"), + ]) { + return failure + } + return wvCheckContract(kitchen_sink_kitchen_contract, [ + (0x01d4f09bbddc986f, 0xffcf2bd3b04aa002, "kitchen.Gadget.new"), + (0x02bbd5f31c953704, 0x2cfa679ea1373e09, "kitchen.Sampler.samples"), + (0x10472d8fe748c2ee, 0xcc570a2649bbf7dc, "kitchen.maybe_gadget"), + (0x11368eec07a74078, 0xfa049f78b77e146a, "kitchen.gadgets"), + (0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, "kitchen.stream_gadgets"), + (0x184bfc4ab0e3fe45, 0xb64587477022696b, "kitchen.ids_u64"), + (0x2111b54980826dd7, 0x627bf01ab365b209, "kitchen.do_cancellable"), + (0x21c70f9371374d74, 0x25d5d88a30fe0a79, "kitchen.ReadyListener.favorite"), + (0x23de5bd597372f46, 0xcf1bc6bb37872798, "kitchen.stream_chunks"), + (0x249f1ddaec6a6cbe, 0x0710d6c9a4de93da, "kitchen.take_from_pantry"), + (0x29dddbc60bf8d7db, 0x452a3e573d57f6db, "kitchen.Priority"), + (0x2b227003b74a0d16, 0x5dae036b8a6aed30, "kitchen.maybe_subscribe"), + (0x2dd66cb0064f0494, 0x74b131724dd0d867, "kitchen.Item"), + (0x33c265bbf079b0e9, 0x97c32b764693f2cb, "kitchen.u32_id"), + (0x33e6381d01660dfe, 0x05e35a563089bf7d, "kitchen.KitchenErrors"), + (0x3d969ae76db6cb23, 0x65a325cfc772296b, "kitchen.ReadyListener.latest"), + (0x3f5fc99d73504b76, 0x417e4a2acd3210b3, "kitchen.Gadget.poke"), + (0x4082f0085db28781, 0x18848e701962912e, "kitchen.Sampler"), + (0x4340a6b1e7ec8c50, 0xa59d0f32a66dc7a2, "kitchen.Sampler.describe"), + (0x4c5764cf109528d1, 0xb374accf14676d90, "kitchen.maybe_item"), + (0x4de17dfb93fd494d, 0x78be3203bcd10727, "kitchen.i32_id"), + (0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, "kitchen.stream_items"), + (0x5dafabb7775fcb2d, 0x6c4b78c7d3f858c1, "kitchen.PantryError.Spoiled"), + (0x5e0a8f6be4b00600, 0x18ea1a42036c96a0, "kitchen.Sampler.threshold"), + (0x5ec0af1d4ecc6fed, 0x6540bdb74246420c, "kitchen.async_samples"), + (0x61a358b0571e0a4e, 0x11a24c4b50d11996, "kitchen.async_maybe_count"), + (0x68cdb9a4bd3e3dd5, 0xcde022fe6bc3b1bf, "kitchen.bool_id"), + (0x69e7cd703d16eb85, 0xb13cb8d39e702c79, "kitchen.do_async"), + (0x70865079ce41be82, 0xbe3b69464672af46, "kitchen.Gadget.describe"), + (0x71c5a1116cb7c49a, 0x1506cfe6f548fac4, "kitchen.echo_bytes"), + (0x735ee1e0049ee91a, 0xa713dbd1be707752, "kitchen.echo_string"), + (0x76d7fc58f4e41d84, 0x6bdb4ccea2841d5e, "kitchen.i64_id"), + (0x78d91fd617e162b8, 0xd88e2753503b36df, "kitchen.stream_maybe"), + (0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, "kitchen.nested.hello"), + (0x7b3865b536da998c, 0x1c4e79d60a20e74f, "kitchen.mixed_slices"), + (0x7ba266dc26a58989, 0x0f86001cac0c21e8, "kitchen.KitchenErrors.InvalidInput"), + (0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, "kitchen.new_op"), + (0x81ded69adee0abf2, 0x8c71141a931906f6, "kitchen.scale_i32"), + (0x81fa72769330fed6, 0x89681e305b55a4f4, "kitchen.ReadyListener.label"), + (0x89a5e664bc25c67b, 0x357fd9b65ad879a9, "kitchen.f64_id"), + (0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, "kitchen.Gadget.version"), + (0x8dc42397e47614b9, 0xcddf6fede09b2e7f, "kitchen.opt_priority"), + (0x92f060ef68961fc1, 0x4af6ce5b4f9fa508, "kitchen.ReadyListener.on_item"), + (0x974b9f3e823c1738, 0xb81b91e975695f27, "kitchen.cross_module_token"), + (0x9f0f001aa85f97d5, 0x55815247f895bd45, "kitchen.sum_f64"), + (0xab2a29fd1ef5458b, 0x471a34aa4c09ae2b, "kitchen.KitchenErrors.NotFound"), + (0xaca0655fac15d230, 0x11c4d2b28abe07bb, "kitchen.list_items"), + (0xb57c8e6ae3fb4383, 0x3f857851ab75366c, "kitchen.PantryError"), + (0xbdc795b195edd428, 0x36127462f8b0969f, "kitchen.opt_mixed"), + (0xbe4821ab3c340e71, 0xe1d32e38b766e623, "kitchen.ReadyListener.on_ready"), + (0xc2ad53f2a6a36323, 0x071e29abee823f17, "kitchen.opt_i32_id"), + (0xc7ab409554fd3d7b, 0xfea12b6bc219fc46, "kitchen.parse_any"), + (0xc82a7f2d85f99f4e, 0xa9bcb8bacedb036c, "kitchen.sample_with"), + (0xc883b4afae20543e, 0xc40560e5411ff392, "kitchen.legacy_op"), + (0xd180bf646bff59e2, 0xa4515a6d00d7c600, "kitchen.PantryError.OutOfStock"), + (0xe6778df0f5c1b82b, 0x38eaafdb350559e1, "kitchen.opt_bool_id"), + (0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, "kitchen.get_attrs"), + (0xfae25d3391922ead, 0xcc034cf506cae182, "kitchen.subscribe"), + (0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, "kitchen.Gadget"), + (0xfed9c551fb9fd2d6, 0xe1020573f084dc1d, "kitchen.ReadyListener"), + (0xff3a99b52597dee2, 0x14c4ecac97bdfc9b, "kitchen.Gadget.readings"), + ]) } -/// Deserializes values from the value-buffer wire format directly from the -/// library's memory, rejecting truncated buffers, invalid flag bytes, -/// oversized length prefixes, and trailing bytes. -struct WvReader { - private let base: UnsafeRawPointer? - private let count: Int - private var pos = 0 - - init(_ base: UnsafeRawPointer?, _ count: Int) { - self.base = base - self.count = base == nil ? 0 : count - } - - /// The number of unread bytes. - var remaining: Int { count - pos } - - private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { - guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } - defer { pos += n } - return base + pos - } - - @inline(__always) - private mutating func load(_ type: T.Type, _ what: StaticString) -> T { - T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) - } - - mutating func readBool() -> Bool { - switch load(UInt8.self, "bool") { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readI8() -> Int8 { load(Int8.self, "i8") } - mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } - mutating func readI16() -> Int16 { load(Int16.self, "i16") } - mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } - mutating func readI32() -> Int32 { load(Int32.self, "i32") } - mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } - mutating func readI64() -> Int64 { load(Int64.self, "i64") } - mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } - mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } - mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } - - /// Reads a byte length, which can't exceed the unread bytes. - mutating func readLen() -> Int { - let n = Int(load(UInt32.self, "length")) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - - /// Reads an element count. Elements may encode to zero bytes, so the - /// count isn't bounded by the unread bytes; callers cap preallocation. - mutating func readCount() -> Int { - Int(load(UInt32.self, "count")) - } - - mutating func readString() -> String { - let n = readLen() - if n == 0 { return "" } - let p = take(n, "string") - guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } +// MARK: - API - mutating func readBytes() -> Data { - let n = readLen() - if n == 0 { return Data() } - return Data(bytes: take(n, "bytes"), count: n) - } +/// The `KitchenErrors` error domain of the `kitchen` module. +/// +/// Every case carries the message the library reported (the code's +/// documented message when it sent none) and the code's fields. +public enum KitchenError: Error, LocalizedError, Hashable, Sendable { + case notFound(message: String) + case invalidInput(message: String) + /// A code these bindings don't declare, from a newer library. + case unknown(code: Int32, message: String) - mutating func readOptionFlag() -> Bool { - switch load(UInt8.self, "option flag") { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") + /// The numeric code the library reported. + public var errorCode: Int32 { + switch self { + case .notFound: return 1 + case .invalidInput: return 2 + case let .unknown(code, _): return code } } - /// Reads an object token: one strong reference the caller adopts into a - /// wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { - wvDecodeFailure("null object token") + /// The message the library reported, or the code's documented message. + public var message: String { + switch self { + case let .notFound(message): return message + case let .invalidInput(message): return message + case let .unknown(_, message): return message } - return p - } - - /// Reads any value that crosses inside a value buffer, typed by context. - @inline(__always) - mutating func read() -> T { - T.wvRead(&self) - } - - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -// MARK: Value-buffer codecs - -/// A type that crosses the C ABI inside a value buffer. Every primitive, -/// optional, list, and map conforms here; each record, enum, and interface -/// conforms where it's declared. -protocol WvCodable { - /// Reads one value, adopting the reference of any object token. - static func wvRead(_ r: inout WvReader) -> Self - /// Writes this value, writing a fresh reference for any object. - func wvWrite(_ w: inout WvWriter) -} - -/// A C-style enum, which crosses as its `i32` discriminant. -protocol WvCEnum: WvCodable, RawRepresentable where RawValue == Int32 {} - -extension WvCEnum { - static func wvRead(_ r: inout WvReader) -> Self { wvEnumCase(Self.self, r.readI32()) } - func wvWrite(_ w: inout WvWriter) { w.writeI32(rawValue) } -} - -extension Bool: WvCodable { - static func wvRead(_ r: inout WvReader) -> Bool { r.readBool() } - func wvWrite(_ w: inout WvWriter) { w.writeBool(self) } -} - -extension Int8: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int8 { r.readI8() } - func wvWrite(_ w: inout WvWriter) { w.writeI8(self) } -} - -extension UInt8: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt8 { r.readU8() } - func wvWrite(_ w: inout WvWriter) { w.writeU8(self) } -} - -extension Int16: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int16 { r.readI16() } - func wvWrite(_ w: inout WvWriter) { w.writeI16(self) } -} - -extension UInt16: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt16 { r.readU16() } - func wvWrite(_ w: inout WvWriter) { w.writeU16(self) } -} - -extension Int32: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int32 { r.readI32() } - func wvWrite(_ w: inout WvWriter) { w.writeI32(self) } -} - -extension UInt32: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt32 { r.readU32() } - func wvWrite(_ w: inout WvWriter) { w.writeU32(self) } -} - -extension Int64: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int64 { r.readI64() } - func wvWrite(_ w: inout WvWriter) { w.writeI64(self) } -} - -extension UInt64: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt64 { r.readU64() } - func wvWrite(_ w: inout WvWriter) { w.writeU64(self) } -} - -extension Float: WvCodable { - static func wvRead(_ r: inout WvReader) -> Float { r.readF32() } - func wvWrite(_ w: inout WvWriter) { w.writeF32(self) } -} - -extension Double: WvCodable { - static func wvRead(_ r: inout WvReader) -> Double { r.readF64() } - func wvWrite(_ w: inout WvWriter) { w.writeF64(self) } -} - -extension String: WvCodable { - static func wvRead(_ r: inout WvReader) -> String { r.readString() } - func wvWrite(_ w: inout WvWriter) { w.writeString(self) } -} - -extension Data: WvCodable { - static func wvRead(_ r: inout WvReader) -> Data { r.readBytes() } - func wvWrite(_ w: inout WvWriter) { w.writeBytes(self) } -} - -extension Optional: WvCodable where Wrapped: WvCodable { - static func wvRead(_ r: inout WvReader) -> Wrapped? { - r.readOptionFlag() ? .some(r.read()) : nil } - func wvWrite(_ w: inout WvWriter) { - w.writeOptionFlag(self != nil) - if let value = self { w.write(value) } - } + public var errorDescription: String? { message } } -extension Array: WvCodable where Element: WvCodable { - static func wvRead(_ r: inout WvReader) -> [Element] { - let n = r.readCount() - var items: [Element] = [] - items.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0..?) { + switch self { + case let .notFound(message): + wvSetError(outErr, 1, message) + case let .invalidInput(message): + wvSetError(outErr, 2, message) + case let .unknown(code, message): + wvSetError(outErr, code, message) + } } } -extension Dictionary: WvCodable where Key: WvCodable, Value: WvCodable { - static func wvRead(_ r: inout WvReader) -> [Key: Value] { - let n = r.readCount() - var map: [Key: Value] = [:] - map.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. { - let value: CheckedContinuation - - init(_ value: CheckedContinuation) { - self.value = value - } -} - -/// One process-wide callback vtable at a stable address the library may hold -/// for the life of the process. -final class WvVtable: @unchecked Sendable { - let pointer: UnsafePointer - init(_ value: T) { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - pointer = UnsafePointer(cell) - } -} - -/// Holds one callback implementation behind the `ctx` the library keeps. -final class WvBox: @unchecked Sendable { - let value: T - - init(_ value: T) { - self.value = value - } -} - -/// Retains `impl` for the library: the `ctx` of a callback parameter, which -/// the vtable's `free` entry releases with ``wvRelease(_:as:)``. -func wvRetain(_ impl: T) -> UnsafeMutableRawPointer { - Unmanaged.passRetained(WvBox(impl)).toOpaque() -} - -/// Releases the implementation behind `ctx`, from the vtable's `free` entry. -func wvRelease(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) { - Unmanaged>.fromOpaque(ctx!).release() + public var errorDescription: String? { message } } -/// Runs one callback method on the implementation behind `ctx`. A thrown -/// error never unwinds through the library: `domain` reports it when it's -/// the method's declared error, anything else is reported as a callback -/// failure (`-4`), and the method returns `fallback`. -func wvInvoke( - _ ctx: UnsafeMutableRawPointer?, - _ outErr: UnsafeMutablePointer?, - as type: T.Type, - fallback: R, - domain: (Error, UnsafeMutablePointer?) -> Bool = { _, _ in false }, - _ body: (T) throws -> R -) -> R { - let impl = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value - do { - return try body(impl) - } catch { - if !domain(error, outErr) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { kitchen_sink_error_set(outErr, KitchenSinkRuntimeError.foreignCode, $0) } +extension PantryError: WvDomain { + init(wvError err: WvError) { + let message = wvErrorMessage(err) + switch err.code { + case 1: + var r = wvPayload(err) + self = .outOfStock(message: message.isEmpty ? "Out of stock" : message, item: r.read()) + r.finish() + case 2: + self = .spoiled(message: message.isEmpty ? "Spoiled" : message) + default: + self = .unknown(code: err.code, message: message) } - return fallback - } -} - -/// Reports a declared domain error from a callback method: its code, its -/// message, and its fields as the payload. -func wvSetError(_ outErr: UnsafeMutablePointer?, _ code: Int32, _ message: String, _ payload: WvWriter? = nil) { - message.withCString { kitchen_sink_error_set(outErr, code, $0) } - if let payload = payload { - payload.bytes.withUnsafeBufferPointer { kitchen_sink_error_set_payload(outErr, $0.baseAddress, $0.count) } } -} -/// Hands `bytes` to the library as a callback method's return: a run -/// allocated with `kitchen_sink_alloc` that the library adopts. -func wvHandOver(_ bytes: UnsafeRawBufferPointer, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - let run = kitchen_sink_alloc(bytes.count) - if let run = run, let base = bytes.baseAddress { - run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) + func wvReport(_ outErr: UnsafeMutablePointer?) { + switch self { + case let .outOfStock(message, v0): + var payload = WvWriter() + payload.write(v0) + wvSetError(outErr, 1, message, payload) + case let .spoiled(message): + wvSetError(outErr, 2, message) + case let .unknown(code, message): + wvSetError(outErr, code, message) + } } - outPtr?.pointee = run - outLen?.pointee = run == nil ? 0 : bytes.count -} - -/// Returns a string from a callback method through its out slots. -func wvReturnString(_ value: String, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var value = value - value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0), outPtr, outLen) } } -/// Returns bytes from a callback method through its out slots. -func wvReturnBytes(_ value: Data, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - value.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -/// Returns a value buffer from a callback method through its out slots. -func wvReturnBuffer(_ value: T, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var w = WvWriter() - w.write(value) - w.bytes.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -// MARK: - Load-time checks - -/// The load-time checks, run once before the first native call: the library -/// must implement C ABI revision 4 and carry every declaration these -/// bindings were generated with, unchanged. -let wvContract: Void = { - wvCheckAbiVersion() - wvCheckContract(kitchen_sink_shared_contract, [ - (0x709e234caa962446, 0x3894f2ca452e9f98, "shared.ping"), - (0xcfb306fa5801ee5b, 0x1cbd9e10983cac2c, "shared.Token"), - ]) - wvCheckContract(kitchen_sink_kitchen_contract, [ - (0x01d4f09bbddc986f, 0x29eb88fecc66544d, "kitchen.Gadget.new"), - (0x10472d8fe748c2ee, 0xb265832b6a759fc4, "kitchen.maybe_gadget"), - (0x11368eec07a74078, 0xfa049f78b77e146a, "kitchen.gadgets"), - (0x137f08a29cde6021, 0xda8de7aeb2dbb8a3, "kitchen.stream_gadgets"), - (0x2111b54980826dd7, 0x452783f3e6ca6e1d, "kitchen.do_cancellable"), - (0x29dddbc60bf8d7db, 0x452a3e573d57f6db, "kitchen.Priority"), - (0x2b227003b74a0d16, 0x12332957470e9782, "kitchen.maybe_subscribe"), - (0x2dd66cb0064f0494, 0x60197363fefd92dd, "kitchen.Item"), - (0x33c265bbf079b0e9, 0xc3dcd0927e6c1deb, "kitchen.u32_id"), - (0x33e6381d01660dfe, 0x077c63c31fd7f90a, "kitchen.KitchenErrors"), - (0x3f5fc99d73504b76, 0x2df88d710f1ea10e, "kitchen.Gadget.poke"), - (0x4c5764cf109528d1, 0x2a65529af4338a58, "kitchen.maybe_item"), - (0x4de17dfb93fd494d, 0xba0ad0dd52ed74af, "kitchen.i32_id"), - (0x504e95b7f7e65570, 0x01b3e8e0c9a10e23, "kitchen.stream_items"), - (0x68cdb9a4bd3e3dd5, 0xc678b9164348d497, "kitchen.bool_id"), - (0x69e7cd703d16eb85, 0xf3b28dd22ae9e689, "kitchen.do_async"), - (0x70865079ce41be82, 0xbe3b69464672af46, "kitchen.Gadget.describe"), - (0x71c5a1116cb7c49a, 0xcba1837108a89b84, "kitchen.echo_bytes"), - (0x735ee1e0049ee91a, 0xca74320bc53eddab, "kitchen.echo_string"), - (0x76d7fc58f4e41d84, 0x7373a0e47d30e690, "kitchen.i64_id"), - (0x79abfe1aa960fd6a, 0xbca7a9e5eb26f3a6, "kitchen.nested.hello"), - (0x7d3dd7d5cf6749e3, 0xa192f5a7298adb39, "kitchen.new_op"), - (0x89a5e664bc25c67b, 0x5764a95e81ecfd31, "kitchen.f64_id"), - (0x8cfdbdcecac5f7f1, 0x07e272f09c41fffc, "kitchen.Gadget.version"), - (0x974b9f3e823c1738, 0xb81b91e975695f27, "kitchen.cross_module_token"), - (0xaca0655fac15d230, 0x11c4d2b28abe07bb, "kitchen.list_items"), - (0xc883b4afae20543e, 0xc40560e5411ff392, "kitchen.legacy_op"), - (0xf2f7753e6e1dffc2, 0xc9047a95bdf49e93, "kitchen.get_attrs"), - (0xfae25d3391922ead, 0x1c7f95739f3210b8, "kitchen.subscribe"), - (0xfd0059b05cb2fcfb, 0x248bf8404a5823b4, "kitchen.Gadget"), - (0xfed9c551fb9fd2d6, 0x59aec980ef3f3291, "kitchen.ReadyListener"), - ]) -}() - -// MARK: - API - /// A reusable token referenced across modules public struct Token: Hashable, Sendable { public var id: Int64 @@ -720,60 +225,8 @@ extension Token: WvCodable { } } -/// Typed errors reported by the `kitchen` module. -public enum KitchenErrorsError: Error, LocalizedError, Sendable { - case notFound(message: String) - case invalidInput(message: String) - - /// The message the library reported, or the code's documented default. - public var errorDescription: String? { - switch self { - case let .notFound(message): return message - case let .invalidInput(message): return message - } - } - - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .notFound: return 1 - case .invalidInput: return 2 - } - } -} - -func wvMapKitchen(_ err: WvError) -> Error { - let message = wvErrorMessage(err) - switch err.code { - case 1: return KitchenErrorsError.notFound(message: message.isEmpty ? "Item not found" : message) - case 2: return KitchenErrorsError.invalidInput(message: message.isEmpty ? "Invalid input" : message) - default: return wvRuntimeError(err) - } -} - -func wvCheckKitchen(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvMapKitchen(err) - kitchen_sink_error_clear(&err) - throw error -} - -/// Reports a ``KitchenErrorsError`` thrown by a callback method declared to throw it: its -/// code, its message, and its fields as the payload. Any other error is left -/// to the caller, which reports a callback failure. -func wvReportKitchen(_ error: Error, _ outErr: UnsafeMutablePointer?) -> Bool { - guard let error = error as? KitchenErrorsError else { return false } - switch error { - case let .notFound(message): - wvSetError(outErr, 1, message) - case let .invalidInput(message): - wvSetError(outErr, 2, message) - } - return true -} - /// Task priority levels -public enum Priority: Int32, Sendable { +public enum Priority: Int32, CaseIterable, Sendable { /// Low priority case low = 0 /// Normal priority @@ -785,7 +238,7 @@ public enum Priority: Int32, Sendable { extension Priority: WvCEnum {} /// Kitchen sink struct exercising every field feature -public struct Item: Sendable { +public struct Item: Hashable, Sendable { /// Stable identifier public var id: Int64 /// Display name @@ -851,70 +304,116 @@ extension Item: WvCodable { /// /// Implement this protocol with a class and pass an instance where the API /// expects it. The library keeps the instance until it no longer needs it and -/// may call any method from any thread. A method that throws fails the -/// library's call in progress: a method documented to throw a module error -/// reports that error, with its code and fields, and any other error reaches -/// the library as a callback failure (code -4). +/// may call any method from any thread. public protocol ReadyListener: AnyObject, Sendable { /// Fires when an item is ready - func onReady(code: Int32, msg: String) throws + func onReady(code: Int32, msg: String) /// Receives the item itself and says whether to keep listening - func onItem(item: Item, gadget: Gadget) throws -> Bool + func onItem(item: Item, gadget: Gadget) -> Bool /// A display label, failing with a kitchen error /// - /// - Throws: ``KitchenErrorsError`` to report a declared failure with its fields, or any other error to fail the call. + /// - Throws: ``KitchenError`` to fail the library's call with that error and + /// its fields; any other error fails it with the error's description. func label() throws -> String /// The most recent item the listener kept, if any - func latest() throws -> Item? + func latest() -> Item? /// The listener's favorite gadget - func favorite() throws -> Gadget + func favorite() -> Gadget } /// The process-wide vtable the library calls `ReadyListener` implementations -/// through (`kitchen_sink_kitchen_ReadyListener_vtable`). Every entry recovers the implementation -/// from `ctx`, copies or adopts its arguments, hands its result back, and -/// reports a thrown error through `out_err` instead of unwinding. +/// through (a `kitchen_sink_kitchen_ReadyListener_vtable`). Every entry +/// recovers the implementation from `ctx`, copies or adopts its arguments, +/// hands its result back, and reports a thrown error through `out_err` instead +/// of unwinding. enum WvReadyListenerVtable { static let shared = WvVtable(kitchen_sink_kitchen_ReadyListener_vtable( size: UInt32(MemoryLayout.stride), flags: 0, - free: { ctx in - wvRelease(ctx, as: (any ReadyListener).self) - }, + free: { ctx in wvRelease(ctx, as: (any ReadyListener).self) }, on_ready: { ctx, code, msg_ptr, msg_len, out_err in wvInvoke(ctx, out_err, as: (any ReadyListener).self, fallback: ()) { wvImpl in - try wvImpl.onReady(code: code, msg: wvBorrowString(msg_ptr, msg_len)) + wvImpl.onReady(code: code, msg: wvBorrowString(msg_ptr, msg_len)) } }, on_item: { ctx, item_ptr, item_len, gadget, out_err in wvInvoke(ctx, out_err, as: (any ReadyListener).self, fallback: false) { wvImpl in - try wvImpl.onItem(item: wvBorrowBuffer(item_ptr, item_len, as: Item.self), gadget: Gadget(ptr: wvNonNull(gadget))) + wvImpl.onItem(item: wvBorrowBuffer(item_ptr, item_len, as: Item.self), gadget: Gadget(ptr: wvNonNull(gadget))) } }, label: { ctx, out_ptr, out_len, out_err in - wvInvoke(ctx, out_err, as: (any ReadyListener).self, fallback: (), domain: wvReportKitchen) { wvImpl in + wvInvoke(ctx, out_err, as: (any ReadyListener).self, throwing: KitchenError.self, fallback: ()) { wvImpl in try wvReturnString(wvImpl.label(), out_ptr, out_len) } }, latest: { ctx, out_ptr, out_len, out_err in wvInvoke(ctx, out_err, as: (any ReadyListener).self, fallback: ()) { wvImpl in - try wvReturnBuffer(wvImpl.latest(), out_ptr, out_len) + wvReturnBuffer(wvImpl.latest(), out_ptr, out_len) } }, favorite: { ctx, out_err in wvInvoke(ctx, out_err, as: (any ReadyListener).self, fallback: nil) { wvImpl in - try wvImpl.favorite().clonePtr() + wvImpl.favorite().clonePtr() + } + } + )) +} + +/// Consumer-implemented source of scalar samples +/// +/// Implement this protocol with a class and pass an instance where the API +/// expects it. The library keeps the instance until it no longer needs it and +/// may call any method from any thread. +public protocol Sampler: AnyObject, Sendable { + /// The cutoff for a level, if the sampler has one + func threshold(level: UInt32?) -> Double? + + /// Up to `limit` samples, failing when the pantry is empty + /// + /// - Throws: ``PantryError`` to fail the library's call with that error and + /// its fields; any other error fails it with the error's description. + func samples(limit: UInt32?, weights: [Double]) throws -> [Int16] + + /// A free-form description, failing with an untyped error + /// + /// - Throws: Any error, which fails the library's call with the error's + /// description. + func describe() throws -> String +} + +/// The process-wide vtable the library calls `Sampler` implementations through +/// (a `kitchen_sink_kitchen_Sampler_vtable`). Every entry recovers the +/// implementation from `ctx`, copies or adopts its arguments, hands its result +/// back, and reports a thrown error through `out_err` instead of unwinding. +enum WvSamplerVtable { + static let shared = WvVtable(kitchen_sink_kitchen_Sampler_vtable( + size: UInt32(MemoryLayout.stride), + flags: 0, + free: { ctx in wvRelease(ctx, as: (any Sampler).self) }, + threshold: { ctx, has_level, level, out_value, out_err in + wvInvoke(ctx, out_err, as: (any Sampler).self, fallback: false) { wvImpl in + wvReturnOptional(wvImpl.threshold(level: has_level ? level : nil), out_value) + } + }, + samples: { ctx, has_limit, limit, weights_ptr, weights_len, out_ptr, out_len, out_err in + wvInvoke(ctx, out_err, as: (any Sampler).self, throwing: PantryError.self, fallback: ()) { wvImpl in + try wvReturnSlice(wvImpl.samples(limit: has_limit ? limit : nil, weights: wvBorrowSlice(weights_ptr, weights_len)), out_ptr, out_len) + } + }, + describe: { ctx, out_ptr, out_len, out_err in + wvInvoke(ctx, out_err, as: (any Sampler).self, fallback: ()) { wvImpl in + try wvReturnString(wvImpl.describe(), out_ptr, out_len) } } )) } /// A pocket-sized interface exercising the object surface -public final class Gadget: @unchecked Sendable { +public final class Gadget: WvObject, Hashable, @unchecked Sendable { let ptr: OpaquePointer init(ptr: OpaquePointer) { @@ -925,12 +424,19 @@ public final class Gadget: @unchecked Sendable { kitchen_sink_kitchen_Gadget_destroy(ptr) } - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it. func clonePtr() -> OpaquePointer { wvNonNull(kitchen_sink_kitchen_Gadget_clone(ptr)) } + /// Whether two wrappers hold the same native object. + public static func == (lhs: Gadget, rhs: Gadget) -> Bool { + lhs.ptr == rhs.ptr + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ptr) + } + /// Create a gadget with the given id public init(id: Int64) { wvLoad() @@ -944,21 +450,33 @@ public final class Gadget: @unchecked Sendable { public func describe() -> String { var err = WvError() var outLen = 0 - let rv = kitchen_sink_kitchen_Gadget_describe(ptr, &outLen, &err) + let rv = kitchen_sink_kitchen_Gadget_describe(self.ptr, &outLen, &err) wvTrap(&err) return wvTakeString(rv, outLen) } /// Poke the gadget a number of times, failing on a negative count /// - /// - Throws: ``KitchenErrorsError`` for a declared failure, ``KitchenSinkRuntimeError`` otherwise. + /// - Throws: ``KitchenError`` for a declared failure, + /// ``KitchenSinkRuntimeError`` otherwise. public func poke(times: Int32) throws -> Int32 { var err = WvError() - let rv = kitchen_sink_kitchen_Gadget_poke(ptr, times, &err) - try wvCheckKitchen(&err) + let rv = kitchen_sink_kitchen_Gadget_poke(self.ptr, times, &err) + try wvCheck(&err, KitchenError.self) return rv } + /// The gadget's sensor readings, optionally limited + /// + /// - Throws: ``KitchenSinkRuntimeError`` with the library's message. + public func readings(limit: UInt32?) throws -> [Double] { + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_Gadget_readings(self.ptr, limit != nil, limit ?? 0, &outLen, &err) + try wvCheck(&err) + return wvTakeSlice(rv, outLen) + } + /// The gadget subsystem version string public static func version() -> String { wvLoad() @@ -970,86 +488,6 @@ public final class Gadget: @unchecked Sendable { } } -extension Gadget: WvCodable { - static func wvRead(_ r: inout WvReader) -> Gadget { Gadget(ptr: r.readObject()) } - func wvWrite(_ w: inout WvWriter) { w.writeObject(clonePtr()) } -} - -/// A lazy sequence over the `Gadget` elements streamed by `kitchen_sink_kitchen_stream_gadgets`. -/// -/// Each `next()` call pulls exactly one element from the library. The -/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` -/// when iteration is abandoned early. -public final class KitchenStreamGadgetsIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the library, or returns `nil` once the - /// stream is exhausted (destroying the underlying iterator). - public func next() -> Gadget? { - guard let handle = handle else { return nil } - var item: OpaquePointer? = nil - var err = WvError() - if kitchen_sink_kitchen_StreamGadgetsIterator_next(handle, &item, &err) == 0 { - wvTrap(&err) - destroyHandle() - return nil - } - return Gadget(ptr: wvNonNull(item)) - } -} - -/// A lazy sequence over the `Item` elements streamed by `kitchen_sink_kitchen_stream_items`. -/// -/// Each `next()` call pulls exactly one element from the library. The -/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` -/// when iteration is abandoned early. -public final class KitchenStreamItemsIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - kitchen_sink_kitchen_StreamItemsIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the library, or returns `nil` once the - /// stream is exhausted (destroying the underlying iterator). - public func next() -> Item? { - guard let handle = handle else { return nil } - var item: UnsafePointer? = nil - var itemLen = 0 - var err = WvError() - if kitchen_sink_kitchen_StreamItemsIterator_next(handle, &item, &itemLen, &err) == 0 { - wvTrap(&err) - destroyHandle() - return nil - } - return wvTakeBuffer(item, itemLen, as: Item.self) - } -} - public enum Shared { /// Trivial cross-module helper public static func ping() -> String { @@ -1167,24 +605,30 @@ public enum Kitchen { /// An iterator over objects /// - /// - Returns: A lazy sequence that pulls one element per step from the library. - public static func streamGadgets() -> KitchenStreamGadgetsIterator { + /// - Returns: A lazy sequence that pulls one element per step from the + /// library. + public static func streamGadgets() -> NativeSequence { wvLoad() var err = WvError() let rv = kitchen_sink_kitchen_stream_gadgets(&err) wvTrap(&err) - return KitchenStreamGadgetsIterator(handle: wvNonNull(rv)) + return NativeSequence(wvNonNull(rv), release: { kitchen_sink_kitchen_StreamGadgetsIterator_destroy($0) }, failure: { wvFatal($0) }) { handle, err in + var item: OpaquePointer? = nil + guard kitchen_sink_kitchen_StreamGadgetsIterator_next(handle, &item, err) != 0 else { return nil } + return Gadget(ptr: wvNonNull(item)) + } } /// Optional struct return /// - /// - Throws: ``KitchenErrorsError`` for a declared failure, ``KitchenSinkRuntimeError`` otherwise. + /// - Throws: ``KitchenError`` for a declared failure, + /// ``KitchenSinkRuntimeError`` otherwise. public static func maybeItem(id: Int64) throws -> Item? { wvLoad() var err = WvError() var outLen = 0 let rv = kitchen_sink_kitchen_maybe_item(id, &outLen, &err) - try wvCheckKitchen(&err) + try wvCheck(&err, KitchenError.self) return wvTakeBuffer(rv, outLen, as: Item?.self) } @@ -1210,13 +654,19 @@ public enum Kitchen { /// Iterator return type /// - /// - Returns: A lazy sequence that pulls one element per step from the library. - public static func streamItems() -> KitchenStreamItemsIterator { + /// - Returns: A lazy sequence that pulls one element per step from the + /// library. + public static func streamItems() -> NativeSequence { wvLoad() var err = WvError() let rv = kitchen_sink_kitchen_stream_items(&err) wvTrap(&err) - return KitchenStreamItemsIterator(handle: wvNonNull(rv)) + return NativeSequence(wvNonNull(rv), release: { kitchen_sink_kitchen_StreamItemsIterator_destroy($0) }, failure: { wvFatal($0) }) { handle, err in + var item: UnsafePointer? = nil + var itemLen = 0 + guard kitchen_sink_kitchen_StreamItemsIterator_next(handle, &item, &itemLen, err) != 0 else { return nil } + return wvTakeBuffer(item, itemLen, as: Item.self) + } } /// Returns the shared Token type from another module @@ -1235,12 +685,12 @@ public enum Kitchen { return await withCheckedContinuation { (continuation: CheckedContinuation) in let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() wvWithUTF8(input) { input_ptr, input_len in - kitchen_sink_kitchen_do_async(input_ptr, input_len, { context, err, resultPtr, resultLen in + kitchen_sink_kitchen_do_async(input_ptr, input_len, { context, err, result_ptr, result_len in let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value if let err = err { - wvTrapBoxed(err) + wvFatal(err.pointee) } - cont.resume(returning: wvTakeString(resultPtr, resultLen)) + cont.resume(returning: wvTakeString(result_ptr, result_len)) }, context) } } @@ -1248,7 +698,8 @@ public enum Kitchen { /// Cancellable async operation /// - /// - Throws: `CancellationError` when the calling task is cancelled, which cancels the native call. + /// - Throws: `CancellationError` when the calling task is cancelled, which + /// cancels the native call. public static func doCancellable(input: String) async throws -> String { wvLoad() let token = WvCancelToken() @@ -1256,13 +707,13 @@ public enum Kitchen { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() wvWithUTF8(input) { input_ptr, input_len in - kitchen_sink_kitchen_do_cancellable(input_ptr, input_len, token.raw, { context, err, resultPtr, resultLen in + kitchen_sink_kitchen_do_cancellable(input_ptr, input_len, token.raw, { context, err, result_ptr, result_len in let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value if let err = err { - cont.resume(throwing: wvTakeError(err, wvCancelledOrTrap)) + cont.resume(throwing: wvTakeCancellation(err)) return } - cont.resume(returning: wvTakeString(resultPtr, resultLen)) + cont.resume(returning: wvTakeString(result_ptr, result_len)) }, context) } } @@ -1272,7 +723,7 @@ public enum Kitchen { } /// Legacy operation kept for compatibility - @available(*, deprecated, message: "Use new_op instead") + @available(*, deprecated, message: "Use `newOp` instead") public static func legacyOp() -> Int32 { wvLoad() var err = WvError() @@ -1281,7 +732,7 @@ public enum Kitchen { return rv } - /// Replacement for legacy_op + /// Replacement for `legacyOp` public static func newOp() -> Int32 { wvLoad() var err = WvError() @@ -1290,6 +741,217 @@ public enum Kitchen { return rv } + /// Optional scalar in both directions + public static func optI32Id(x: Int32?) -> Int32? { + wvLoad() + var err = WvError() + var outValue: Int32 = 0 + let rv = kitchen_sink_kitchen_opt_i32_id(x != nil, x ?? 0, &outValue, &err) + wvTrap(&err) + return rv ? outValue : nil + } + + public static func optBoolId(x: Bool?) -> Bool? { + wvLoad() + var err = WvError() + var outValue: Bool = false + let rv = kitchen_sink_kitchen_opt_bool_id(x != nil, x ?? false, &outValue, &err) + wvTrap(&err) + return rv ? outValue : nil + } + + /// Optional C-style enum in both directions + public static func optPriority(p: Priority?) -> Priority? { + wvLoad() + var err = WvError() + var outValue: Int32 = 0 + let rv = kitchen_sink_kitchen_opt_priority(p != nil, p?.rawValue ?? 0, &outValue, &err) + wvTrap(&err) + return rv ? wvEnumCase(Priority.self, outValue) : nil + } + + /// Optional scalars next to ordinary parameters + public static func optMixed(base: Double, scale: Float?, offset: Int64?, label: String?) -> UInt16? { + wvLoad() + var err = WvError() + var outValue: UInt16 = 0 + let rv: Bool = wvWithEncoded(label) { label_ptr, label_len in + kitchen_sink_kitchen_opt_mixed(base, scale != nil, scale ?? 0, offset != nil, offset ?? 0, label_ptr, label_len, &outValue, &err) + } + wvTrap(&err) + return rv ? outValue : nil + } + + /// A typed-array parameter + public static func sumF64(xs: [Double]) -> Double { + wvLoad() + var err = WvError() + let rv: Double = wvWithSlice(xs) { xs_ptr, xs_len in + kitchen_sink_kitchen_sum_f64(xs_ptr, xs_len, &err) + } + wvTrap(&err) + return rv + } + + /// Typed arrays in both directions + public static func scaleI32(xs: [Int32], factor: Int32) -> [Int32] { + wvLoad() + var err = WvError() + var outLen = 0 + let rv: UnsafeMutablePointer? = wvWithSlice(xs) { xs_ptr, xs_len in + kitchen_sink_kitchen_scale_i32(xs_ptr, xs_len, factor, &outLen, &err) + } + wvTrap(&err) + return wvTakeSlice(rv, outLen) + } + + public static func idsU64(ids: [UInt64]) -> [UInt64] { + wvLoad() + var err = WvError() + var outLen = 0 + let rv: UnsafeMutablePointer? = wvWithSlice(ids) { ids_ptr, ids_len in + kitchen_sink_kitchen_ids_u64(ids_ptr, ids_len, &outLen, &err) + } + wvTrap(&err) + return wvTakeSlice(rv, outLen) + } + + /// Every slice element type + public static func mixedSlices(a: [Int8], b: [Int16], c: [UInt16], d: [UInt32], e: [Int64], f: [Float]) -> [UInt32] { + wvLoad() + var err = WvError() + var outLen = 0 + let rv: UnsafeMutablePointer? = wvWithSlice(a) { a_ptr, a_len in + wvWithSlice(b) { b_ptr, b_len in + wvWithSlice(c) { c_ptr, c_len in + wvWithSlice(d) { d_ptr, d_len in + wvWithSlice(e) { e_ptr, e_len in + wvWithSlice(f) { f_ptr, f_len in + kitchen_sink_kitchen_mixed_slices(a_ptr, a_len, b_ptr, b_len, c_ptr, c_len, d_ptr, d_len, e_ptr, e_len, f_ptr, f_len, &outLen, &err) + } + } + } + } + } + } + wvTrap(&err) + return wvTakeSlice(rv, outLen) + } + + /// Async returning an optional scalar + public static func asyncMaybeCount() async -> Int64? { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + kitchen_sink_kitchen_async_maybe_count({ context, err, has_result, result in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvFatal(err.pointee) + } + cont.resume(returning: has_result ? result : nil) + }, context) + } + } + + /// Async returning a typed array + /// + /// - Throws: `CancellationError` when the calling task is cancelled, which + /// cancels the native call. + public static func asyncSamples(n: UInt32) async throws -> [Float] { + wvLoad() + let token = WvCancelToken() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[Float], Error>) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + kitchen_sink_kitchen_async_samples(n, token.raw, { context, err, result_ptr, result_len in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeCancellation(err)) + return + } + cont.resume(returning: wvTakeSlice(result_ptr, result_len)) + }, context) + } + } onCancel: { + token.cancel() + } + } + + /// Iterator of optional scalars + /// + /// - Returns: A lazy sequence that pulls one element per step from the + /// library. + public static func streamMaybe() -> NativeSequence { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_stream_maybe(&err) + wvTrap(&err) + return NativeSequence(wvNonNull(rv), release: { kitchen_sink_kitchen_StreamMaybeIterator_destroy($0) }, failure: { wvFatal($0) }) { handle, err in + var hasItem = false + var item: Int32 = 0 + guard kitchen_sink_kitchen_StreamMaybeIterator_next(handle, &hasItem, &item, err) != 0 else { return nil } + return .some(hasItem ? item : nil) + } + } + + /// Iterator of typed arrays + /// + /// - Returns: A lazy sequence that pulls one element per step from the + /// library. + public static func streamChunks() -> NativeSequence<[Int32]> { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_stream_chunks(&err) + wvTrap(&err) + return NativeSequence(wvNonNull(rv), release: { kitchen_sink_kitchen_StreamChunksIterator_destroy($0) }, failure: { wvFatal($0) }) { handle, err in + var item: UnsafeMutablePointer? = nil + var itemLen = 0 + guard kitchen_sink_kitchen_StreamChunksIterator_next(handle, &item, &itemLen, err) != 0 else { return nil } + return wvTakeSlice(item, itemLen) + } + } + + /// Drive a consumer-implemented sampler + /// + /// - Throws: ``PantryError`` for a declared failure, + /// ``KitchenSinkRuntimeError`` otherwise. + public static func sampleWith(sampler: any Sampler, level: UInt32?) throws -> [Int16] { + wvLoad() + var err = WvError() + let sampler_ctx = wvRetain(sampler) + var outLen = 0 + let rv = kitchen_sink_kitchen_sample_with(sampler_ctx, WvSamplerVtable.shared.pointer, level != nil, level ?? 0, &outLen, &err) + try wvCheck(&err, PantryError.self) + return wvTakeSlice(rv, outLen) + } + + /// Fails with the second domain + /// + /// - Throws: ``PantryError`` for a declared failure, + /// ``KitchenSinkRuntimeError`` otherwise. + public static func takeFromPantry(item: String) throws -> Int32 { + wvLoad() + var err = WvError() + let rv: Int32 = wvWithUTF8(item) { item_ptr, item_len in + kitchen_sink_kitchen_take_from_pantry(item_ptr, item_len, &err) + } + try wvCheck(&err, PantryError.self) + return rv + } + + /// Fails with an untyped error + /// + /// - Throws: ``KitchenSinkRuntimeError`` with the library's message. + public static func parseAny(text: String) throws -> Int64 { + wvLoad() + var err = WvError() + let rv: Int64 = wvWithUTF8(text) { text_ptr, text_len in + kitchen_sink_kitchen_parse_any(text_ptr, text_len, &err) + } + try wvCheck(&err) + return rv + } + public enum Nested { /// Trivial nested-module function public static func hello() -> String { diff --git a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_NestedModules_NestedModules_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_NestedModules_NestedModules_swift.snap deleted file mode 100644 index b9935f75..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_NestedModules_NestedModules_swift.snap +++ /dev/null @@ -1,838 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import CNestedModules -import Foundation - -// MARK: - Runtime support - -/// A failure the native library reports outside any declared error domain: -/// an unknown error code or a runtime failure. Throwing wrappers raise it; -/// non-throwing wrappers stop the process with `fatalError` instead, since -/// Swift has no unchecked errors. -public struct NestedModulesRuntimeError: Error, LocalizedError, Hashable, Sendable { - /// The code of a generic runtime failure. - public static let genericCode: Int32 = -1 - /// The code of a panic caught at the library boundary. - public static let panicCode: Int32 = -2 - /// The code of an argument or result that failed to marshal. - public static let marshalCode: Int32 = -3 - /// The code of a callback implementation that threw. - public static let foreignCode: Int32 = -4 - - /// The numeric code the native library reported. - public let errorCode: Int32 - /// The message the native library reported. - public let message: String - - /// Creates an error carrying a native code and message. - public init(errorCode: Int32, message: String) { - self.errorCode = errorCode - self.message = message - } - - public var errorDescription: String? { message } -} - -/// The error slot every native call reports through. -typealias WvError = nested_modules_error - -/// The C ABI revision these bindings were generated against. -let wvAbiVersion: UInt32 = 4 - -/// Runs the load-time checks if they haven't run yet. -@inline(__always) -func wvLoad() { - _ = wvContract -} - -/// Stops the process unless the library implements the ABI revision these -/// bindings were generated against. -func wvCheckAbiVersion() { - let abi = nested_modules_abi_version() - guard abi == wvAbiVersion else { - fatalError("NestedModules: the native library 'nested_modules' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") - } -} - -/// Stops the process unless every declaration these bindings were generated -/// with is in a top-level module's contract table with an equal signature -/// hash. Declarations the library adds are fine. -func wvCheckContract( - _ table: (UnsafeMutablePointer?) -> UnsafePointer?, - _ expected: [(id: UInt64, hash: UInt64, path: StaticString)] -) { - var len = 0 - let entries = UnsafeBufferPointer(start: table(&len), count: len) - var hashes: [UInt64: UInt64] = [:] - for entry in entries { - hashes[entry.id] = entry.hash - } - for (id, hash, path) in expected { - guard let actual = hashes[id] else { - fatalError("NestedModules: \(path) is missing from the library 'nested_modules'; regenerate the bindings or rebuild the library") - } - guard actual == hash else { - fatalError("NestedModules: \(path) changed since these bindings were generated; regenerate the bindings or rebuild the library 'nested_modules'") - } - } -} - -/// The message carried by an error slot. -func wvErrorMessage(_ err: WvError) -> String { - err.message.map { String(cString: $0) } ?? "" -} - -/// The Swift error for a code outside any declared domain: cancellation -/// (`-5`) becomes `CancellationError`, anything else the runtime error. -func wvRuntimeError(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - return NestedModulesRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) -} - -/// Stops the process for a failed call that can't throw, naming the code and -/// message the library reported. -func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { - fatalError("NestedModules.\(function) failed with code \(code): \(message)") -} - -/// Throws the runtime error for a filled error slot, clearing it. -@inline(__always) -func wvCheck(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvRuntimeError(err) - nested_modules_error_clear(&err) - throw error -} - -/// Stops the process for a filled error slot of a call that can't throw. -@inline(__always) -func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { - guard err.code != 0 else { return } - let code = err.code - let message = wvErrorMessage(err) - nested_modules_error_clear(&err) - wvFatal(code, message, function) -} - -/// Maps and releases the heap-boxed error an async completion received. -func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { - let error = map(err.pointee) - nested_modules_error_free(err) - return error -} - -/// The error mapping of a cancellable call that declares no error domain: -/// cancellation throws `CancellationError`, anything else stops the process. -func wvCancelledOrTrap(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - wvFatal(err.code, wvErrorMessage(err), "async call") -} - -/// Stops the process for the boxed error of an async call that can't throw. -func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { - wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) -} - -/// Unwraps a pointer the library promised is non-null. -@inline(__always) -func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { - guard let ptr = ptr else { - wvFatal(NestedModulesRuntimeError.marshalCode, "the native library returned a null pointer", function) - } - return ptr -} - -/// The Swift enum case for a C-style enum discriminant. -func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { - guard let value = E(rawValue: raw) else { - wvDecodeFailure("unknown \(E.self) discriminant \(raw)") - } - return value -} - -// MARK: Strings, bytes, and buffers - -/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. -@inline(__always) -func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var s = s - return try s.withUTF8 { try body($0.baseAddress, $0.count) } -} - -/// Lends the bytes of `d` for one call. -@inline(__always) -func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } -} - -/// Encodes `value` as a value buffer and lends it for one call. -@inline(__always) -func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var w = WvWriter() - w.write(value) - return try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } -} - -/// Copies a returned string, then releases the library's allocation. -func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - defer { nested_modules_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies returned bytes, then releases the library's allocation. -func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - defer { nested_modules_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return Data(bytes: ptr, count: len) -} - -/// Decodes a returned value buffer in place, then releases it. -func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - defer { - if let ptr = ptr { nested_modules_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - } - return wvBorrowBuffer(ptr, len, as: type) -} - -/// Copies a borrowed string. -func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies borrowed bytes. -func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - return Data(bytes: ptr, count: len) -} - -/// Decodes a borrowed value buffer in place. -func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - var r = WvReader(UnsafeRawPointer(ptr), len) - let value: T = r.read() - r.finish() - return value -} - -/// Stops the process on a malformed value buffer, which the wire format -/// treats like a marshalling failure. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("NestedModules: malformed value buffer: \(context)") -} - -/// Serializes values in the value-buffer wire format: little-endian, packed, -/// no alignment. -struct WvWriter { - var bytes: [UInt8] = [] - - init() { - bytes.reserveCapacity(64) - } - - @inline(__always) - private mutating func append(_ v: T) { - withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } - } - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeI8(_ v: Int8) { append(v) } - mutating func writeU8(_ v: UInt8) { append(v) } - mutating func writeI16(_ v: Int16) { append(v) } - mutating func writeU16(_ v: UInt16) { append(v) } - mutating func writeI32(_ v: Int32) { append(v) } - mutating func writeU32(_ v: UInt32) { append(v) } - mutating func writeI64(_ v: Int64) { append(v) } - mutating func writeU64(_ v: UInt64) { append(v) } - mutating func writeF32(_ v: Float) { append(v.bitPattern) } - mutating func writeF64(_ v: Double) { append(v.bitPattern) } - - /// Writes a `u32` length or element count. - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") - append(UInt32(n)) - } - - mutating func writeString(_ v: String) { - let utf8 = v.utf8 - writeLen(utf8.count) - bytes.append(contentsOf: utf8) - } - - mutating func writeBytes(_ v: Data) { - writeLen(v.count) - bytes.append(contentsOf: v) - } - - mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } - - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } - - /// Writes any value that crosses inside a value buffer. - @inline(__always) - mutating func write(_ value: T) { - value.wvWrite(&self) - } -} - -/// Deserializes values from the value-buffer wire format directly from the -/// library's memory, rejecting truncated buffers, invalid flag bytes, -/// oversized length prefixes, and trailing bytes. -struct WvReader { - private let base: UnsafeRawPointer? - private let count: Int - private var pos = 0 - - init(_ base: UnsafeRawPointer?, _ count: Int) { - self.base = base - self.count = base == nil ? 0 : count - } - - /// The number of unread bytes. - var remaining: Int { count - pos } - - private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { - guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } - defer { pos += n } - return base + pos - } - - @inline(__always) - private mutating func load(_ type: T.Type, _ what: StaticString) -> T { - T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) - } - - mutating func readBool() -> Bool { - switch load(UInt8.self, "bool") { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readI8() -> Int8 { load(Int8.self, "i8") } - mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } - mutating func readI16() -> Int16 { load(Int16.self, "i16") } - mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } - mutating func readI32() -> Int32 { load(Int32.self, "i32") } - mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } - mutating func readI64() -> Int64 { load(Int64.self, "i64") } - mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } - mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } - mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } - - /// Reads a byte length, which can't exceed the unread bytes. - mutating func readLen() -> Int { - let n = Int(load(UInt32.self, "length")) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - - /// Reads an element count. Elements may encode to zero bytes, so the - /// count isn't bounded by the unread bytes; callers cap preallocation. - mutating func readCount() -> Int { - Int(load(UInt32.self, "count")) - } - - mutating func readString() -> String { - let n = readLen() - if n == 0 { return "" } - let p = take(n, "string") - guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - - mutating func readBytes() -> Data { - let n = readLen() - if n == 0 { return Data() } - return Data(bytes: take(n, "bytes"), count: n) - } - - mutating func readOptionFlag() -> Bool { - switch load(UInt8.self, "option flag") { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - - /// Reads an object token: one strong reference the caller adopts into a - /// wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { - wvDecodeFailure("null object token") - } - return p - } - - /// Reads any value that crosses inside a value buffer, typed by context. - @inline(__always) - mutating func read() -> T { - T.wvRead(&self) - } - - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -// MARK: Value-buffer codecs - -/// A type that crosses the C ABI inside a value buffer. Every primitive, -/// optional, list, and map conforms here; each record, enum, and interface -/// conforms where it's declared. -protocol WvCodable { - /// Reads one value, adopting the reference of any object token. - static func wvRead(_ r: inout WvReader) -> Self - /// Writes this value, writing a fresh reference for any object. - func wvWrite(_ w: inout WvWriter) -} - -/// A C-style enum, which crosses as its `i32` discriminant. -protocol WvCEnum: WvCodable, RawRepresentable where RawValue == Int32 {} - -extension WvCEnum { - static func wvRead(_ r: inout WvReader) -> Self { wvEnumCase(Self.self, r.readI32()) } - func wvWrite(_ w: inout WvWriter) { w.writeI32(rawValue) } -} - -extension Bool: WvCodable { - static func wvRead(_ r: inout WvReader) -> Bool { r.readBool() } - func wvWrite(_ w: inout WvWriter) { w.writeBool(self) } -} - -extension Int8: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int8 { r.readI8() } - func wvWrite(_ w: inout WvWriter) { w.writeI8(self) } -} - -extension UInt8: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt8 { r.readU8() } - func wvWrite(_ w: inout WvWriter) { w.writeU8(self) } -} - -extension Int16: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int16 { r.readI16() } - func wvWrite(_ w: inout WvWriter) { w.writeI16(self) } -} - -extension UInt16: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt16 { r.readU16() } - func wvWrite(_ w: inout WvWriter) { w.writeU16(self) } -} - -extension Int32: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int32 { r.readI32() } - func wvWrite(_ w: inout WvWriter) { w.writeI32(self) } -} - -extension UInt32: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt32 { r.readU32() } - func wvWrite(_ w: inout WvWriter) { w.writeU32(self) } -} - -extension Int64: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int64 { r.readI64() } - func wvWrite(_ w: inout WvWriter) { w.writeI64(self) } -} - -extension UInt64: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt64 { r.readU64() } - func wvWrite(_ w: inout WvWriter) { w.writeU64(self) } -} - -extension Float: WvCodable { - static func wvRead(_ r: inout WvReader) -> Float { r.readF32() } - func wvWrite(_ w: inout WvWriter) { w.writeF32(self) } -} - -extension Double: WvCodable { - static func wvRead(_ r: inout WvReader) -> Double { r.readF64() } - func wvWrite(_ w: inout WvWriter) { w.writeF64(self) } -} - -extension String: WvCodable { - static func wvRead(_ r: inout WvReader) -> String { r.readString() } - func wvWrite(_ w: inout WvWriter) { w.writeString(self) } -} - -extension Data: WvCodable { - static func wvRead(_ r: inout WvReader) -> Data { r.readBytes() } - func wvWrite(_ w: inout WvWriter) { w.writeBytes(self) } -} - -extension Optional: WvCodable where Wrapped: WvCodable { - static func wvRead(_ r: inout WvReader) -> Wrapped? { - r.readOptionFlag() ? .some(r.read()) : nil - } - - func wvWrite(_ w: inout WvWriter) { - w.writeOptionFlag(self != nil) - if let value = self { w.write(value) } - } -} - -extension Array: WvCodable where Element: WvCodable { - static func wvRead(_ r: inout WvReader) -> [Element] { - let n = r.readCount() - var items: [Element] = [] - items.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. [Key: Value] { - let n = r.readCount() - var map: [Key: Value] = [:] - map.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. { - let value: CheckedContinuation - - init(_ value: CheckedContinuation) { - self.value = value - } -} - -/// One process-wide callback vtable at a stable address the library may hold -/// for the life of the process. -final class WvVtable: @unchecked Sendable { - let pointer: UnsafePointer - - init(_ value: T) { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - pointer = UnsafePointer(cell) - } -} - -/// Holds one callback implementation behind the `ctx` the library keeps. -final class WvBox: @unchecked Sendable { - let value: T - - init(_ value: T) { - self.value = value - } -} - -/// Retains `impl` for the library: the `ctx` of a callback parameter, which -/// the vtable's `free` entry releases with ``wvRelease(_:as:)``. -func wvRetain(_ impl: T) -> UnsafeMutableRawPointer { - Unmanaged.passRetained(WvBox(impl)).toOpaque() -} - -/// Releases the implementation behind `ctx`, from the vtable's `free` entry. -func wvRelease(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) { - Unmanaged>.fromOpaque(ctx!).release() -} - -/// Runs one callback method on the implementation behind `ctx`. A thrown -/// error never unwinds through the library: `domain` reports it when it's -/// the method's declared error, anything else is reported as a callback -/// failure (`-4`), and the method returns `fallback`. -func wvInvoke( - _ ctx: UnsafeMutableRawPointer?, - _ outErr: UnsafeMutablePointer?, - as type: T.Type, - fallback: R, - domain: (Error, UnsafeMutablePointer?) -> Bool = { _, _ in false }, - _ body: (T) throws -> R -) -> R { - let impl = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value - do { - return try body(impl) - } catch { - if !domain(error, outErr) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { nested_modules_error_set(outErr, NestedModulesRuntimeError.foreignCode, $0) } - } - return fallback - } -} - -/// Reports a declared domain error from a callback method: its code, its -/// message, and its fields as the payload. -func wvSetError(_ outErr: UnsafeMutablePointer?, _ code: Int32, _ message: String, _ payload: WvWriter? = nil) { - message.withCString { nested_modules_error_set(outErr, code, $0) } - if let payload = payload { - payload.bytes.withUnsafeBufferPointer { nested_modules_error_set_payload(outErr, $0.baseAddress, $0.count) } - } -} - -/// Hands `bytes` to the library as a callback method's return: a run -/// allocated with `nested_modules_alloc` that the library adopts. -func wvHandOver(_ bytes: UnsafeRawBufferPointer, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - let run = nested_modules_alloc(bytes.count) - if let run = run, let base = bytes.baseAddress { - run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - outPtr?.pointee = run - outLen?.pointee = run == nil ? 0 : bytes.count -} - -/// Returns a string from a callback method through its out slots. -func wvReturnString(_ value: String, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var value = value - value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0), outPtr, outLen) } -} - -/// Returns bytes from a callback method through its out slots. -func wvReturnBytes(_ value: Data, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - value.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -/// Returns a value buffer from a callback method through its out slots. -func wvReturnBuffer(_ value: T, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var w = WvWriter() - w.write(value) - w.bytes.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -// MARK: - Load-time checks - -/// The load-time checks, run once before the first native call: the library -/// must implement C ABI revision 4 and carry every declaration these -/// bindings were generated with, unchanged. -let wvContract: Void = { - wvCheckAbiVersion() - wvCheckContract(nested_modules_graphics_contract, [ - (0x2c29eef9327a6bd4, 0x12983d5271651389, "graphics.Unit"), - (0x304088bbc28f0015, 0x97e6e809913be6b6, "graphics.new_canvas"), - (0x49f8837532df9ac6, 0x32e5b27e81ee0957, "graphics.Canvas"), - (0x55717fa027336b47, 0xb2bd3a6bdcce48c8, "graphics.shapes.describe"), - (0x7626b91b8f1f53cd, 0x867bbf7cf0f41c74, "graphics.shapes.geometry.compute"), - (0xb3efcb50d9a61ab7, 0x3cb0207dfad2a149, "graphics.GraphicsError"), - (0xc62b16a9591b744b, 0x7895610c8487eb1b, "graphics.shapes.Shape"), - (0xe13001f4983ccd35, 0x16e7de070b4a4cfc, "graphics.shapes.geometry.Detail"), - (0xf3f658bcfc5dba62, 0x2665ccb47356e746, "graphics.shapes.make_shape"), - ]) -}() - -// MARK: - API - -/// Typed errors reported by the `graphics` module. -public enum GraphicsError: Error, LocalizedError, Sendable { - case outOfBounds(message: String) - - /// The message the library reported, or the code's documented default. - public var errorDescription: String? { - switch self { - case let .outOfBounds(message): return message - } - } - - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .outOfBounds: return 1 - } - } -} - -func wvMapGraphics(_ err: WvError) -> Error { - let message = wvErrorMessage(err) - switch err.code { - case 1: return GraphicsError.outOfBounds(message: message.isEmpty ? "coordinate out of bounds" : message) - default: return wvRuntimeError(err) - } -} - -func wvCheckGraphics(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvMapGraphics(err) - nested_modules_error_clear(&err) - throw error -} - -/// A measurement unit shared with nested submodules. -public enum Unit: Int32, Sendable { - case pixels = 0 - case points = 1 -} - -extension Unit: WvCEnum {} - -/// Top-level struct that references a struct defined one level deeper. -public struct Canvas: Hashable, Sendable { - public var width: Int32 - /// Reference down into a nested module - public var root: Shape - - /// Creates a value from its fields. - public init(width: Int32, root: Shape) { - self.width = width - self.root = root - } -} - -extension Canvas: WvCodable { - static func wvRead(_ r: inout WvReader) -> Canvas { - Canvas(width: r.read(), root: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.width) - w.write(self.root) - } -} - -/// A struct one level deep that references both an ancestor enum and a deeper struct. -public struct Shape: Hashable, Sendable { - public var sides: Int32 - /// Reference up to the parent module's enum - public var unit: Unit - /// Reference down into a deeper module - public var detail: Detail - - /// Creates a value from its fields. - public init(sides: Int32, unit: Unit, detail: Detail) { - self.sides = sides - self.unit = unit - self.detail = detail - } -} - -extension Shape: WvCodable { - static func wvRead(_ r: inout WvReader) -> Shape { - Shape(sides: r.read(), unit: r.read(), detail: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.sides) - w.write(self.unit) - w.write(self.detail) - } -} - -/// A struct two levels deep. -public struct Detail: Hashable, Sendable { - public var area: Double - - /// Creates a value from its fields. - public init(area: Double) { - self.area = area - } -} - -extension Detail: WvCodable { - static func wvRead(_ r: inout WvReader) -> Detail { - Detail(area: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.area) - } -} - -public enum Graphics { - /// Construct a Canvas. - public static func newCanvas(width: Int32) -> Canvas { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = nested_modules_graphics_new_canvas(width, &outLen, &err) - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Canvas.self) - } - - public enum Shapes { - /// Construct a Shape. - public static func makeShape(sides: Int32) -> Shape { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = nested_modules_graphics_shapes_make_shape(sides, &outLen, &err) - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Shape.self) - } - - /// Nested function returning a struct owned by an ancestor module. - public static func describe() -> Canvas { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = nested_modules_graphics_shapes_describe(&outLen, &err) - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Canvas.self) - } - - public enum Geometry { - /// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - /// - /// - Throws: ``GraphicsError`` for a declared failure, ``NestedModulesRuntimeError`` otherwise. - public static func compute(shape: Shape) throws -> Detail { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(shape) { shape_ptr, shape_len in - nested_modules_graphics_shapes_geometry_compute(shape_ptr, shape_len, &outLen, &err) - } - try wvCheckGraphics(&err) - return wvTakeBuffer(rv, outLen, as: Detail.self) - } - } - } -} - -// END NestedModules.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_Shapes_Shapes_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_Shapes_Shapes_swift.snap deleted file mode 100644 index 95182c14..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_Shapes_Shapes_swift.snap +++ /dev/null @@ -1,827 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import CShapes -import Foundation - -// MARK: - Runtime support - -/// A failure the native library reports outside any declared error domain: -/// an unknown error code or a runtime failure. Throwing wrappers raise it; -/// non-throwing wrappers stop the process with `fatalError` instead, since -/// Swift has no unchecked errors. -public struct ShapesRuntimeError: Error, LocalizedError, Hashable, Sendable { - /// The code of a generic runtime failure. - public static let genericCode: Int32 = -1 - /// The code of a panic caught at the library boundary. - public static let panicCode: Int32 = -2 - /// The code of an argument or result that failed to marshal. - public static let marshalCode: Int32 = -3 - /// The code of a callback implementation that threw. - public static let foreignCode: Int32 = -4 - - /// The numeric code the native library reported. - public let errorCode: Int32 - /// The message the native library reported. - public let message: String - - /// Creates an error carrying a native code and message. - public init(errorCode: Int32, message: String) { - self.errorCode = errorCode - self.message = message - } - - public var errorDescription: String? { message } -} - -/// The error slot every native call reports through. -typealias WvError = shapes_error - -/// The C ABI revision these bindings were generated against. -let wvAbiVersion: UInt32 = 4 - -/// Runs the load-time checks if they haven't run yet. -@inline(__always) -func wvLoad() { - _ = wvContract -} - -/// Stops the process unless the library implements the ABI revision these -/// bindings were generated against. -func wvCheckAbiVersion() { - let abi = shapes_abi_version() - guard abi == wvAbiVersion else { - fatalError("Shapes: the native library 'shapes' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") - } -} - -/// Stops the process unless every declaration these bindings were generated -/// with is in a top-level module's contract table with an equal signature -/// hash. Declarations the library adds are fine. -func wvCheckContract( - _ table: (UnsafeMutablePointer?) -> UnsafePointer?, - _ expected: [(id: UInt64, hash: UInt64, path: StaticString)] -) { - var len = 0 - let entries = UnsafeBufferPointer(start: table(&len), count: len) - var hashes: [UInt64: UInt64] = [:] - for entry in entries { - hashes[entry.id] = entry.hash - } - for (id, hash, path) in expected { - guard let actual = hashes[id] else { - fatalError("Shapes: \(path) is missing from the library 'shapes'; regenerate the bindings or rebuild the library") - } - guard actual == hash else { - fatalError("Shapes: \(path) changed since these bindings were generated; regenerate the bindings or rebuild the library 'shapes'") - } - } -} - -/// The message carried by an error slot. -func wvErrorMessage(_ err: WvError) -> String { - err.message.map { String(cString: $0) } ?? "" -} - -/// The Swift error for a code outside any declared domain: cancellation -/// (`-5`) becomes `CancellationError`, anything else the runtime error. -func wvRuntimeError(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - return ShapesRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) -} - -/// Stops the process for a failed call that can't throw, naming the code and -/// message the library reported. -func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { - fatalError("Shapes.\(function) failed with code \(code): \(message)") -} - -/// Throws the runtime error for a filled error slot, clearing it. -@inline(__always) -func wvCheck(_ err: inout WvError) throws { - guard err.code != 0 else { return } - let error = wvRuntimeError(err) - shapes_error_clear(&err) - throw error -} - -/// Stops the process for a filled error slot of a call that can't throw. -@inline(__always) -func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { - guard err.code != 0 else { return } - let code = err.code - let message = wvErrorMessage(err) - shapes_error_clear(&err) - wvFatal(code, message, function) -} - -/// Maps and releases the heap-boxed error an async completion received. -func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { - let error = map(err.pointee) - shapes_error_free(err) - return error -} - -/// The error mapping of a cancellable call that declares no error domain: -/// cancellation throws `CancellationError`, anything else stops the process. -func wvCancelledOrTrap(_ err: WvError) -> Error { - if err.code == -5 { - return CancellationError() - } - wvFatal(err.code, wvErrorMessage(err), "async call") -} - -/// Stops the process for the boxed error of an async call that can't throw. -func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { - wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) -} - -/// Unwraps a pointer the library promised is non-null. -@inline(__always) -func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { - guard let ptr = ptr else { - wvFatal(ShapesRuntimeError.marshalCode, "the native library returned a null pointer", function) - } - return ptr -} - -/// The Swift enum case for a C-style enum discriminant. -func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { - guard let value = E(rawValue: raw) else { - wvDecodeFailure("unknown \(E.self) discriminant \(raw)") - } - return value -} - -// MARK: Strings, bytes, and buffers - -/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. -@inline(__always) -func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var s = s - return try s.withUTF8 { try body($0.baseAddress, $0.count) } -} - -/// Lends the bytes of `d` for one call. -@inline(__always) -func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } -} - -/// Encodes `value` as a value buffer and lends it for one call. -@inline(__always) -func wvWithEncoded(_ value: T, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { - var w = WvWriter() - w.write(value) - return try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } -} - -/// Copies a returned string, then releases the library's allocation. -func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - defer { shapes_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies returned bytes, then releases the library's allocation. -func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - defer { shapes_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - return Data(bytes: ptr, count: len) -} - -/// Decodes a returned value buffer in place, then releases it. -func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - defer { - if let ptr = ptr { shapes_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } - } - return wvBorrowBuffer(ptr, len, as: type) -} - -/// Copies a borrowed string. -func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { - guard let ptr = ptr else { return "" } - return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) -} - -/// Copies borrowed bytes. -func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { - guard let ptr = ptr else { return Data() } - return Data(bytes: ptr, count: len) -} - -/// Decodes a borrowed value buffer in place. -func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, as type: T.Type) -> T { - var r = WvReader(UnsafeRawPointer(ptr), len) - let value: T = r.read() - r.finish() - return value -} - -/// Stops the process on a malformed value buffer, which the wire format -/// treats like a marshalling failure. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("Shapes: malformed value buffer: \(context)") -} - -/// Serializes values in the value-buffer wire format: little-endian, packed, -/// no alignment. -struct WvWriter { - var bytes: [UInt8] = [] - - init() { - bytes.reserveCapacity(64) - } - - @inline(__always) - private mutating func append(_ v: T) { - withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } - } - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeI8(_ v: Int8) { append(v) } - mutating func writeU8(_ v: UInt8) { append(v) } - mutating func writeI16(_ v: Int16) { append(v) } - mutating func writeU16(_ v: UInt16) { append(v) } - mutating func writeI32(_ v: Int32) { append(v) } - mutating func writeU32(_ v: UInt32) { append(v) } - mutating func writeI64(_ v: Int64) { append(v) } - mutating func writeU64(_ v: UInt64) { append(v) } - mutating func writeF32(_ v: Float) { append(v.bitPattern) } - mutating func writeF64(_ v: Double) { append(v.bitPattern) } - - /// Writes a `u32` length or element count. - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") - append(UInt32(n)) - } - - mutating func writeString(_ v: String) { - let utf8 = v.utf8 - writeLen(utf8.count) - bytes.append(contentsOf: utf8) - } - - mutating func writeBytes(_ v: Data) { - writeLen(v.count) - bytes.append(contentsOf: v) - } - - mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } - - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } - - /// Writes any value that crosses inside a value buffer. - @inline(__always) - mutating func write(_ value: T) { - value.wvWrite(&self) - } -} - -/// Deserializes values from the value-buffer wire format directly from the -/// library's memory, rejecting truncated buffers, invalid flag bytes, -/// oversized length prefixes, and trailing bytes. -struct WvReader { - private let base: UnsafeRawPointer? - private let count: Int - private var pos = 0 - - init(_ base: UnsafeRawPointer?, _ count: Int) { - self.base = base - self.count = base == nil ? 0 : count - } - - /// The number of unread bytes. - var remaining: Int { count - pos } - - private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { - guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } - defer { pos += n } - return base + pos - } - - @inline(__always) - private mutating func load(_ type: T.Type, _ what: StaticString) -> T { - T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) - } - - mutating func readBool() -> Bool { - switch load(UInt8.self, "bool") { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readI8() -> Int8 { load(Int8.self, "i8") } - mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } - mutating func readI16() -> Int16 { load(Int16.self, "i16") } - mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } - mutating func readI32() -> Int32 { load(Int32.self, "i32") } - mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } - mutating func readI64() -> Int64 { load(Int64.self, "i64") } - mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } - mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } - mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } - - /// Reads a byte length, which can't exceed the unread bytes. - mutating func readLen() -> Int { - let n = Int(load(UInt32.self, "length")) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - - /// Reads an element count. Elements may encode to zero bytes, so the - /// count isn't bounded by the unread bytes; callers cap preallocation. - mutating func readCount() -> Int { - Int(load(UInt32.self, "count")) - } - - mutating func readString() -> String { - let n = readLen() - if n == 0 { return "" } - let p = take(n, "string") - guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - - mutating func readBytes() -> Data { - let n = readLen() - if n == 0 { return Data() } - return Data(bytes: take(n, "bytes"), count: n) - } - - mutating func readOptionFlag() -> Bool { - switch load(UInt8.self, "option flag") { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - - /// Reads an object token: one strong reference the caller adopts into a - /// wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { - wvDecodeFailure("null object token") - } - return p - } - - /// Reads any value that crosses inside a value buffer, typed by context. - @inline(__always) - mutating func read() -> T { - T.wvRead(&self) - } - - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -// MARK: Value-buffer codecs - -/// A type that crosses the C ABI inside a value buffer. Every primitive, -/// optional, list, and map conforms here; each record, enum, and interface -/// conforms where it's declared. -protocol WvCodable { - /// Reads one value, adopting the reference of any object token. - static func wvRead(_ r: inout WvReader) -> Self - /// Writes this value, writing a fresh reference for any object. - func wvWrite(_ w: inout WvWriter) -} - -/// A C-style enum, which crosses as its `i32` discriminant. -protocol WvCEnum: WvCodable, RawRepresentable where RawValue == Int32 {} - -extension WvCEnum { - static func wvRead(_ r: inout WvReader) -> Self { wvEnumCase(Self.self, r.readI32()) } - func wvWrite(_ w: inout WvWriter) { w.writeI32(rawValue) } -} - -extension Bool: WvCodable { - static func wvRead(_ r: inout WvReader) -> Bool { r.readBool() } - func wvWrite(_ w: inout WvWriter) { w.writeBool(self) } -} - -extension Int8: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int8 { r.readI8() } - func wvWrite(_ w: inout WvWriter) { w.writeI8(self) } -} - -extension UInt8: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt8 { r.readU8() } - func wvWrite(_ w: inout WvWriter) { w.writeU8(self) } -} - -extension Int16: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int16 { r.readI16() } - func wvWrite(_ w: inout WvWriter) { w.writeI16(self) } -} - -extension UInt16: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt16 { r.readU16() } - func wvWrite(_ w: inout WvWriter) { w.writeU16(self) } -} - -extension Int32: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int32 { r.readI32() } - func wvWrite(_ w: inout WvWriter) { w.writeI32(self) } -} - -extension UInt32: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt32 { r.readU32() } - func wvWrite(_ w: inout WvWriter) { w.writeU32(self) } -} - -extension Int64: WvCodable { - static func wvRead(_ r: inout WvReader) -> Int64 { r.readI64() } - func wvWrite(_ w: inout WvWriter) { w.writeI64(self) } -} - -extension UInt64: WvCodable { - static func wvRead(_ r: inout WvReader) -> UInt64 { r.readU64() } - func wvWrite(_ w: inout WvWriter) { w.writeU64(self) } -} - -extension Float: WvCodable { - static func wvRead(_ r: inout WvReader) -> Float { r.readF32() } - func wvWrite(_ w: inout WvWriter) { w.writeF32(self) } -} - -extension Double: WvCodable { - static func wvRead(_ r: inout WvReader) -> Double { r.readF64() } - func wvWrite(_ w: inout WvWriter) { w.writeF64(self) } -} - -extension String: WvCodable { - static func wvRead(_ r: inout WvReader) -> String { r.readString() } - func wvWrite(_ w: inout WvWriter) { w.writeString(self) } -} - -extension Data: WvCodable { - static func wvRead(_ r: inout WvReader) -> Data { r.readBytes() } - func wvWrite(_ w: inout WvWriter) { w.writeBytes(self) } -} - -extension Optional: WvCodable where Wrapped: WvCodable { - static func wvRead(_ r: inout WvReader) -> Wrapped? { - r.readOptionFlag() ? .some(r.read()) : nil - } - - func wvWrite(_ w: inout WvWriter) { - w.writeOptionFlag(self != nil) - if let value = self { w.write(value) } - } -} - -extension Array: WvCodable where Element: WvCodable { - static func wvRead(_ r: inout WvReader) -> [Element] { - let n = r.readCount() - var items: [Element] = [] - items.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. [Key: Value] { - let n = r.readCount() - var map: [Key: Value] = [:] - map.reserveCapacity(Swift.min(n, r.remaining)) - for _ in 0.. { - let value: CheckedContinuation - - init(_ value: CheckedContinuation) { - self.value = value - } -} - -/// One process-wide callback vtable at a stable address the library may hold -/// for the life of the process. -final class WvVtable: @unchecked Sendable { - let pointer: UnsafePointer - - init(_ value: T) { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - pointer = UnsafePointer(cell) - } -} - -/// Holds one callback implementation behind the `ctx` the library keeps. -final class WvBox: @unchecked Sendable { - let value: T - - init(_ value: T) { - self.value = value - } -} - -/// Retains `impl` for the library: the `ctx` of a callback parameter, which -/// the vtable's `free` entry releases with ``wvRelease(_:as:)``. -func wvRetain(_ impl: T) -> UnsafeMutableRawPointer { - Unmanaged.passRetained(WvBox(impl)).toOpaque() -} - -/// Releases the implementation behind `ctx`, from the vtable's `free` entry. -func wvRelease(_ ctx: UnsafeMutableRawPointer?, as type: T.Type) { - Unmanaged>.fromOpaque(ctx!).release() -} - -/// Runs one callback method on the implementation behind `ctx`. A thrown -/// error never unwinds through the library: `domain` reports it when it's -/// the method's declared error, anything else is reported as a callback -/// failure (`-4`), and the method returns `fallback`. -func wvInvoke( - _ ctx: UnsafeMutableRawPointer?, - _ outErr: UnsafeMutablePointer?, - as type: T.Type, - fallback: R, - domain: (Error, UnsafeMutablePointer?) -> Bool = { _, _ in false }, - _ body: (T) throws -> R -) -> R { - let impl = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue().value - do { - return try body(impl) - } catch { - if !domain(error, outErr) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { shapes_error_set(outErr, ShapesRuntimeError.foreignCode, $0) } - } - return fallback - } -} - -/// Reports a declared domain error from a callback method: its code, its -/// message, and its fields as the payload. -func wvSetError(_ outErr: UnsafeMutablePointer?, _ code: Int32, _ message: String, _ payload: WvWriter? = nil) { - message.withCString { shapes_error_set(outErr, code, $0) } - if let payload = payload { - payload.bytes.withUnsafeBufferPointer { shapes_error_set_payload(outErr, $0.baseAddress, $0.count) } - } -} - -/// Hands `bytes` to the library as a callback method's return: a run -/// allocated with `shapes_alloc` that the library adopts. -func wvHandOver(_ bytes: UnsafeRawBufferPointer, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - let run = shapes_alloc(bytes.count) - if let run = run, let base = bytes.baseAddress { - run.initialize(from: base.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - outPtr?.pointee = run - outLen?.pointee = run == nil ? 0 : bytes.count -} - -/// Returns a string from a callback method through its out slots. -func wvReturnString(_ value: String, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var value = value - value.withUTF8 { wvHandOver(UnsafeRawBufferPointer($0), outPtr, outLen) } -} - -/// Returns bytes from a callback method through its out slots. -func wvReturnBytes(_ value: Data, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - value.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -/// Returns a value buffer from a callback method through its out slots. -func wvReturnBuffer(_ value: T, _ outPtr: UnsafeMutablePointer?>?, _ outLen: UnsafeMutablePointer?) { - var w = WvWriter() - w.write(value) - w.bytes.withUnsafeBytes { wvHandOver($0, outPtr, outLen) } -} - -// MARK: - Load-time checks - -/// The load-time checks, run once before the first native call: the library -/// must implement C ABI revision 4 and carry every declaration these -/// bindings were generated with, unchanged. -let wvContract: Void = { - wvCheckAbiVersion() - wvCheckContract(shapes_shapes_contract, [ - (0x0d7b3eec49243e7a, 0x5f232311808e1b1f, "shapes.Channel"), - (0x183fa8198100860e, 0xe40c6993cb31d359, "shapes.describe"), - (0x2d1395eb837b848e, 0x42202a0c12f3a16e, "shapes.sum_bytes"), - (0x7f9a5b87ba09b86b, 0xc5b32fb0efbbaa17, "shapes.scale"), - (0xc8edd779240fda17, 0x68e1be78fddb20e4, "shapes.Numerics"), - (0xd3d3881b2fd18a62, 0xf24d0eb76a99d918, "shapes.make_numerics"), - (0xf010844afff5ac3c, 0x50de28ae9b41ff15, "shapes.Shape"), - ]) -}() - -// MARK: - API - -/// An algebraic shape (sum type with associated data) -public enum Shape: Hashable, Sendable { - /// The empty shape (a unit variant) - case empty - /// A circle with a radius - case circle(radius: Double) - /// An axis-aligned rectangle - case rectangle(width: Float, height: Float) - /// An integer-bounded shape (exercises narrow + wide ints) - case bounded(min: Int16, max: Int16, weight: UInt64) - /// A labeled shape with a small count - case labeled(label: String, count: UInt8) -} - -extension Shape: WvCodable { - static func wvRead(_ r: inout WvReader) -> Shape { - let tag = r.readI32() - switch tag { - case 0: return .empty - case 1: return .circle(radius: r.read()) - case 2: return .rectangle(width: r.read(), height: r.read()) - case 3: return .bounded(min: r.read(), max: r.read(), weight: r.read()) - case 4: return .labeled(label: r.read(), count: r.read()) - default: wvDecodeFailure("unknown Shape tag \(tag)") - } - } - - func wvWrite(_ w: inout WvWriter) { - switch self { - case .empty: - w.writeI32(0) - case let .circle(v0): - w.writeI32(1) - w.write(v0) - case let .rectangle(v0, v1): - w.writeI32(2) - w.write(v0) - w.write(v1) - case let .bounded(v0, v1, v2): - w.writeI32(3) - w.write(v0) - w.write(v1) - w.write(v2) - case let .labeled(v0, v1): - w.writeI32(4) - w.write(v0) - w.write(v1) - } - } -} - -/// A plain C-style enum (no payloads) -public enum Channel: Int32, Sendable { - case red = 0 - case green = 1 - case blue = 2 -} - -extension Channel: WvCEnum {} - -/// One field per supported numeric primitive -public struct Numerics: Hashable, Sendable { - public var a: Int8 - public var b: Int16 - public var c: Int32 - public var d: Int64 - public var e: UInt8 - public var f: UInt16 - public var g: UInt32 - public var h: UInt64 - public var i: Float - public var j: Double - - /// Creates a value from its fields. - public init(a: Int8, b: Int16, c: Int32, d: Int64, e: UInt8, f: UInt16, g: UInt32, h: UInt64, i: Float, j: Double) { - self.a = a - self.b = b - self.c = c - self.d = d - self.e = e - self.f = f - self.g = g - self.h = h - self.i = i - self.j = j - } -} - -extension Numerics: WvCodable { - static func wvRead(_ r: inout WvReader) -> Numerics { - Numerics(a: r.read(), b: r.read(), c: r.read(), d: r.read(), e: r.read(), f: r.read(), g: r.read(), h: r.read(), i: r.read(), j: r.read()) - } - - func wvWrite(_ w: inout WvWriter) { - w.write(self.a) - w.write(self.b) - w.write(self.c) - w.write(self.d) - w.write(self.e) - w.write(self.f) - w.write(self.g) - w.write(self.h) - w.write(self.i) - w.write(self.j) - } -} - -/// Rich (algebraic) enums and the expanded numeric primitives. -public enum Shapes { - /// Render a shape to a string - public static func describe(shape: Shape) -> String { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(shape) { shape_ptr, shape_len in - shapes_shapes_describe(shape_ptr, shape_len, &outLen, &err) - } - wvTrap(&err) - return wvTakeString(rv, outLen) - } - - /// Scale a shape by a factor, returning a new shape - public static func scale(shape: Shape, factor: Double) -> Shape { - wvLoad() - var err = WvError() - var outLen = 0 - let rv: UnsafePointer? = wvWithEncoded(shape) { shape_ptr, shape_len in - shapes_shapes_scale(shape_ptr, shape_len, factor, &outLen, &err) - } - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Shape.self) - } - - /// Sum a list of bytes into a wide integer (numerics smoke) - public static func sumBytes(values: Data) -> UInt64 { - wvLoad() - var err = WvError() - let rv: UInt64 = wvWithBytes(values) { values_ptr, values_len in - shapes_shapes_sum_bytes(values_ptr, values_len, &err) - } - wvTrap(&err) - return rv - } - - /// Build a Numerics struct from every numeric primitive - public static func makeNumerics(a: Int8, b: Int16, c: Int32, d: Int64, e: UInt8, f: UInt16, g: UInt32, h: UInt64, i: Float, j: Double) -> Numerics { - wvLoad() - var err = WvError() - var outLen = 0 - let rv = shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &outLen, &err) - wvTrap(&err) - return wvTakeBuffer(rv, outLen, as: Numerics.self) - } -} - -// END Shapes.swift diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_d_ts.snap deleted file mode 100644 index 5be9440f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_d_ts.snap +++ /dev/null @@ -1,156 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class DocsEverywhereError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends DocsEverywhereError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof DocsEverywhereError; -/** - * Load the WebAssembly module and check that it matches these bindings. - * Every other export throws until the returned promise resolves. - */ -export declare function init( - source?: string | URL | BufferSource | WebAssembly.Module | Response, -): Promise; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -/** Document storage: create, publish, and observe documents. */ -export declare namespace docs { - /** The errors of the `docs` error domain. */ - export class DocsErrorsError extends $Error { - constructor(code: number, message?: string); - } - /** Returned when no document matches the requested id */ - export class NotFoundError extends docs.DocsErrorsError { - static readonly CODE: 1; - constructor(message?: string); - } - /** Raised when attempting to move into a forbidden state */ - export class InvalidStatusError extends docs.DocsErrorsError { - static readonly CODE: 2; - constructor(message?: string); - } - /** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ - export enum Status { - /** Document is being edited */ - Draft = 0, - /** Document is publicly visible */ - Published = 1, - /** Document is hidden from listings */ - Archived = 2, - } - /** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ - export interface Document { - /** Stable opaque identifier */ - id: bigint; - /** Human-readable title shown in lists */ - title: string; - /** Current lifecycle status */ - status: docs.Status; - /** Free-form tags for grouping */ - tags: string[]; - } - /** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - */ - export interface SavedListener { - /** - * Fires after a document has been persisted successfully - * @param docId Identifier of the saved document - * @param revision Monotonically increasing revision - */ - onSaved(docId: bigint, revision: number): void; - } - /** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ - export class Notebook { - /** Create an empty notebook */ - constructor(); - /** - * Add a document, returning its position in the notebook - * @param document The document to add - */ - insert(document: docs.Document): number; - /** - * Look up a document by identifier, failing when none matches - * @param id Identifier of the document to find - * @throws {docs.DocsErrorsError} - */ - find(id: bigint): docs.Document; - /** The number of documents currently held */ - size(): number; - /** The largest number of documents one notebook will hold */ - static maxDocuments(): number; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; - } - /** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - */ - export function createDocument(title: string, tags: string[]): docs.Document; - /** - * Look up a document by identifier - * @param id Identifier returned from create_document - */ - export function getDocument(id: bigint): docs.Document | null; - /** - * Publish a document in the background - * @param id Identifier of the document to publish - */ - export function publishAsync(id: bigint): Promise; - /** - * Register a listener for saved notifications - * @param listener Receiver of saved notifications - */ - export function watch(listener: docs.SavedListener): void; -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_js.snap deleted file mode 100644 index 252ff588..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_js.snap +++ /dev/null @@ -1,348 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - DocsEverywhereError as $Error, -} from './runtime.js'; -export { DocsEverywhereError, CancelledError } from './runtime.js'; -import { $loadWasm, $unloaded } from './linear.js'; - -let $raw = $unloaded('docs_everywhere'); -// Handles are linear-memory addresses; object tokens are u64s. -const $token = Number; -let $loading = null; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['docs', [ - [0x00b2187fd75634ecn, 0x2263c6f787bf6081n, 'docs.publish_async'], - [0x12fc242b9dc6444cn, 0x96405f484a642b31n, 'docs.Notebook.insert'], - [0x2213822ed05cd5bdn, 0x26c5833392aa63cen, 'docs.Notebook.new'], - [0x2bd762bcdaa207ffn, 0x1b0ebab2d04c49b0n, 'docs.SavedListener'], - [0x2e086c45e9d617den, 0x4d43bf32358c01ffn, 'docs.Notebook.find'], - [0x4817dc98197ad960n, 0xf8017b5a7d58b2c7n, 'docs.Notebook.size'], - [0x614de47452d5a55cn, 0xd9aae1472494ffefn, 'docs.get_document'], - [0x67c5cb9fc119bcd5n, 0x08d5b25a1a22a7fan, 'docs.Document'], - [0x8d484bf2d78c9b70n, 0xe9093cf96912194cn, 'docs.Notebook.max_documents'], - [0xaea61d6451691c67n, 0xe4704128b3524b3dn, 'docs.watch'], - [0xd68d6ad1540cdec5n, 0x01afeb40b0a8bfe1n, 'docs.Notebook'], - [0xdb0601444a4760ccn, 0xe56c69629b444797n, 'docs.create_document'], - [0xe12d6f4838347b3en, 0xa9bab86646d84f51n, 'docs.Status'], - [0xee2f8d25b3313b50n, 0x9691232f35439a2bn, 'docs.DocsErrors'], - ]], -]; - -/** - * Load the WebAssembly module (by default `docs_everywhere.wasm` next to this file, or - * the file named by `DOCS_EVERYWHERE_LIBRARY` on Node.js) and check that it matches - * these bindings. Every other export throws until the returned promise - * resolves. - */ -export function init(source) { - $loading ??= $loadWasm(source, 'docs_everywhere', 'DOCS_EVERYWHERE_LIBRARY', new URL('./docs_everywhere.wasm', import.meta.url)) - .then((m) => { - $raw = $bind(m); - try { - $verify($raw, 'docs_everywhere', 'docs_everywhere', 4, $contract); - } catch (e) { - $raw = $unloaded('docs_everywhere'); - throw e; - } - }) - .catch((e) => { - $loading = null; - throw e; - }); - return $loading; -} - -function $vt_docs_everywhere_docs_SavedListener(m) { - return [ - [['i32', 'i64', 'i32', 'i32'], [], 'vijii', (ctx, p0, p1, err) => { - try { - m.adapter(ctx).on_saved(p0, p1); - } catch (e) { - m.foreign(err, e); - } - }], - ]; -} - -// The raw entry points, one per C symbol. -function $bind(m) { - const x = m.x; - return { - docs_everywhere_abi_version: () => x.docs_everywhere_abi_version() >>> 0, - docs_everywhere_debug_live: (kind) => m.live(x.docs_everywhere_debug_live(m.num(kind)), kind), - docs_everywhere_cancel_token_create: () => x.docs_everywhere_cancel_token_create(), - docs_everywhere_cancel_token_cancel: (t) => x.docs_everywhere_cancel_token_cancel(m.handle(t)), - docs_everywhere_cancel_token_destroy: (t) => x.docs_everywhere_cancel_token_destroy(m.handle(t)), - docs_everywhere_docs_contract: () => m.contract(x.docs_everywhere_docs_contract), - docs_everywhere_docs_Notebook_clone: (h) => x.docs_everywhere_docs_Notebook_clone(m.handle(h)), - docs_everywhere_docs_Notebook_destroy: (h) => x.docs_everywhere_docs_Notebook_destroy(m.handle(h)), - docs_everywhere_docs_create_document: (a0, a1) => { - let s0 = null, s1 = null; - try { - s0 = m.str(a0); - s1 = m.data(a1); - const r = x.docs_everywhere_docs_create_document(s0[0], s0[1], s1[0], s1[1], m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - m.unstage(s1); - } - }, - docs_everywhere_docs_get_document: (a0) => { - const r = x.docs_everywhere_docs_get_document(m.i64(a0), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - docs_everywhere_docs_publish_async: (a0) => { - return m.launch(['i32', 'i32', 'i32'], [], 'viii', (cb, ctx) => x.docs_everywhere_docs_publish_async(m.i64(a0), cb, ctx), (result) => result !== 0); - }, - docs_everywhere_docs_watch: (a0) => { - x.docs_everywhere_docs_watch(m.register(a0), m.vtable('docs_everywhere_docs_SavedListener', () => $vt_docs_everywhere_docs_SavedListener(m)), m.err); - m.check(); - }, - docs_everywhere_docs_Notebook_new: () => { - const r = x.docs_everywhere_docs_Notebook_new(m.err); - m.check(); - return r; - }, - docs_everywhere_docs_Notebook_insert: (self, a0) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.docs_everywhere_docs_Notebook_insert(m.handle(self), s0[0], s0[1], m.err); - m.check(); - return r; - } finally { - m.unstage(s0); - } - }, - docs_everywhere_docs_Notebook_find: (self, a0) => { - const r = x.docs_everywhere_docs_Notebook_find(m.handle(self), m.i64(a0), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - docs_everywhere_docs_Notebook_size: (self) => { - const r = x.docs_everywhere_docs_Notebook_size(m.handle(self), m.err); - m.check(); - return r; - }, - docs_everywhere_docs_Notebook_max_documents: () => { - const r = x.docs_everywhere_docs_Notebook_max_documents(m.err); - m.check(); - return r; - }, - }; -} - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.docs_everywhere_debug_live(kind); -} - -const docs$DocsErrorsError = class DocsErrorsError extends $Error {}; -const docs$NotFoundError = class NotFoundError extends docs$DocsErrorsError { - static CODE = 1; - constructor(message = 'Document not found') { - super(1, message); - } -}; -const docs$InvalidStatusError = class InvalidStatusError extends docs$DocsErrorsError { - static CODE = 2; - constructor(message = 'Invalid status transition') { - super(2, message); - } -}; -const $codes$docs$DocsErrors = new Map([ - [1, docs$NotFoundError], - [2, docs$InvalidStatusError], -]); -const $payloads$docs$DocsErrors = new Map(); -function $from$docs$DocsErrors(e) { - return $domain(e, $codes$docs$DocsErrors, $payloads$docs$DocsErrors); -} - -const docs$Status = Object.freeze({ - Draft: 0, - Published: 1, - Archived: 2, - 0: 'Draft', - 1: 'Published', - 2: 'Archived', -}); - -function $w$docs$Document(w, v) { - w.writeI64(v.id); - w.writeString(v.title); - w.writeI32(v.status); - $w_list_string(w, v.tags); -} -function $r$docs$Document(r) { - return { - id: r.readI64(), - title: r.readString(), - status: r.readI32(), - tags: $r_list_string(r), - }; -} -/** `[string]` */ -function $w_list_string(w, v) { - w.writeList(v, $W.String); -} -function $r_list_string(r) { - return r.readList($R.String); -} -/** `Document?` */ -function $w_opt_Document(w, v) { - w.writeOpt(v, $w$docs$Document); -} -function $r_opt_Document(r) { - return r.readOpt($r$docs$Document); -} - -function $adapt$docs$SavedListener(impl) { - $impl(impl, 'SavedListener'); - return { - on_saved($a0, $a1) { - impl.onSaved($a0, $a1); - }, - }; -} - -const docs$Notebook = class Notebook extends $Object { - constructor() { - super(); - try { - $own(this, $raw.docs_everywhere_docs_Notebook_new()); - } catch ($e) { - throw $fault($e); - } - } - static $destroy(h) { - $raw.docs_everywhere_docs_Notebook_destroy(h); - } - static $clone(h) { - return $raw.docs_everywhere_docs_Notebook_clone(h); - } - insert(document) { - const $self = $lend(this, docs$Notebook); - try { - return $raw.docs_everywhere_docs_Notebook_insert($self, $encode(document, $w$docs$Document)); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - find(id) { - const $self = $lend(this, docs$Notebook); - try { - return $decode($raw.docs_everywhere_docs_Notebook_find($self, id), $r$docs$Document); - } catch ($e) { - throw $from$docs$DocsErrors($e); - } finally { - $unlend(this); - } - } - size() { - const $self = $lend(this, docs$Notebook); - try { - return $raw.docs_everywhere_docs_Notebook_size($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - static maxDocuments() { - try { - return $raw.docs_everywhere_docs_Notebook_max_documents(); - } catch ($e) { - throw $fault($e); - } - } -}; - -const docs$createDocument = function createDocument(title, tags) { - try { - return $decode($raw.docs_everywhere_docs_create_document(title, $encode(tags, $w_list_string)), $r$docs$Document); - } catch ($e) { - throw $fault($e); - } -}; - -const docs$getDocument = function getDocument(id) { - try { - return $decode($raw.docs_everywhere_docs_get_document(id), $r_opt_Document); - } catch ($e) { - throw $fault($e); - } -}; - -const docs$publishAsync = async function publishAsync(id) { - try { - return await $raw.docs_everywhere_docs_publish_async(id); - } catch ($e) { - throw $fault($e); - } -}; - -const docs$watch = function watch(listener) { - try { - $raw.docs_everywhere_docs_watch($adapt$docs$SavedListener(listener)); - } catch ($e) { - throw $fault($e); - } -}; - -export const docs = Object.freeze({ - DocsErrorsError: docs$DocsErrorsError, - NotFoundError: docs$NotFoundError, - InvalidStatusError: docs$InvalidStatusError, - Status: docs$Status, - Notebook: docs$Notebook, - createDocument: docs$createDocument, - getDocument: docs$getDocument, - publishAsync: docs$publishAsync, - watch: docs$watch, -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_d_ts.snap deleted file mode 100644 index ce2fd5ab..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_d_ts.snap +++ /dev/null @@ -1,285 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class EdgeCasesError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends EdgeCasesError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof EdgeCasesError; -/** - * Load the WebAssembly module and check that it matches these bindings. - * Every other export throws until the returned promise resolves. - */ -export declare function init( - source?: string | URL | BufferSource | WebAssembly.Module | Response, -): Promise; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -export declare namespace edge_cases { - /** The errors of the `edge_cases` error domain. */ - export class EdgeErrorsError extends $Error { - constructor(code: number, message?: string); - } - /** Resource is busy */ - export class BusyError extends edge_cases.EdgeErrorsError { - static readonly CODE: 7; - constructor(message?: string); - } - /** - * Deprecated enum with non-contiguous values and keyword-like variants - * @deprecated Use Mode instead - */ - export enum Legacy { - None = 0, - Default = 5, - Class = 10, - } - export enum Mode { - Fast = 1, - Safe = 2, - } - export interface Leaf { - weight: number; - } - /** Middle layer of a three-deep struct nesting */ - export interface Inner { - leaf: edge_cases.Leaf; - leaves: edge_cases.Leaf[]; - } - export interface Outer { - inner: edge_cases.Inner; - maybe_inner: edge_cases.Inner | null; - by_name: Record; - modes: edge_cases.Mode[]; - matrix: number[][]; - sparse: (bigint | null)[]; - blob: Uint8Array | null; - note: string | null; - by_id: Record; - _hidden: boolean; - v2: number; - } - /** Every field is a reserved word somewhere */ - export interface Keywords { - class: number; - def: number; - import: number; - from: number; - func: number; - range: number; - register: number; - volatile: number; - new: number; - default: number; - end: number; - in: number; - is: number; - int: number; - var: number; - val: number; - self: number; - this: number; - object: number; - string: number; - case: number; - select: number; - switch: number; - void: number; - union: number; - namespace: number; - operator: number; - template: number; - package: number; - protocol: number; - guard: number; - defer: number; - inout: number; - internal: number; - when: number; - fun: number; - data: number; - late: number; - part: number; - out: number; - ref: number; - event: number; - base: number; - lambda: number; - yield: number; - nil: number; - null: number; - begin: number; - rescue: number; - module: number; - chan: number; - go: number; - let: number; - export: number; - delete: number; - typedef: number; - sizeof: number; - static: number; - super: number; - } - /** - * Deprecated struct - * @deprecated Use Outer instead - */ - export interface Retired { - id: bigint; - } - /** Record carrying objects in every buffered position */ - export interface WidgetHolder { - primary: edge_cases.Widget; - fallback: edge_cases.Widget | null; - extras: edge_cases.Widget[]; - by_name: Record; - } - /** Callback interface carrying composite payloads and objects */ - export interface BatchListener { - /** Composite payloads cross as value buffers */ - onBatch(items: edge_cases.Outer[], raw: Uint8Array, first: edge_cases.Outer | null, mode: edge_cases.Mode): void; - /** Objects cross as strong references the consumer must release */ - onWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null, children: edge_cases.Widget[]): void; - onTick(): void; - /** Scalar return travels back through the vtable */ - shouldContinue(class_: number): boolean; - /** Enum return travels back as its integer discriminant */ - pickMode(): edge_cases.Mode; - } - /** Callback interface whose name and methods are keywords */ - export interface Keyword { - delete(self: number): void; - import(): number; - } - /** Interface with no constructor; instances come from make_widget */ - export class Widget { - private constructor(); - /** - * Void method that can fail - * @throws {edge_cases.EdgeErrorsError} - */ - refresh(): void; - rename(class_: string, import_: string | null): string; - /** - * Async method with a struct result - * @throws {edge_cases.EdgeErrorsError} - */ - fetch(id: bigint): Promise; - /** @deprecated Use rename instead */ - oldName(): string; - /** Spelled like the release method of several wrappers */ - close_(): boolean; - /** Spelled like the release method of the Dart and .NET wrappers */ - dispose(force: boolean): void; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; - } - /** - * Deprecated interface with a throwing constructor - * @deprecated Use Widget instead - */ - export class Vault { - private constructor(); - /** - * Fails when the vault is busy - * @throws {edge_cases.EdgeErrorsError} - */ - static open(path: string): edge_cases.Vault; - peek(): edge_cases.Widget | null; - /** Spelled like the handle accessor of several wrappers */ - handle(): bigint; - /** - * Throwing static - * @throws {edge_cases.EdgeErrorsError} - */ - static probe(): boolean; - /** Static spelled like an inherited object member */ - static toString(): string; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; - } - /** No parameters and no result */ - export function noop(): void; - /** Function named after a keyword whose parameters are all keywords */ - export function select(class_: number, from: string, func: string[], range: number | null, type: boolean): number; - /** Optional list of optional structs, the deepest legal nesting */ - export function nestedOptional(items: (edge_cases.Outer | null)[] | null): (edge_cases.Outer | null)[] | null; - export function optionalParams(text: string | null, count: number | null, outer: edge_cases.Outer | null, tags: string[] | null, blob: Uint8Array | null, mode: edge_cases.Mode | null): edge_cases.Mode | null; - /** Two callback interface parameters on one function */ - export function registerBatch(listener: edge_cases.BatchListener, keyword: edge_cases.Keyword): void; - /** Objects in every buffered position */ - export function widgetsEverywhere(list: edge_cases.Widget[], byName: Record, sparse: (edge_cases.Widget | null)[], holder: edge_cases.WidgetHolder): edge_cases.WidgetHolder; - /** Iterator of objects */ - export function widgetStream(): IterableIterator; - /** - * Async returning an object - * @throws {edge_cases.EdgeErrorsError} - */ - export function asyncWidget(): Promise; - /** Factory for an interface with no constructor */ - export function makeWidget(label: string): edge_cases.Widget; - /** - * Optional interface return - * @throws {edge_cases.EdgeErrorsError} - */ - export function findWidget(id: bigint): edge_cases.Widget | null; - /** Interface parameter, including an optional one */ - export function inspectWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null): string; - /** Iterator of strings */ - export function stringStream(): IterableIterator; - /** Iterator of scalars */ - export function scalarStream(): IterableIterator; - /** Iterator of enums */ - export function modeStream(): IterableIterator; - /** Async with no result */ - export function asyncVoid(): Promise; - export function asyncBytes(size: number): Promise; - /** @throws {edge_cases.EdgeErrorsError} */ - export function asyncStruct(): Promise; - /** @throws {edge_cases.EdgeErrorsError} */ - export function asyncList(options?: { signal?: AbortSignal }): Promise; - export function asyncOptional(): Promise; - export function asyncEnum(): Promise; - export function asyncKeywords(class_: string, import_: number): Promise; - export function keywordsRoundtrip(value: edge_cases.Keywords): edge_cases.Keywords; - /** @deprecated Use Mode-based APIs */ - export function legacyValue(value: edge_cases.Legacy): edge_cases.Retired; -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_js.snap deleted file mode 100644 index 5d5b6db3..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_js.snap +++ /dev/null @@ -1,1270 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - EdgeCasesError as $Error, -} from './runtime.js'; -export { EdgeCasesError, CancelledError } from './runtime.js'; -import { $loadWasm, $unloaded } from './linear.js'; - -let $raw = $unloaded('edge_cases'); -// Handles are linear-memory addresses; object tokens are u64s. -const $token = Number; -let $loading = null; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['edge_cases', [ - [0x056a86511785ba2bn, 0x5cba3f16d57c6cdbn, 'edge_cases.make_widget'], - [0x07bfa9ecb2b720d2n, 0x3813b3f57c64bf35n, 'edge_cases.EdgeErrors'], - [0x0aaa11867e37f991n, 0xf090dd52146ebdf0n, 'edge_cases.Widget.refresh'], - [0x0f9701493898ae4cn, 0x6944f47a275c96e8n, 'edge_cases.Inner'], - [0x1226b1adac179f7dn, 0x0e95a00ab0bef637n, 'edge_cases.async_keywords'], - [0x16d36bef1541136cn, 0x5ea7021ec324298fn, 'edge_cases.find_widget'], - [0x1b7324c55e980febn, 0xabf87fe372358f42n, 'edge_cases.Widget.old_name'], - [0x1cbd731467c2bab8n, 0x4915b9989c2c9074n, 'edge_cases.Vault'], - [0x2c2a6de67d894b0an, 0x50a4496b1e7191can, 'edge_cases.Vault.open'], - [0x322b4cfcd2d6861cn, 0xca7527a379ae8465n, 'edge_cases.Keywords'], - [0x3f8e23a5f32906f4n, 0x8d4c908d796f130dn, 'edge_cases.Leaf'], - [0x42ab8a685457b97cn, 0xe5ce30500cadb32dn, 'edge_cases.Vault.handle'], - [0x4a68540e2836d3d0n, 0xa20e44c71e420ed2n, 'edge_cases.mode_stream'], - [0x4e030580ac0b111dn, 0xb68b7a18d910e175n, 'edge_cases.legacy_value'], - [0x4f5e4e5291864ef3n, 0x55a1a88b21a08f8bn, 'edge_cases.Legacy'], - [0x4f84b06171b4213dn, 0xbaedcfdebb6a2820n, 'edge_cases.scalar_stream'], - [0x5067a5ff2d9ef910n, 0xbfc79b18fc4714fcn, 'edge_cases.Widget'], - [0x59aff159112fbf5bn, 0x18e4def57a8cfa49n, 'edge_cases.optional_params'], - [0x5daf65b98fa77c48n, 0xc817a8cb202e59c2n, 'edge_cases.keywords_roundtrip'], - [0x5de8a5c9726c2c5an, 0x7c50b5b65f7a896dn, 'edge_cases.register_batch'], - [0x618a620309994c3cn, 0x826ad0b1d208d723n, 'edge_cases.async_bytes'], - [0x648285cdba0a7b8en, 0xcf6e80e783a4d8ben, 'edge_cases.WidgetHolder'], - [0x649c78079824b10dn, 0x5b12dd26e0634b69n, 'edge_cases.async_widget'], - [0x7da7a7357d122a37n, 0xf544e025ffaa3d20n, 'edge_cases.async_void'], - [0x7e4015bc3a472e2dn, 0xfe22291d90b82e4an, 'edge_cases.widget_stream'], - [0x805276e5d81fdba7n, 0xd9b64e360bd03b2bn, 'edge_cases.Outer'], - [0x80e1183326542df4n, 0xe8a57e12cc6ed276n, 'edge_cases.select'], - [0x923ce5e9bcd769a1n, 0x0b973f5dc07dda8bn, 'edge_cases.Vault.to_string'], - [0x98ca8c01f5239404n, 0xfe11c93eb6b303efn, 'edge_cases.Widget.rename'], - [0xa13e22e98ea278d4n, 0xf1fae215d7387800n, 'edge_cases.widgets_everywhere'], - [0xa84a8c9bf2a881e1n, 0xbe1dc840bf221996n, 'edge_cases.async_optional'], - [0xad958c478fbf205dn, 0xe386b0716d24f19bn, 'edge_cases.Retired'], - [0xb2fc3964ef56ab39n, 0x73c325c8bc351236n, 'edge_cases.async_list'], - [0xb3403af521cf0006n, 0x6abc30d48206a3ban, 'edge_cases.Widget.close'], - [0xb794304a22a406b8n, 0xf6fb98711c516105n, 'edge_cases.async_struct'], - [0xba3936106a49c47dn, 0xb7025f2fadd6288en, 'edge_cases.Vault.peek'], - [0xc155b0c3cfb164f4n, 0xc777ae57d92ebc47n, 'edge_cases.noop'], - [0xcdc18607777019ddn, 0xb81a31e5c20aa7a8n, 'edge_cases.inspect_widget'], - [0xd164a82aeb64b8ccn, 0xded02c68005c4737n, 'edge_cases.nested_optional'], - [0xd978ccad2468038fn, 0x30c39c65d6235153n, 'edge_cases.Mode'], - [0xdbe9d947431c9124n, 0x39537fb848fdf980n, 'edge_cases.BatchListener'], - [0xeae8b7862c08bb07n, 0xfe12eeaa830452ean, 'edge_cases.Keyword'], - [0xf1e155a05ae262b8n, 0xb575eb818a423d42n, 'edge_cases.async_enum'], - [0xf22e210bdbce7343n, 0x2a5e0f801bc8c768n, 'edge_cases.Widget.dispose'], - [0xf3c7cf675563ee6cn, 0x0ecffeef399facc8n, 'edge_cases.Vault.probe'], - [0xfd34bc67acac379en, 0x576caa14f3c0901an, 'edge_cases.Widget.fetch'], - [0xff2e9d2bff941786n, 0x7e96764ef1f5fbaan, 'edge_cases.string_stream'], - ]], -]; - -/** - * Load the WebAssembly module (by default `edge_cases.wasm` next to this file, or - * the file named by `EDGE_CASES_LIBRARY` on Node.js) and check that it matches - * these bindings. Every other export throws until the returned promise - * resolves. - */ -export function init(source) { - $loading ??= $loadWasm(source, 'edge_cases', 'EDGE_CASES_LIBRARY', new URL('./edge_cases.wasm', import.meta.url)) - .then((m) => { - $raw = $bind(m); - try { - $verify($raw, 'edge_cases', 'edge_cases', 4, $contract); - } catch (e) { - $raw = $unloaded('edge_cases'); - throw e; - } - }) - .catch((e) => { - $loading = null; - throw e; - }); - return $loading; -} - -function $vt_edge_cases_edge_cases_BatchListener(m) { - return [ - [['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiiiiiii', (ctx, p0, p1, p2, p3, p4, p5, p6, err) => { - try { - m.adapter(ctx).on_batch(m.readData(p0, p1), m.readData(p2, p3), m.readData(p4, p5), p6); - } catch (e) { - m.foreign(err, e); - } - }], - [['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiiii', (ctx, p0, p1, p2, p3, err) => { - try { - m.adapter(ctx).on_widget(p0, p1 === 0 ? null : p1, m.readData(p2, p3)); - } catch (e) { - m.foreign(err, e); - } - }], - [['i32', 'i32'], [], 'vii', (ctx, err) => { - try { - m.adapter(ctx).on_tick(); - } catch (e) { - m.foreign(err, e); - } - }], - [['i32', 'i32', 'i32'], ['i32'], 'iiii', (ctx, p0, err) => { - try { - return m.adapter(ctx).should_continue(p0) ? 1 : 0; - } catch (e) { - m.foreign(err, e); - return 0; - } - }], - [['i32', 'i32'], ['i32'], 'iii', (ctx, err) => { - try { - return m.adapter(ctx).pick_mode(); - } catch (e) { - m.foreign(err, e); - return 0; - } - }], - ]; -} - -function $vt_edge_cases_edge_cases_Keyword(m) { - return [ - [['i32', 'i32', 'i32'], [], 'viii', (ctx, p0, err) => { - try { - m.adapter(ctx).delete(p0); - } catch (e) { - m.foreign(err, e); - } - }], - [['i32', 'i32'], ['i32'], 'iii', (ctx, err) => { - try { - return m.adapter(ctx).import(); - } catch (e) { - m.foreign(err, e); - return 0; - } - }], - ]; -} - -// The raw entry points, one per C symbol. -function $bind(m) { - const x = m.x; - return { - edge_cases_abi_version: () => x.edge_cases_abi_version() >>> 0, - edge_cases_debug_live: (kind) => m.live(x.edge_cases_debug_live(m.num(kind)), kind), - edge_cases_cancel_token_create: () => x.edge_cases_cancel_token_create(), - edge_cases_cancel_token_cancel: (t) => x.edge_cases_cancel_token_cancel(m.handle(t)), - edge_cases_cancel_token_destroy: (t) => x.edge_cases_cancel_token_destroy(m.handle(t)), - edge_cases_edge_cases_contract: () => m.contract(x.edge_cases_edge_cases_contract), - edge_cases_edge_cases_Widget_clone: (h) => x.edge_cases_edge_cases_Widget_clone(m.handle(h)), - edge_cases_edge_cases_Widget_destroy: (h) => x.edge_cases_edge_cases_Widget_destroy(m.handle(h)), - edge_cases_edge_cases_Vault_clone: (h) => x.edge_cases_edge_cases_Vault_clone(m.handle(h)), - edge_cases_edge_cases_Vault_destroy: (h) => x.edge_cases_edge_cases_Vault_destroy(m.handle(h)), - edge_cases_edge_cases_noop: () => { - x.edge_cases_edge_cases_noop(m.err); - m.check(); - }, - edge_cases_edge_cases_select: (a0, a1, a2, a3, a4) => { - let s1 = null, s2 = null, s3 = null; - try { - s1 = m.str(a1); - s2 = m.data(a2); - s3 = m.data(a3); - const r = x.edge_cases_edge_cases_select(m.num(a0), s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], m.bool(a4), m.err); - m.check(); - return r; - } finally { - m.unstage(s1); - m.unstage(s2); - m.unstage(s3); - } - }, - edge_cases_edge_cases_nested_optional: (a0) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.edge_cases_edge_cases_nested_optional(s0[0], s0[1], m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - } - }, - edge_cases_edge_cases_optional_params: (a0, a1, a2, a3, a4, a5) => { - let s0 = null, s1 = null, s2 = null, s3 = null, s4 = null, s5 = null; - try { - s0 = m.data(a0); - s1 = m.data(a1); - s2 = m.data(a2); - s3 = m.data(a3); - s4 = m.data(a4); - s5 = m.data(a5); - const r = x.edge_cases_edge_cases_optional_params(s0[0], s0[1], s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], s4[0], s4[1], s5[0], s5[1], m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - m.unstage(s1); - m.unstage(s2); - m.unstage(s3); - m.unstage(s4); - m.unstage(s5); - } - }, - edge_cases_edge_cases_register_batch: (a0, a1) => { - x.edge_cases_edge_cases_register_batch(m.register(a0), m.vtable('edge_cases_edge_cases_BatchListener', () => $vt_edge_cases_edge_cases_BatchListener(m)), m.register(a1), m.vtable('edge_cases_edge_cases_Keyword', () => $vt_edge_cases_edge_cases_Keyword(m)), m.err); - m.check(); - }, - edge_cases_edge_cases_widgets_everywhere: (a0, a1, a2, a3) => { - let s0 = null, s1 = null, s2 = null, s3 = null; - try { - s0 = m.data(a0); - s1 = m.data(a1); - s2 = m.data(a2); - s3 = m.data(a3); - const r = x.edge_cases_edge_cases_widgets_everywhere(s0[0], s0[1], s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - m.unstage(s1); - m.unstage(s2); - m.unstage(s3); - } - }, - edge_cases_edge_cases_widget_stream: () => { - const r = x.edge_cases_edge_cases_widget_stream(m.err); - m.check(); - return r; - }, - edge_cases_edge_cases_WidgetStreamIterator_next: (h) => { - const has = x.edge_cases_edge_cases_WidgetStreamIterator_next(m.handle(h), m.item, m.err); - m.check(); - if (has === 0) return undefined; - const p = m.view().getUint32(m.item, true); - return p; - }, - edge_cases_edge_cases_WidgetStreamIterator_destroy: (h) => x.edge_cases_edge_cases_WidgetStreamIterator_destroy(m.handle(h)), - edge_cases_edge_cases_async_widget: () => { - return m.launch(['i32', 'i32', 'i32'], [], 'viii', (cb, ctx) => x.edge_cases_edge_cases_async_widget(cb, ctx), (result) => result); - }, - edge_cases_edge_cases_make_widget: (a0) => { - let s0 = null; - try { - s0 = m.str(a0); - const r = x.edge_cases_edge_cases_make_widget(s0[0], s0[1], m.err); - m.check(); - return r; - } finally { - m.unstage(s0); - } - }, - edge_cases_edge_cases_find_widget: (a0) => { - const r = x.edge_cases_edge_cases_find_widget(m.i64(a0), m.err); - m.check(); - return r === 0 ? null : r; - }, - edge_cases_edge_cases_inspect_widget: (a0, a1) => { - const r = x.edge_cases_edge_cases_inspect_widget(m.handle(a0), m.handleOpt(a1), m.len, m.err); - m.check(); - return m.takeStr(r, m.outLen()); - }, - edge_cases_edge_cases_string_stream: () => { - const r = x.edge_cases_edge_cases_string_stream(m.err); - m.check(); - return r; - }, - edge_cases_edge_cases_StringStreamIterator_next: (h) => { - const has = x.edge_cases_edge_cases_StringStreamIterator_next(m.handle(h), m.item, m.len, m.err); - m.check(); - if (has === 0) return undefined; - const p = m.view().getUint32(m.item, true); - return m.takeStr(p, m.outLen()); - }, - edge_cases_edge_cases_StringStreamIterator_destroy: (h) => x.edge_cases_edge_cases_StringStreamIterator_destroy(m.handle(h)), - edge_cases_edge_cases_scalar_stream: () => { - const r = x.edge_cases_edge_cases_scalar_stream(m.err); - m.check(); - return r; - }, - edge_cases_edge_cases_ScalarStreamIterator_next: (h) => { - const has = x.edge_cases_edge_cases_ScalarStreamIterator_next(m.handle(h), m.item, m.err); - m.check(); - if (has === 0) return undefined; - return m.view().getInt32(m.item, true); - }, - edge_cases_edge_cases_ScalarStreamIterator_destroy: (h) => x.edge_cases_edge_cases_ScalarStreamIterator_destroy(m.handle(h)), - edge_cases_edge_cases_mode_stream: () => { - const r = x.edge_cases_edge_cases_mode_stream(m.err); - m.check(); - return r; - }, - edge_cases_edge_cases_ModeStreamIterator_next: (h) => { - const has = x.edge_cases_edge_cases_ModeStreamIterator_next(m.handle(h), m.item, m.err); - m.check(); - if (has === 0) return undefined; - return m.view().getInt32(m.item, true); - }, - edge_cases_edge_cases_ModeStreamIterator_destroy: (h) => x.edge_cases_edge_cases_ModeStreamIterator_destroy(m.handle(h)), - edge_cases_edge_cases_async_void: () => { - return m.launch(['i32', 'i32'], [], 'vii', (cb, ctx) => x.edge_cases_edge_cases_async_void(cb, ctx), () => undefined); - }, - edge_cases_edge_cases_async_bytes: (a0) => { - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_bytes(m.num(a0), cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); - }, - edge_cases_edge_cases_async_struct: () => { - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_struct(cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); - }, - edge_cases_edge_cases_async_list: (token) => { - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_list(m.handleOpt(token), cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); - }, - edge_cases_edge_cases_async_optional: () => { - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_optional(cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); - }, - edge_cases_edge_cases_async_enum: () => { - return m.launch(['i32', 'i32', 'i32'], [], 'viii', (cb, ctx) => x.edge_cases_edge_cases_async_enum(cb, ctx), (result) => result); - }, - edge_cases_edge_cases_async_keywords: (a0, a1) => { - let s0 = null; - try { - s0 = m.str(a0); - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_keywords(s0[0], s0[1], m.num(a1), cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); - } finally { - m.unstage(s0); - } - }, - edge_cases_edge_cases_keywords_roundtrip: (a0) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.edge_cases_edge_cases_keywords_roundtrip(s0[0], s0[1], m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - } - }, - edge_cases_edge_cases_legacy_value: (a0) => { - const r = x.edge_cases_edge_cases_legacy_value(m.num(a0), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - edge_cases_edge_cases_Widget_refresh: (self) => { - x.edge_cases_edge_cases_Widget_refresh(m.handle(self), m.err); - m.check(); - }, - edge_cases_edge_cases_Widget_rename: (self, a0, a1) => { - let s0 = null, s1 = null; - try { - s0 = m.str(a0); - s1 = m.data(a1); - const r = x.edge_cases_edge_cases_Widget_rename(m.handle(self), s0[0], s0[1], s1[0], s1[1], m.len, m.err); - m.check(); - return m.takeStr(r, m.outLen()); - } finally { - m.unstage(s0); - m.unstage(s1); - } - }, - edge_cases_edge_cases_Widget_fetch: (self, a0) => { - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_Widget_fetch(m.handle(self), m.i64(a0), cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); - }, - edge_cases_edge_cases_Widget_old_name: (self) => { - const r = x.edge_cases_edge_cases_Widget_old_name(m.handle(self), m.len, m.err); - m.check(); - return m.takeStr(r, m.outLen()); - }, - edge_cases_edge_cases_Widget_close: (self) => { - const r = x.edge_cases_edge_cases_Widget_close(m.handle(self), m.err); - m.check(); - return r !== 0; - }, - edge_cases_edge_cases_Widget_dispose: (self, a0) => { - x.edge_cases_edge_cases_Widget_dispose(m.handle(self), m.bool(a0), m.err); - m.check(); - }, - edge_cases_edge_cases_Vault_open: (a0) => { - let s0 = null; - try { - s0 = m.str(a0); - const r = x.edge_cases_edge_cases_Vault_open(s0[0], s0[1], m.err); - m.check(); - return r; - } finally { - m.unstage(s0); - } - }, - edge_cases_edge_cases_Vault_peek: (self) => { - const r = x.edge_cases_edge_cases_Vault_peek(m.handle(self), m.err); - m.check(); - return r === 0 ? null : r; - }, - edge_cases_edge_cases_Vault_handle: (self) => { - const r = x.edge_cases_edge_cases_Vault_handle(m.handle(self), m.err); - m.check(); - return r; - }, - edge_cases_edge_cases_Vault_probe: () => { - const r = x.edge_cases_edge_cases_Vault_probe(m.err); - m.check(); - return r !== 0; - }, - edge_cases_edge_cases_Vault_to_string: () => { - const r = x.edge_cases_edge_cases_Vault_to_string(m.len, m.err); - m.check(); - return m.takeStr(r, m.outLen()); - }, - }; -} - -// The native cancel tokens behind `AbortSignal`s. -const $tokens = { - create: () => $raw.edge_cases_cancel_token_create(), - cancel: (t) => $raw.edge_cases_cancel_token_cancel(t), - destroy: (t) => $raw.edge_cases_cancel_token_destroy(t), -}; - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.edge_cases_debug_live(kind); -} - -const edge_cases$EdgeErrorsError = class EdgeErrorsError extends $Error {}; -const edge_cases$BusyError = class BusyError extends edge_cases$EdgeErrorsError { - static CODE = 7; - constructor(message = 'Resource is busy') { - super(7, message); - } -}; -const $codes$edge_cases$EdgeErrors = new Map([ - [7, edge_cases$BusyError], -]); -const $payloads$edge_cases$EdgeErrors = new Map(); -function $from$edge_cases$EdgeErrors(e) { - return $domain(e, $codes$edge_cases$EdgeErrors, $payloads$edge_cases$EdgeErrors); -} - -const edge_cases$Legacy = Object.freeze({ - None: 0, - Default: 5, - Class: 10, - 0: 'None', - 5: 'Default', - 10: 'Class', -}); - -const edge_cases$Mode = Object.freeze({ - Fast: 1, - Safe: 2, - 1: 'Fast', - 2: 'Safe', -}); - -function $w$edge_cases$Leaf(w, v) { - w.writeF32(v.weight); -} -function $r$edge_cases$Leaf(r) { - return { - weight: r.readF32(), - }; -} -function $w$edge_cases$Inner(w, v) { - $w$edge_cases$Leaf(w, v.leaf); - $w_list_Leaf(w, v.leaves); -} -function $r$edge_cases$Inner(r) { - return { - leaf: $r$edge_cases$Leaf(r), - leaves: $r_list_Leaf(r), - }; -} -function $w$edge_cases$Outer(w, v) { - $w$edge_cases$Inner(w, v.inner); - $w_opt_Inner(w, v.maybe_inner); - $w_map_string_Inner(w, v.by_name); - $w_list_Mode(w, v.modes); - $w_list_list_i32(w, v.matrix); - $w_list_opt_i64(w, v.sparse); - $w_opt_bytes(w, v.blob); - $w_opt_string(w, v.note); - $w_map_i64_string(w, v.by_id); - w.writeBool(v._hidden); - w.writeU8(v.v2); -} -function $r$edge_cases$Outer(r) { - return { - inner: $r$edge_cases$Inner(r), - maybe_inner: $r_opt_Inner(r), - by_name: $r_map_string_Inner(r), - modes: $r_list_Mode(r), - matrix: $r_list_list_i32(r), - sparse: $r_list_opt_i64(r), - blob: $r_opt_bytes(r), - note: $r_opt_string(r), - by_id: $r_map_i64_string(r), - _hidden: r.readBool(), - v2: r.readU8(), - }; -} -function $w$edge_cases$Keywords(w, v) { - w.writeI32(v.class); - w.writeI32(v.def); - w.writeI32(v.import); - w.writeI32(v.from); - w.writeI32(v.func); - w.writeI32(v.range); - w.writeI32(v.register); - w.writeI32(v.volatile); - w.writeI32(v.new); - w.writeI32(v.default); - w.writeI32(v.end); - w.writeI32(v.in); - w.writeI32(v.is); - w.writeI32(v.int); - w.writeI32(v.var); - w.writeI32(v.val); - w.writeI32(v.self); - w.writeI32(v.this); - w.writeI32(v.object); - w.writeI32(v.string); - w.writeI32(v.case); - w.writeI32(v.select); - w.writeI32(v.switch); - w.writeI32(v.void); - w.writeI32(v.union); - w.writeI32(v.namespace); - w.writeI32(v.operator); - w.writeI32(v.template); - w.writeI32(v.package); - w.writeI32(v.protocol); - w.writeI32(v.guard); - w.writeI32(v.defer); - w.writeI32(v.inout); - w.writeI32(v.internal); - w.writeI32(v.when); - w.writeI32(v.fun); - w.writeI32(v.data); - w.writeI32(v.late); - w.writeI32(v.part); - w.writeI32(v.out); - w.writeI32(v.ref); - w.writeI32(v.event); - w.writeI32(v.base); - w.writeI32(v.lambda); - w.writeI32(v.yield); - w.writeI32(v.nil); - w.writeI32(v.null); - w.writeI32(v.begin); - w.writeI32(v.rescue); - w.writeI32(v.module); - w.writeI32(v.chan); - w.writeI32(v.go); - w.writeI32(v.let); - w.writeI32(v.export); - w.writeI32(v.delete); - w.writeI32(v.typedef); - w.writeI32(v.sizeof); - w.writeI32(v.static); - w.writeI32(v.super); -} -function $r$edge_cases$Keywords(r) { - return { - class: r.readI32(), - def: r.readI32(), - import: r.readI32(), - from: r.readI32(), - func: r.readI32(), - range: r.readI32(), - register: r.readI32(), - volatile: r.readI32(), - new: r.readI32(), - default: r.readI32(), - end: r.readI32(), - in: r.readI32(), - is: r.readI32(), - int: r.readI32(), - var: r.readI32(), - val: r.readI32(), - self: r.readI32(), - this: r.readI32(), - object: r.readI32(), - string: r.readI32(), - case: r.readI32(), - select: r.readI32(), - switch: r.readI32(), - void: r.readI32(), - union: r.readI32(), - namespace: r.readI32(), - operator: r.readI32(), - template: r.readI32(), - package: r.readI32(), - protocol: r.readI32(), - guard: r.readI32(), - defer: r.readI32(), - inout: r.readI32(), - internal: r.readI32(), - when: r.readI32(), - fun: r.readI32(), - data: r.readI32(), - late: r.readI32(), - part: r.readI32(), - out: r.readI32(), - ref: r.readI32(), - event: r.readI32(), - base: r.readI32(), - lambda: r.readI32(), - yield: r.readI32(), - nil: r.readI32(), - null: r.readI32(), - begin: r.readI32(), - rescue: r.readI32(), - module: r.readI32(), - chan: r.readI32(), - go: r.readI32(), - let: r.readI32(), - export: r.readI32(), - delete: r.readI32(), - typedef: r.readI32(), - sizeof: r.readI32(), - static: r.readI32(), - super: r.readI32(), - }; -} -function $w$edge_cases$Retired(w, v) { - w.writeI64(v.id); -} -function $r$edge_cases$Retired(r) { - return { - id: r.readI64(), - }; -} -function $w$edge_cases$WidgetHolder(w, v) { - $w$edge_cases$Widget(w, v.primary); - $w_opt_Widget(w, v.fallback); - $w_list_Widget(w, v.extras); - $w_map_string_Widget(w, v.by_name); -} -function $r$edge_cases$WidgetHolder(r) { - return { - primary: $r$edge_cases$Widget(r), - fallback: $r_opt_Widget(r), - extras: $r_list_Widget(r), - by_name: $r_map_string_Widget(r), - }; -} -function $w$edge_cases$Widget(w, v) { - w.writeU64(BigInt($clone(v, edge_cases$Widget))); -} -function $r$edge_cases$Widget(r) { - return $adopt(edge_cases$Widget, $token(r.readU64())); -} -/** `[Leaf]` */ -function $w_list_Leaf(w, v) { - w.writeList(v, $w$edge_cases$Leaf); -} -function $r_list_Leaf(r) { - return r.readList($r$edge_cases$Leaf); -} -/** `[Mode]` */ -function $w_list_Mode(w, v) { - w.writeList(v, $W.I32); -} -function $r_list_Mode(r) { - return r.readList($R.I32); -} -/** `[Outer]` */ -function $w_list_Outer(w, v) { - w.writeList(v, $w$edge_cases$Outer); -} -function $r_list_Outer(r) { - return r.readList($r$edge_cases$Outer); -} -/** `[Widget]` */ -function $w_list_Widget(w, v) { - w.writeList(v, $w$edge_cases$Widget); -} -function $r_list_Widget(r) { - return r.readList($r$edge_cases$Widget); -} -/** `[i32]` */ -function $w_list_i32(w, v) { - w.writeList(v, $W.I32); -} -function $r_list_i32(r) { - return r.readList($R.I32); -} -/** `[[i32]]` */ -function $w_list_list_i32(w, v) { - w.writeList(v, $w_list_i32); -} -function $r_list_list_i32(r) { - return r.readList($r_list_i32); -} -/** `[Outer?]` */ -function $w_list_opt_Outer(w, v) { - w.writeList(v, $w_opt_Outer); -} -function $r_list_opt_Outer(r) { - return r.readList($r_opt_Outer); -} -/** `[Widget?]` */ -function $w_list_opt_Widget(w, v) { - w.writeList(v, $w_opt_Widget); -} -function $r_list_opt_Widget(r) { - return r.readList($r_opt_Widget); -} -/** `[i64?]` */ -function $w_list_opt_i64(w, v) { - w.writeList(v, $w_opt_i64); -} -function $r_list_opt_i64(r) { - return r.readList($r_opt_i64); -} -/** `[string]` */ -function $w_list_string(w, v) { - w.writeList(v, $W.String); -} -function $r_list_string(r) { - return r.readList($R.String); -} -/** `{i64:string}` */ -function $w_map_i64_string(w, v) { - w.writeMap(v, $WK.I64, $W.String); -} -function $r_map_i64_string(r) { - return r.readMap($R.I64, $R.String); -} -/** `{string:Inner}` */ -function $w_map_string_Inner(w, v) { - w.writeMap(v, $WK.String, $w$edge_cases$Inner); -} -function $r_map_string_Inner(r) { - return r.readMap($R.String, $r$edge_cases$Inner); -} -/** `{string:Widget}` */ -function $w_map_string_Widget(w, v) { - w.writeMap(v, $WK.String, $w$edge_cases$Widget); -} -function $r_map_string_Widget(r) { - return r.readMap($R.String, $r$edge_cases$Widget); -} -/** `Inner?` */ -function $w_opt_Inner(w, v) { - w.writeOpt(v, $w$edge_cases$Inner); -} -function $r_opt_Inner(r) { - return r.readOpt($r$edge_cases$Inner); -} -/** `Mode?` */ -function $w_opt_Mode(w, v) { - w.writeOpt(v, $W.I32); -} -function $r_opt_Mode(r) { - return r.readOpt($R.I32); -} -/** `Outer?` */ -function $w_opt_Outer(w, v) { - w.writeOpt(v, $w$edge_cases$Outer); -} -function $r_opt_Outer(r) { - return r.readOpt($r$edge_cases$Outer); -} -/** `Widget?` */ -function $w_opt_Widget(w, v) { - w.writeOpt(v, $w$edge_cases$Widget); -} -function $r_opt_Widget(r) { - return r.readOpt($r$edge_cases$Widget); -} -/** `bytes?` */ -function $w_opt_bytes(w, v) { - w.writeOpt(v, $W.Bytes); -} -function $r_opt_bytes(r) { - return r.readOpt($R.Bytes); -} -/** `i32?` */ -function $w_opt_i32(w, v) { - w.writeOpt(v, $W.I32); -} -function $r_opt_i32(r) { - return r.readOpt($R.I32); -} -/** `i64?` */ -function $w_opt_i64(w, v) { - w.writeOpt(v, $W.I64); -} -function $r_opt_i64(r) { - return r.readOpt($R.I64); -} -/** `[Outer?]?` */ -function $w_opt_list_opt_Outer(w, v) { - w.writeOpt(v, $w_list_opt_Outer); -} -function $r_opt_list_opt_Outer(r) { - return r.readOpt($r_list_opt_Outer); -} -/** `[string]?` */ -function $w_opt_list_string(w, v) { - w.writeOpt(v, $w_list_string); -} -function $r_opt_list_string(r) { - return r.readOpt($r_list_string); -} -/** `string?` */ -function $w_opt_string(w, v) { - w.writeOpt(v, $W.String); -} -function $r_opt_string(r) { - return r.readOpt($R.String); -} - -function $adapt$edge_cases$BatchListener(impl) { - $impl(impl, 'BatchListener'); - return { - on_batch($a0, $a1, $a2, $a3) { - impl.onBatch($decode($a0, $r_list_Outer), $a1, $decode($a2, $r_opt_Outer), $a3); - }, - on_widget($a0, $a1, $a2) { - impl.onWidget($adopt(edge_cases$Widget, $a0), $adoptOpt(edge_cases$Widget, $a1), $decode($a2, $r_list_Widget)); - }, - on_tick() { - impl.onTick(); - }, - should_continue($a0) { - return $ret.Bool(impl.shouldContinue($a0), 'BatchListener.shouldContinue'); - }, - pick_mode() { - return $ret.I32(impl.pickMode(), 'BatchListener.pickMode'); - }, - }; -} - -function $adapt$edge_cases$Keyword(impl) { - $impl(impl, 'Keyword'); - return { - delete($a0) { - impl.delete($a0); - }, - import() { - return $ret.I32(impl.import(), 'Keyword.import'); - }, - }; -} - -const edge_cases$Widget = class Widget extends $Object { - constructor() { - super(); - throw new TypeError('Widget has no public constructor; use its factory functions'); - } - static $destroy(h) { - $raw.edge_cases_edge_cases_Widget_destroy(h); - } - static $clone(h) { - return $raw.edge_cases_edge_cases_Widget_clone(h); - } - refresh() { - const $self = $lend(this, edge_cases$Widget); - try { - $raw.edge_cases_edge_cases_Widget_refresh($self); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } finally { - $unlend(this); - } - } - rename(class_, import_) { - const $self = $lend(this, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_Widget_rename($self, class_, $encode(import_, $w_opt_string)); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - async fetch(id) { - const $self = $lend(this, edge_cases$Widget); - try { - return $decode(await $raw.edge_cases_edge_cases_Widget_fetch($self, id), $r$edge_cases$Outer); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } finally { - $unlend(this); - } - } - oldName() { - const $self = $lend(this, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_Widget_old_name($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - close_() { - const $self = $lend(this, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_Widget_close($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - dispose(force) { - const $self = $lend(this, edge_cases$Widget); - try { - $raw.edge_cases_edge_cases_Widget_dispose($self, force); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } -}; - -const edge_cases$Vault = class Vault extends $Object { - constructor() { - super(); - throw new TypeError('Vault has no public constructor; use its factory functions'); - } - static $destroy(h) { - $raw.edge_cases_edge_cases_Vault_destroy(h); - } - static $clone(h) { - return $raw.edge_cases_edge_cases_Vault_clone(h); - } - static open(path) { - try { - return $adopt(edge_cases$Vault, $raw.edge_cases_edge_cases_Vault_open(path)); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } - } - peek() { - const $self = $lend(this, edge_cases$Vault); - try { - return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_Vault_peek($self)); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - handle() { - const $self = $lend(this, edge_cases$Vault); - try { - return $raw.edge_cases_edge_cases_Vault_handle($self); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(this); - } - } - static probe() { - try { - return $raw.edge_cases_edge_cases_Vault_probe(); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } - } - static toString() { - try { - return $raw.edge_cases_edge_cases_Vault_to_string(); - } catch ($e) { - throw $fault($e); - } - } -}; - -const edge_cases$noop = function noop() { - try { - $raw.edge_cases_edge_cases_noop(); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$select = function select(class_, from, func, range, type) { - try { - return $raw.edge_cases_edge_cases_select(class_, from, $encode(func, $w_list_string), $encode(range, $w_opt_i32), type); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$nestedOptional = function nestedOptional(items) { - try { - return $decode($raw.edge_cases_edge_cases_nested_optional($encode(items, $w_opt_list_opt_Outer)), $r_opt_list_opt_Outer); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$optionalParams = function optionalParams(text, count, outer, tags, blob, mode) { - try { - return $decode($raw.edge_cases_edge_cases_optional_params($encode(text, $w_opt_string), $encode(count, $w_opt_i32), $encode(outer, $w_opt_Outer), $encode(tags, $w_opt_list_string), $encode(blob, $w_opt_bytes), $encode(mode, $w_opt_Mode)), $r_opt_Mode); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$registerBatch = function registerBatch(listener, keyword) { - try { - $raw.edge_cases_edge_cases_register_batch($adapt$edge_cases$BatchListener(listener), $adapt$edge_cases$Keyword(keyword)); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$widgetsEverywhere = function widgetsEverywhere(list, byName, sparse, holder) { - try { - return $decode($raw.edge_cases_edge_cases_widgets_everywhere($encode(list, $w_list_Widget), $encode(byName, $w_map_string_Widget), $encode(sparse, $w_list_opt_Widget), $encode(holder, $w$edge_cases$WidgetHolder)), $r$edge_cases$WidgetHolder); - } catch ($e) { - throw $fault($e); - } -}; - -const $it$edge_cases$widget_stream = { - next: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_destroy(h), - map: $fault, - convert: (v) => $adopt(edge_cases$Widget, v), -}; -const edge_cases$widgetStream = function widgetStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_widget_stream(), $it$edge_cases$widget_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncWidget = async function asyncWidget() { - try { - return $adopt(edge_cases$Widget, await $raw.edge_cases_edge_cases_async_widget()); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$makeWidget = function makeWidget(label) { - try { - return $adopt(edge_cases$Widget, $raw.edge_cases_edge_cases_make_widget(label)); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$findWidget = function findWidget(id) { - try { - return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_find_widget(id)); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$inspectWidget = function inspectWidget(widget, fallback) { - const $o0 = $lend(widget, edge_cases$Widget); - try { - const $o1 = $lendOpt(fallback, edge_cases$Widget); - try { - return $raw.edge_cases_edge_cases_inspect_widget($o0, $o1); - } catch ($e) { - throw $fault($e); - } finally { - $unlend(fallback); - } - } finally { - $unlend(widget); - } -}; - -const $it$edge_cases$string_stream = { - next: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_destroy(h), - map: $fault, - convert: null, -}; -const edge_cases$stringStream = function stringStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_string_stream(), $it$edge_cases$string_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const $it$edge_cases$scalar_stream = { - next: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_destroy(h), - map: $fault, - convert: null, -}; -const edge_cases$scalarStream = function scalarStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_scalar_stream(), $it$edge_cases$scalar_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const $it$edge_cases$mode_stream = { - next: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_next(h), - destroy: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_destroy(h), - map: $fault, - convert: null, -}; -const edge_cases$modeStream = function modeStream() { - try { - return new $Iterator($raw.edge_cases_edge_cases_mode_stream(), $it$edge_cases$mode_stream); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncVoid = async function asyncVoid() { - try { - await $raw.edge_cases_edge_cases_async_void(); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncBytes = async function asyncBytes(size) { - try { - return await $raw.edge_cases_edge_cases_async_bytes(size); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncStruct = async function asyncStruct() { - try { - return $decode(await $raw.edge_cases_edge_cases_async_struct(), $r$edge_cases$Outer); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$asyncList = async function asyncList($options) { - try { - return $decode(await $cancellable($options?.signal, $tokens, ($t) => $raw.edge_cases_edge_cases_async_list($t)), $r_list_Outer); - } catch ($e) { - throw $from$edge_cases$EdgeErrors($e); - } -}; - -const edge_cases$asyncOptional = async function asyncOptional() { - try { - return $decode(await $raw.edge_cases_edge_cases_async_optional(), $r_opt_i64); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncEnum = async function asyncEnum() { - try { - return await $raw.edge_cases_edge_cases_async_enum(); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$asyncKeywords = async function asyncKeywords(class_, import_) { - try { - return await $raw.edge_cases_edge_cases_async_keywords(class_, import_); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$keywordsRoundtrip = function keywordsRoundtrip(value) { - try { - return $decode($raw.edge_cases_edge_cases_keywords_roundtrip($encode(value, $w$edge_cases$Keywords)), $r$edge_cases$Keywords); - } catch ($e) { - throw $fault($e); - } -}; - -const edge_cases$legacyValue = function legacyValue(value) { - try { - return $decode($raw.edge_cases_edge_cases_legacy_value(value), $r$edge_cases$Retired); - } catch ($e) { - throw $fault($e); - } -}; - -export const edge_cases = Object.freeze({ - EdgeErrorsError: edge_cases$EdgeErrorsError, - BusyError: edge_cases$BusyError, - Legacy: edge_cases$Legacy, - Mode: edge_cases$Mode, - Widget: edge_cases$Widget, - Vault: edge_cases$Vault, - noop: edge_cases$noop, - select: edge_cases$select, - nestedOptional: edge_cases$nestedOptional, - optionalParams: edge_cases$optionalParams, - registerBatch: edge_cases$registerBatch, - widgetsEverywhere: edge_cases$widgetsEverywhere, - widgetStream: edge_cases$widgetStream, - asyncWidget: edge_cases$asyncWidget, - makeWidget: edge_cases$makeWidget, - findWidget: edge_cases$findWidget, - inspectWidget: edge_cases$inspectWidget, - stringStream: edge_cases$stringStream, - scalarStream: edge_cases$scalarStream, - modeStream: edge_cases$modeStream, - asyncVoid: edge_cases$asyncVoid, - asyncBytes: edge_cases$asyncBytes, - asyncStruct: edge_cases$asyncStruct, - asyncList: edge_cases$asyncList, - asyncOptional: edge_cases$asyncOptional, - asyncEnum: edge_cases$asyncEnum, - asyncKeywords: edge_cases$asyncKeywords, - keywordsRoundtrip: edge_cases$keywordsRoundtrip, - legacyValue: edge_cases$legacyValue, -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__README_md.snap deleted file mode 100644 index 84377620..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__README_md.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# kitchen_sink - -Native bindings for kitchen_sink - -WebAssembly bindings for a `wasm32-unknown-unknown` build of the `kitchen_sink` library, linked with `--export-table --growable-table` (for example `RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"`). `init()` loads `kitchen_sink.wasm` from next to this package, or from the path in `KITCHEN_SINK_LIBRARY` on Node.js; pass a URL, bytes, or a compiled module to load it from elsewhere: - -```js -import { init, shared, kitchen } from 'kitchen_sink'; - -await init(); -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap index f2659778..02990a78 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap @@ -9,9 +9,9 @@ source: crates/weaveffi-cli/tests/snapshots.rs /** * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). + * code: positive for a domain error, negative for a runtime failure (-1 + * an untyped failure, -2 producer panic, -3 marshalling failure, -4 a + * callback implementation failed, -5 cancelled). */ export declare class KitchenSinkError extends Error { readonly code: number; @@ -23,6 +23,18 @@ export declare class CancelledError extends KitchenSinkError { } /** The root error class, under a name no namespace member can shadow. */ declare const $Error: typeof KitchenSinkError; +/** + * A lazy iterator over a native `iter`: each `next()` makes one native + * call. The native iterator is released when it is exhausted or fails, by + * `return()` (which `for...of` calls on `break`), by `close()` or a `using` + * declaration, or when the iterator is garbage collected. + */ +export interface NativeIterator extends IterableIterator { + /** Release the native iterator now. Safe to call more than once. */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; +} /** * Load the WebAssembly module and check that it matches these bindings. * Every other export throws until the returned promise resolves. @@ -30,13 +42,6 @@ declare const $Error: typeof KitchenSinkError; export declare function init( source?: string | URL | BufferSource | WebAssembly.Module | Response, ): Promise; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; export declare namespace shared { /** A reusable token referenced across modules */ @@ -49,17 +54,32 @@ export declare namespace shared { } export declare namespace kitchen { - /** The errors of the `kitchen` error domain. */ - export class KitchenErrorsError extends $Error { + /** The errors of the `KitchenErrors` domain (an unknown code is this class itself). */ + export class KitchenError extends $Error { constructor(code: number, message?: string); } /** Item not found */ - export class NotFoundError extends kitchen.KitchenErrorsError { + export class NotFoundError extends kitchen.KitchenError { static readonly CODE: 1; constructor(message?: string); } /** Invalid input */ - export class InvalidInputError extends kitchen.KitchenErrorsError { + export class InvalidInputError extends kitchen.KitchenError { + static readonly CODE: 2; + constructor(message?: string); + } + /** The errors of the `PantryError` domain (an unknown code is this class itself). */ + export class PantryError extends $Error { + constructor(code: number, message?: string); + } + /** Out of stock */ + export class OutOfStockError extends kitchen.PantryError { + static readonly CODE: 1; + readonly item: string; + constructor(fields: { item: string }, message?: string); + } + /** Spoiled */ + export class SpoiledError extends kitchen.PantryError { static readonly CODE: 2; constructor(message?: string); } @@ -103,7 +123,7 @@ export declare namespace kitchen { onItem(item: kitchen.Item, gadget: kitchen.Gadget): boolean; /** * A display label, failing with a kitchen error - * @throws {kitchen.KitchenErrorsError} to report that error, with its fields, to the library + * @throws {kitchen.KitchenError} to report one of its codes, with its fields, to the library */ label(): string; /** The most recent item the listener kept, if any */ @@ -111,6 +131,21 @@ export declare namespace kitchen { /** The listener's favorite gadget */ favorite(): kitchen.Gadget; } + /** Consumer-implemented source of scalar samples */ + export interface Sampler { + /** The cutoff for a level, if the sampler has one */ + threshold(level: number | null): number | null; + /** + * Up to `limit` samples, failing when the pantry is empty + * @throws {kitchen.PantryError} to report one of its codes, with its fields, to the library + */ + samples(limit: number | null, weights: number[]): readonly number[] | Int16Array; + /** + * A free-form description, failing with an untyped error + * @throws to report a failure, with its message, to the library + */ + describe(): string; + } /** A pocket-sized interface exercising the object surface */ export class Gadget { /** Create a gadget with the given id */ @@ -119,9 +154,14 @@ export declare namespace kitchen { describe(): string; /** * Poke the gadget a number of times, failing on a negative count - * @throws {kitchen.KitchenErrorsError} + * @throws {kitchen.KitchenError} when the call fails with one of its codes */ poke(times: number): number; + /** + * The gadget's sensor readings, optionally limited + * @throws {KitchenSinkError} (code -1) when the call fails + */ + readings(limit: number | null): number[]; /** The gadget subsystem version string */ static version(): string; /** @@ -150,10 +190,10 @@ export declare namespace kitchen { /** A list of objects */ export function gadgets(): kitchen.Gadget[]; /** An iterator over objects */ - export function streamGadgets(): IterableIterator; + export function streamGadgets(): NativeIterator; /** * Optional struct return - * @throws {kitchen.KitchenErrorsError} + * @throws {kitchen.KitchenError} when the call fails with one of its codes */ export function maybeItem(id: bigint): kitchen.Item | null; /** List of structs */ @@ -161,7 +201,7 @@ export declare namespace kitchen { /** Map return type */ export function getAttrs(): Record; /** Iterator return type */ - export function streamItems(): IterableIterator; + export function streamItems(): NativeIterator; /** Returns the shared Token type from another module */ export function crossModuleToken(): shared.Token; /** Async operation */ @@ -170,11 +210,48 @@ export declare namespace kitchen { export function doCancellable(input: string, options?: { signal?: AbortSignal }): Promise; /** * Legacy operation kept for compatibility - * @deprecated Use new_op instead + * @deprecated Use `newOp` instead */ export function legacyOp(): number; - /** Replacement for legacy_op */ + /** Replacement for `legacyOp` */ export function newOp(): number; + /** Optional scalar in both directions */ + export function optI32Id(x: number | null): number | null; + export function optBoolId(x: boolean | null): boolean | null; + /** Optional C-style enum in both directions */ + export function optPriority(p: kitchen.Priority | null): kitchen.Priority | null; + /** Optional scalars next to ordinary parameters */ + export function optMixed(base: number, scale: number | null, offset: bigint | null, label: string | null): number | null; + /** A typed-array parameter */ + export function sumF64(xs: readonly number[] | Float64Array): number; + /** Typed arrays in both directions */ + export function scaleI32(xs: readonly number[] | Int32Array, factor: number): number[]; + export function idsU64(ids: readonly bigint[] | BigUint64Array): bigint[]; + /** Every slice element type */ + export function mixedSlices(a: readonly number[] | Int8Array, b: readonly number[] | Int16Array, c: readonly number[] | Uint16Array, d: readonly number[] | Uint32Array, e: readonly bigint[] | BigInt64Array, f: readonly number[] | Float32Array): number[]; + /** Async returning an optional scalar */ + export function asyncMaybeCount(): Promise; + /** Async returning a typed array */ + export function asyncSamples(n: number, options?: { signal?: AbortSignal }): Promise; + /** Iterator of optional scalars */ + export function streamMaybe(): NativeIterator; + /** Iterator of typed arrays */ + export function streamChunks(): NativeIterator; + /** + * Drive a consumer-implemented sampler + * @throws {kitchen.PantryError} when the call fails with one of its codes + */ + export function sampleWith(sampler: kitchen.Sampler, level: number | null): number[]; + /** + * Fails with the second domain + * @throws {kitchen.PantryError} when the call fails with one of its codes + */ + export function takeFromPantry(item: string): number; + /** + * Fails with an untyped error + * @throws {KitchenSinkError} (code -1) when the call fails + */ + export function parseAny(text: string): bigint; export namespace nested { /** Trivial nested-module function */ export function hello(): string; diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap index 2623bd36..8bfd2dc0 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap @@ -10,9 +10,13 @@ import { $fault, $domain, $raise, + $untyped, $W, $WK, $R, + $check, + $opt, + $slice, $encode, $decode, $Object, @@ -27,8 +31,8 @@ import { $Iterator, $cancellable, $impl, - $ret, $verify, + $setLive, KitchenSinkError as $Error, } from './runtime.js'; export { KitchenSinkError, CancelledError } from './runtime.js'; @@ -40,45 +44,75 @@ const $token = Number; let $loading = null; // The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). +// module's table function, as [id, hash, path]: the library's tables must +// hold each one (see $verify). const $contract = [ - ['shared', [ - [0x709e234caa962446n, 0x3894f2ca452e9f98n, 'shared.ping'], - [0xcfb306fa5801ee5bn, 0x1cbd9e10983cac2cn, 'shared.Token'], + ['kitchen_sink_shared_contract', [ + [0x709e234caa962446n, 0x3894f2ca452e9f98n, 'shared.ping'], // function ping() -> string + [0xcfb306fa5801ee5bn, 0x18e9fc204a13df6fn, 'shared.Token'], // record Token {i64, string} ]], - ['kitchen', [ - [0x01d4f09bbddc986fn, 0x29eb88fecc66544dn, 'kitchen.Gadget.new'], - [0x10472d8fe748c2een, 0xb265832b6a759fc4n, 'kitchen.maybe_gadget'], - [0x11368eec07a74078n, 0xfa049f78b77e146an, 'kitchen.gadgets'], - [0x137f08a29cde6021n, 0xda8de7aeb2dbb8a3n, 'kitchen.stream_gadgets'], - [0x2111b54980826dd7n, 0x452783f3e6ca6e1dn, 'kitchen.do_cancellable'], - [0x29dddbc60bf8d7dbn, 0x452a3e573d57f6dbn, 'kitchen.Priority'], - [0x2b227003b74a0d16n, 0x12332957470e9782n, 'kitchen.maybe_subscribe'], - [0x2dd66cb0064f0494n, 0x60197363fefd92ddn, 'kitchen.Item'], - [0x33c265bbf079b0e9n, 0xc3dcd0927e6c1debn, 'kitchen.u32_id'], - [0x33e6381d01660dfen, 0x077c63c31fd7f90an, 'kitchen.KitchenErrors'], - [0x3f5fc99d73504b76n, 0x2df88d710f1ea10en, 'kitchen.Gadget.poke'], - [0x4c5764cf109528d1n, 0x2a65529af4338a58n, 'kitchen.maybe_item'], - [0x4de17dfb93fd494dn, 0xba0ad0dd52ed74afn, 'kitchen.i32_id'], - [0x504e95b7f7e65570n, 0x01b3e8e0c9a10e23n, 'kitchen.stream_items'], - [0x68cdb9a4bd3e3dd5n, 0xc678b9164348d497n, 'kitchen.bool_id'], - [0x69e7cd703d16eb85n, 0xf3b28dd22ae9e689n, 'kitchen.do_async'], - [0x70865079ce41be82n, 0xbe3b69464672af46n, 'kitchen.Gadget.describe'], - [0x71c5a1116cb7c49an, 0xcba1837108a89b84n, 'kitchen.echo_bytes'], - [0x735ee1e0049ee91an, 0xca74320bc53eddabn, 'kitchen.echo_string'], - [0x76d7fc58f4e41d84n, 0x7373a0e47d30e690n, 'kitchen.i64_id'], - [0x79abfe1aa960fd6an, 0xbca7a9e5eb26f3a6n, 'kitchen.nested.hello'], - [0x7d3dd7d5cf6749e3n, 0xa192f5a7298adb39n, 'kitchen.new_op'], - [0x89a5e664bc25c67bn, 0x5764a95e81ecfd31n, 'kitchen.f64_id'], - [0x8cfdbdcecac5f7f1n, 0x07e272f09c41fffcn, 'kitchen.Gadget.version'], - [0x974b9f3e823c1738n, 0xb81b91e975695f27n, 'kitchen.cross_module_token'], - [0xaca0655fac15d230n, 0x11c4d2b28abe07bbn, 'kitchen.list_items'], - [0xc883b4afae20543en, 0xc40560e5411ff392n, 'kitchen.legacy_op'], - [0xf2f7753e6e1dffc2n, 0xc9047a95bdf49e93n, 'kitchen.get_attrs'], - [0xfae25d3391922eadn, 0x1c7f95739f3210b8n, 'kitchen.subscribe'], - [0xfd0059b05cb2fcfbn, 0x248bf8404a5823b4n, 'kitchen.Gadget'], - [0xfed9c551fb9fd2d6n, 0x59aec980ef3f3291n, 'kitchen.ReadyListener'], + ['kitchen_sink_kitchen_contract', [ + [0x01d4f09bbddc986fn, 0xffcf2bd3b04aa002n, 'kitchen.Gadget.new'], // constructor new(i64) -> Gadget + [0x02bbd5f31c953704n, 0x2cfa679ea1373e09n, 'kitchen.Sampler.samples'], // callback_method #1 samples(u32?, [f64]) -> [i16] throws PantryError + [0x10472d8fe748c2een, 0xcc570a2649bbf7dcn, 'kitchen.maybe_gadget'], // function maybe_gadget(Gadget?) -> Gadget? + [0x11368eec07a74078n, 0xfa049f78b77e146an, 'kitchen.gadgets'], // function gadgets() -> [Gadget] + [0x137f08a29cde6021n, 0xda8de7aeb2dbb8a3n, 'kitchen.stream_gadgets'], // function stream_gadgets() -> iter + [0x184bfc4ab0e3fe45n, 0xb64587477022696bn, 'kitchen.ids_u64'], // function ids_u64([u64]) -> [u64] + [0x2111b54980826dd7n, 0x627bf01ab365b209n, 'kitchen.do_cancellable'], // function do_cancellable(string) -> string async cancellable + [0x21c70f9371374d74n, 0x25d5d88a30fe0a79n, 'kitchen.ReadyListener.favorite'], // callback_method #4 favorite() -> Gadget + [0x23de5bd597372f46n, 0xcf1bc6bb37872798n, 'kitchen.stream_chunks'], // function stream_chunks() -> iter<[i32]> + [0x249f1ddaec6a6cben, 0x0710d6c9a4de93dan, 'kitchen.take_from_pantry'], // function take_from_pantry(string) -> i32 throws PantryError + [0x29dddbc60bf8d7dbn, 0x452a3e573d57f6dbn, 'kitchen.Priority'], // enum Priority {Low = 0, Normal = 1, High = 2} + [0x2b227003b74a0d16n, 0x5dae036b8a6aed30n, 'kitchen.maybe_subscribe'], // function maybe_subscribe(ReadyListener?) -> bool + [0x2dd66cb0064f0494n, 0x74b131724dd0d867n, 'kitchen.Item'], // record Item {i64, string, i32, bool, f64, u32, bytes, [string], {string:string}, i64?, Token, Priority, Gadget?} + [0x33c265bbf079b0e9n, 0x97c32b764693f2cbn, 'kitchen.u32_id'], // function u32_id(u32) -> u32 + [0x33e6381d01660dfen, 0x05e35a563089bf7dn, 'kitchen.KitchenErrors'], // errors KitchenErrors + [0x3d969ae76db6cb23n, 0x65a325cfc772296bn, 'kitchen.ReadyListener.latest'], // callback_method #3 latest() -> Item? + [0x3f5fc99d73504b76n, 0x417e4a2acd3210b3n, 'kitchen.Gadget.poke'], // method poke(i32) -> i32 throws KitchenErrors + [0x4082f0085db28781n, 0x18848e701962912en, 'kitchen.Sampler'], // callback Sampler + [0x4340a6b1e7ec8c50n, 0xa59d0f32a66dc7a2n, 'kitchen.Sampler.describe'], // callback_method #2 describe() -> string throws any + [0x4c5764cf109528d1n, 0xb374accf14676d90n, 'kitchen.maybe_item'], // function maybe_item(i64) -> Item? throws KitchenErrors + [0x4de17dfb93fd494dn, 0x78be3203bcd10727n, 'kitchen.i32_id'], // function i32_id(i32) -> i32 + [0x504e95b7f7e65570n, 0x01b3e8e0c9a10e23n, 'kitchen.stream_items'], // function stream_items() -> iter + [0x5dafabb7775fcb2dn, 0x6c4b78c7d3f858c1n, 'kitchen.PantryError.Spoiled'], // code Spoiled = 2 + [0x5e0a8f6be4b00600n, 0x18ea1a42036c96a0n, 'kitchen.Sampler.threshold'], // callback_method #0 threshold(u32?) -> f64? + [0x5ec0af1d4ecc6fedn, 0x6540bdb74246420cn, 'kitchen.async_samples'], // function async_samples(u32) -> [f32] async cancellable + [0x61a358b0571e0a4en, 0x11a24c4b50d11996n, 'kitchen.async_maybe_count'], // function async_maybe_count() -> i64? async + [0x68cdb9a4bd3e3dd5n, 0xcde022fe6bc3b1bfn, 'kitchen.bool_id'], // function bool_id(bool) -> bool + [0x69e7cd703d16eb85n, 0xb13cb8d39e702c79n, 'kitchen.do_async'], // function do_async(string) -> string async + [0x70865079ce41be82n, 0xbe3b69464672af46n, 'kitchen.Gadget.describe'], // method describe() -> string + [0x71c5a1116cb7c49an, 0x1506cfe6f548fac4n, 'kitchen.echo_bytes'], // function echo_bytes(bytes) -> bytes + [0x735ee1e0049ee91an, 0xa713dbd1be707752n, 'kitchen.echo_string'], // function echo_string(string) -> string + [0x76d7fc58f4e41d84n, 0x6bdb4ccea2841d5en, 'kitchen.i64_id'], // function i64_id(i64) -> i64 + [0x78d91fd617e162b8n, 0xd88e2753503b36dfn, 'kitchen.stream_maybe'], // function stream_maybe() -> iter + [0x79abfe1aa960fd6an, 0xbca7a9e5eb26f3a6n, 'kitchen.nested.hello'], // function hello() -> string + [0x7b3865b536da998cn, 0x1c4e79d60a20e74fn, 'kitchen.mixed_slices'], // function mixed_slices([i8], [i16], [u16], [u32], [i64], [f32]) -> [u32] + [0x7ba266dc26a58989n, 0x0f86001cac0c21e8n, 'kitchen.KitchenErrors.InvalidInput'], // code InvalidInput = 2 + [0x7d3dd7d5cf6749e3n, 0xa192f5a7298adb39n, 'kitchen.new_op'], // function new_op() -> i32 + [0x81ded69adee0abf2n, 0x8c71141a931906f6n, 'kitchen.scale_i32'], // function scale_i32([i32], i32) -> [i32] + [0x81fa72769330fed6n, 0x89681e305b55a4f4n, 'kitchen.ReadyListener.label'], // callback_method #2 label() -> string throws KitchenErrors + [0x89a5e664bc25c67bn, 0x357fd9b65ad879a9n, 'kitchen.f64_id'], // function f64_id(f64) -> f64 + [0x8cfdbdcecac5f7f1n, 0x07e272f09c41fffcn, 'kitchen.Gadget.version'], // static version() -> string + [0x8dc42397e47614b9n, 0xcddf6fede09b2e7fn, 'kitchen.opt_priority'], // function opt_priority(Priority?) -> Priority? + [0x92f060ef68961fc1n, 0x4af6ce5b4f9fa508n, 'kitchen.ReadyListener.on_item'], // callback_method #1 on_item(Item, Gadget) -> bool + [0x974b9f3e823c1738n, 0xb81b91e975695f27n, 'kitchen.cross_module_token'], // function cross_module_token() -> Token + [0x9f0f001aa85f97d5n, 0x55815247f895bd45n, 'kitchen.sum_f64'], // function sum_f64([f64]) -> f64 + [0xab2a29fd1ef5458bn, 0x471a34aa4c09ae2bn, 'kitchen.KitchenErrors.NotFound'], // code NotFound = 1 + [0xaca0655fac15d230n, 0x11c4d2b28abe07bbn, 'kitchen.list_items'], // function list_items() -> [Item] + [0xb57c8e6ae3fb4383n, 0x3f857851ab75366cn, 'kitchen.PantryError'], // errors PantryError + [0xbdc795b195edd428n, 0x36127462f8b0969fn, 'kitchen.opt_mixed'], // function opt_mixed(f64, f32?, i64?, string?) -> u16? + [0xbe4821ab3c340e71n, 0xe1d32e38b766e623n, 'kitchen.ReadyListener.on_ready'], // callback_method #0 on_ready(i32, string) -> void + [0xc2ad53f2a6a36323n, 0x071e29abee823f17n, 'kitchen.opt_i32_id'], // function opt_i32_id(i32?) -> i32? + [0xc7ab409554fd3d7bn, 0xfea12b6bc219fc46n, 'kitchen.parse_any'], // function parse_any(string) -> i64 throws any + [0xc82a7f2d85f99f4en, 0xa9bcb8bacedb036cn, 'kitchen.sample_with'], // function sample_with(Sampler, u32?) -> [i16] throws PantryError + [0xc883b4afae20543en, 0xc40560e5411ff392n, 'kitchen.legacy_op'], // function legacy_op() -> i32 + [0xd180bf646bff59e2n, 0xa4515a6d00d7c600n, 'kitchen.PantryError.OutOfStock'], // code OutOfStock = 1 {string} + [0xe6778df0f5c1b82bn, 0x38eaafdb350559e1n, 'kitchen.opt_bool_id'], // function opt_bool_id(bool?) -> bool? + [0xf2f7753e6e1dffc2n, 0xc9047a95bdf49e93n, 'kitchen.get_attrs'], // function get_attrs() -> {string:i32} + [0xfae25d3391922eadn, 0xcc034cf506cae182n, 'kitchen.subscribe'], // function subscribe(ReadyListener) -> i32 + [0xfd0059b05cb2fcfbn, 0x248bf8404a5823b4n, 'kitchen.Gadget'], // interface Gadget + [0xfed9c551fb9fd2d6n, 0xe1020573f084dc1dn, 'kitchen.ReadyListener'], // callback ReadyListener + [0xff3a99b52597dee2n, 0x14c4ecac97bdfc9bn, 'kitchen.Gadget.readings'], // method readings(u32?) -> [f64] throws any ]], ]; @@ -89,11 +123,11 @@ const $contract = [ * resolves. */ export function init(source) { - $loading ??= $loadWasm(source, 'kitchen_sink', 'KITCHEN_SINK_LIBRARY', new URL('./kitchen_sink.wasm', import.meta.url)) + $loading ??= $loadWasm('kitchen_sink', source, 'kitchen_sink', 'KITCHEN_SINK_LIBRARY', new URL('./kitchen_sink.wasm', import.meta.url)) .then((m) => { $raw = $bind(m); try { - $verify($raw, 'kitchen_sink', 'kitchen_sink', 4, $contract); + $verify($raw, 'kitchen_sink', 'kitchen_sink', 5, $contract); } catch (e) { $raw = $unloaded('kitchen_sink'); throw e; @@ -108,55 +142,86 @@ export function init(source) { function $vt_kitchen_sink_kitchen_ReadyListener(m) { return [ - [['i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiii', (ctx, p0, p1, p2, err) => { + [['i32', 'i32', 'i32', 'i32', 'i32'], [], (p_ctx, p_code, p_msg_ptr, p_msg_len, p_out_err) => { try { - m.adapter(ctx).on_ready(p0, m.readStr(p1, p2)); + m.adapter(p_ctx).on_ready(p_code, m.readStr(p_msg_ptr, p_msg_len)); } catch (e) { - m.foreign(err, e); + m.foreign(p_out_err, e); } }], - [['i32', 'i32', 'i32', 'i32', 'i32'], ['i32'], 'iiiiii', (ctx, p0, p1, p2, err) => { + [['i32', 'i32', 'i32', 'i32', 'i32'], ['i32'], (p_ctx, p_item_ptr, p_item_len, p_gadget, p_out_err) => { try { - return m.adapter(ctx).on_item(m.readData(p0, p1), p2) ? 1 : 0; + return m.adapter(p_ctx).on_item(m.readData(p_item_ptr, p_item_len), p_gadget) ? 1 : 0; } catch (e) { - m.foreign(err, e); + m.foreign(p_out_err, e); return 0; } }], - [['i32', 'i32', 'i32', 'i32'], [], 'viiii', (ctx, out, outLen, err) => { + [['i32', 'i32', 'i32', 'i32'], [], (p_ctx, p_out_ptr, p_out_len, p_out_err) => { try { - m.giveStr(out, outLen, m.adapter(ctx).label()); + m.giveStr(p_out_ptr, p_out_len, m.adapter(p_ctx).label()); } catch (e) { - m.foreign(err, e); + m.foreign(p_out_err, e); } }], - [['i32', 'i32', 'i32', 'i32'], [], 'viiii', (ctx, out, outLen, err) => { + [['i32', 'i32', 'i32', 'i32'], [], (p_ctx, p_out_ptr, p_out_len, p_out_err) => { try { - m.give(out, outLen, m.adapter(ctx).latest()); + m.give(p_out_ptr, p_out_len, m.adapter(p_ctx).latest()); } catch (e) { - m.foreign(err, e); + m.foreign(p_out_err, e); } }], - [['i32', 'i32'], ['i32'], 'iii', (ctx, err) => { + [['i32', 'i32'], ['i32'], (p_ctx, p_out_err) => { try { - return m.handleOpt(m.adapter(ctx).favorite()); + return m.handleOpt(m.adapter(p_ctx).favorite()); } catch (e) { - m.foreign(err, e); + m.foreign(p_out_err, e); return 0; } }], ]; } +function $vt_kitchen_sink_kitchen_Sampler(m) { + return [ + [['i32', 'i32', 'i32', 'i32', 'i32'], ['i32'], (p_ctx, p_has_level, p_level, p_out_value, p_out_err) => { + try { + const v = m.adapter(p_ctx).threshold(p_has_level !== 0 ? p_level >>> 0 : null); + if (v === null) return 0; + m.view().setFloat64(p_out_value >>> 0, v, true); + return 1; + } catch (e) { + m.foreign(p_out_err, e); + return 0; + } + }], + [['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], (p_ctx, p_has_limit, p_limit, p_weights_ptr, p_weights_len, p_out_ptr, p_out_len, p_out_err) => { + try { + m.giveSlice(p_out_ptr, p_out_len, m.adapter(p_ctx).samples(p_has_limit !== 0 ? p_limit >>> 0 : null, m.readSlice(p_weights_ptr, p_weights_len, Float64Array)), Int16Array); + } catch (e) { + m.foreign(p_out_err, e); + } + }], + [['i32', 'i32', 'i32', 'i32'], [], (p_ctx, p_out_ptr, p_out_len, p_out_err) => { + try { + m.giveStr(p_out_ptr, p_out_len, m.adapter(p_ctx).describe()); + } catch (e) { + m.foreign(p_out_err, e); + } + }], + ]; +} + // The raw entry points, one per C symbol. function $bind(m) { const x = m.x; + const q = m.q; return { kitchen_sink_abi_version: () => x.kitchen_sink_abi_version() >>> 0, - kitchen_sink_debug_live: (kind) => m.live(x.kitchen_sink_debug_live(m.num(kind)), kind), + kitchen_sink_debug_live: (kind) => m.live(x.kitchen_sink_debug_live(m.i32(kind)), kind), kitchen_sink_cancel_token_create: () => x.kitchen_sink_cancel_token_create(), - kitchen_sink_cancel_token_cancel: (t) => x.kitchen_sink_cancel_token_cancel(m.handle(t)), - kitchen_sink_cancel_token_destroy: (t) => x.kitchen_sink_cancel_token_destroy(m.handle(t)), + kitchen_sink_cancel_token_cancel: (t) => q.kitchen_sink_cancel_token_cancel(m.handle(t)), + kitchen_sink_cancel_token_destroy: (t) => q.kitchen_sink_cancel_token_destroy(m.handle(t)), kitchen_sink_shared_contract: () => m.contract(x.kitchen_sink_shared_contract), kitchen_sink_kitchen_contract: () => m.contract(x.kitchen_sink_kitchen_contract), kitchen_sink_shared_ping: () => { @@ -165,29 +230,34 @@ function $bind(m) { return m.takeStr(r, m.outLen()); }, kitchen_sink_kitchen_Gadget_clone: (h) => x.kitchen_sink_kitchen_Gadget_clone(m.handle(h)), - kitchen_sink_kitchen_Gadget_destroy: (h) => x.kitchen_sink_kitchen_Gadget_destroy(m.handle(h)), + kitchen_sink_kitchen_Gadget_destroy: (h) => q.kitchen_sink_kitchen_Gadget_destroy(m.handle(h)), kitchen_sink_kitchen_bool_id: (a0) => { - const r = x.kitchen_sink_kitchen_bool_id(m.bool(a0), m.err); + const c0 = m.bool(a0); + const r = x.kitchen_sink_kitchen_bool_id(c0, m.err); m.check(); return r !== 0; }, kitchen_sink_kitchen_i32_id: (a0) => { - const r = x.kitchen_sink_kitchen_i32_id(m.num(a0), m.err); + const c0 = m.i32(a0); + const r = x.kitchen_sink_kitchen_i32_id(c0, m.err); m.check(); return r; }, kitchen_sink_kitchen_u32_id: (a0) => { - const r = x.kitchen_sink_kitchen_u32_id(m.num(a0), m.err); + const c0 = m.u32(a0); + const r = x.kitchen_sink_kitchen_u32_id(c0, m.err); m.check(); return r >>> 0; }, kitchen_sink_kitchen_i64_id: (a0) => { - const r = x.kitchen_sink_kitchen_i64_id(m.i64(a0), m.err); + const c0 = m.i64(a0); + const r = x.kitchen_sink_kitchen_i64_id(c0, m.err); m.check(); return r; }, kitchen_sink_kitchen_f64_id: (a0) => { - const r = x.kitchen_sink_kitchen_f64_id(m.num(a0), m.err); + const c0 = m.num(a0); + const r = x.kitchen_sink_kitchen_f64_id(c0, m.err); m.check(); return r; }, @@ -224,7 +294,8 @@ function $bind(m) { return r !== 0; }, kitchen_sink_kitchen_maybe_gadget: (a0) => { - const r = x.kitchen_sink_kitchen_maybe_gadget(m.handleOpt(a0), m.err); + const c0 = m.handleOpt(a0); + const r = x.kitchen_sink_kitchen_maybe_gadget(c0, m.err); m.check(); return r === 0 ? null : r; }, @@ -242,12 +313,12 @@ function $bind(m) { const has = x.kitchen_sink_kitchen_StreamGadgetsIterator_next(m.handle(h), m.item, m.err); m.check(); if (has === 0) return undefined; - const p = m.view().getUint32(m.item, true); - return p; + return m.ptrAt(m.item); }, - kitchen_sink_kitchen_StreamGadgetsIterator_destroy: (h) => x.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(m.handle(h)), + kitchen_sink_kitchen_StreamGadgetsIterator_destroy: (h) => q.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(m.handle(h)), kitchen_sink_kitchen_maybe_item: (a0) => { - const r = x.kitchen_sink_kitchen_maybe_item(m.i64(a0), m.len, m.err); + const c0 = m.i64(a0); + const r = x.kitchen_sink_kitchen_maybe_item(c0, m.len, m.err); m.check(); return m.takeData(r, m.outLen()); }, @@ -270,10 +341,9 @@ function $bind(m) { const has = x.kitchen_sink_kitchen_StreamItemsIterator_next(m.handle(h), m.item, m.len, m.err); m.check(); if (has === 0) return undefined; - const p = m.view().getUint32(m.item, true); - return m.takeData(p, m.outLen()); + return m.takeData(m.ptrAt(m.item), m.outLen()); }, - kitchen_sink_kitchen_StreamItemsIterator_destroy: (h) => x.kitchen_sink_kitchen_StreamItemsIterator_destroy(m.handle(h)), + kitchen_sink_kitchen_StreamItemsIterator_destroy: (h) => q.kitchen_sink_kitchen_StreamItemsIterator_destroy(m.handle(h)), kitchen_sink_kitchen_cross_module_token: () => { const r = x.kitchen_sink_kitchen_cross_module_token(m.len, m.err); m.check(); @@ -283,7 +353,7 @@ function $bind(m) { let s0 = null; try { s0 = m.str(a0); - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.kitchen_sink_kitchen_do_async(s0[0], s0[1], cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); + return m.launch(['i32', 'i32', 'i32', 'i32'], [], (cb, ctx) => x.kitchen_sink_kitchen_do_async(s0[0], s0[1], cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); } finally { m.unstage(s0); } @@ -292,7 +362,7 @@ function $bind(m) { let s0 = null; try { s0 = m.str(a0); - return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.kitchen_sink_kitchen_do_cancellable(s0[0], s0[1], m.handleOpt(token), cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); + return m.launch(['i32', 'i32', 'i32', 'i32'], [], (cb, ctx) => x.kitchen_sink_kitchen_do_cancellable(s0[0], s0[1], m.handleOpt(token), cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); } finally { m.unstage(s0); } @@ -307,8 +377,161 @@ function $bind(m) { m.check(); return r; }, + kitchen_sink_kitchen_opt_i32_id: (a0) => { + const h0 = m.some(a0); + const c0 = h0 ? m.i32(a0) : 0; + const r = x.kitchen_sink_kitchen_opt_i32_id(h0 ? 1 : 0, c0, m.out, m.err); + m.check(); + return r !== 0 ? m.view().getInt32(m.out, true) : null; + }, + kitchen_sink_kitchen_opt_bool_id: (a0) => { + const h0 = m.some(a0); + const c0 = h0 ? m.bool(a0) : 0; + const r = x.kitchen_sink_kitchen_opt_bool_id(h0 ? 1 : 0, c0, m.out, m.err); + m.check(); + return r !== 0 ? m.view().getUint8(m.out) !== 0 : null; + }, + kitchen_sink_kitchen_opt_priority: (a0) => { + const h0 = m.some(a0); + const c0 = h0 ? m.i32(a0) : 0; + const r = x.kitchen_sink_kitchen_opt_priority(h0 ? 1 : 0, c0, m.out, m.err); + m.check(); + return r !== 0 ? m.view().getInt32(m.out, true) : null; + }, + kitchen_sink_kitchen_opt_mixed: (a0, a1, a2, a3) => { + const c0 = m.num(a0); + const h1 = m.some(a1); + const c1 = h1 ? m.num(a1) : 0; + const h2 = m.some(a2); + const c2 = h2 ? m.i64(a2) : 0n; + let s3 = null; + try { + s3 = m.data(a3); + const r = x.kitchen_sink_kitchen_opt_mixed(c0, h1 ? 1 : 0, c1, h2 ? 1 : 0, c2, s3[0], s3[1], m.out, m.err); + m.check(); + return r !== 0 ? m.view().getUint16(m.out, true) : null; + } finally { + m.unstage(s3); + } + }, + kitchen_sink_kitchen_sum_f64: (a0) => { + let s0 = null; + try { + s0 = m.slice(a0, Float64Array); + const r = x.kitchen_sink_kitchen_sum_f64(s0[0], s0[1], m.err); + m.check(); + return r; + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_scale_i32: (a0, a1) => { + const c1 = m.i32(a1); + let s0 = null; + try { + s0 = m.slice(a0, Int32Array); + const r = x.kitchen_sink_kitchen_scale_i32(s0[0], s0[1], c1, m.len, m.err); + m.check(); + return m.takeSlice(r, m.outLen(), Int32Array); + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_ids_u64: (a0) => { + let s0 = null; + try { + s0 = m.slice(a0, BigUint64Array); + const r = x.kitchen_sink_kitchen_ids_u64(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeSlice(r, m.outLen(), BigUint64Array); + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_mixed_slices: (a0, a1, a2, a3, a4, a5) => { + let s0 = null, s1 = null, s2 = null, s3 = null, s4 = null, s5 = null; + try { + s0 = m.slice(a0, Int8Array); + s1 = m.slice(a1, Int16Array); + s2 = m.slice(a2, Uint16Array); + s3 = m.slice(a3, Uint32Array); + s4 = m.slice(a4, BigInt64Array); + s5 = m.slice(a5, Float32Array); + const r = x.kitchen_sink_kitchen_mixed_slices(s0[0], s0[1], s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], s4[0], s4[1], s5[0], s5[1], m.len, m.err); + m.check(); + return m.takeSlice(r, m.outLen(), Uint32Array); + } finally { + m.unstage(s0); + m.unstage(s1); + m.unstage(s2); + m.unstage(s3); + m.unstage(s4); + m.unstage(s5); + } + }, + kitchen_sink_kitchen_async_maybe_count: () => { + return m.launch(['i32', 'i32', 'i32', 'i64'], [], (cb, ctx) => x.kitchen_sink_kitchen_async_maybe_count(cb, ctx), (has_result, result) => has_result !== 0 ? result : null); + }, + kitchen_sink_kitchen_async_samples: (a0, token) => { + const c0 = m.u32(a0); + return m.launch(['i32', 'i32', 'i32', 'i32'], [], (cb, ctx) => x.kitchen_sink_kitchen_async_samples(c0, m.handleOpt(token), cb, ctx), (result_ptr, result_len) => m.takeSlice(result_ptr, result_len, Float32Array)); + }, + kitchen_sink_kitchen_stream_maybe: () => { + const r = x.kitchen_sink_kitchen_stream_maybe(m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_StreamMaybeIterator_next: (h) => { + const has = x.kitchen_sink_kitchen_StreamMaybeIterator_next(m.handle(h), m.has, m.item, m.err); + m.check(); + if (has === 0) return undefined; + return m.view().getUint8(m.has) !== 0 ? m.view().getInt32(m.item, true) : null; + }, + kitchen_sink_kitchen_StreamMaybeIterator_destroy: (h) => q.kitchen_sink_kitchen_StreamMaybeIterator_destroy(m.handle(h)), + kitchen_sink_kitchen_stream_chunks: () => { + const r = x.kitchen_sink_kitchen_stream_chunks(m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_StreamChunksIterator_next: (h) => { + const has = x.kitchen_sink_kitchen_StreamChunksIterator_next(m.handle(h), m.item, m.len, m.err); + m.check(); + if (has === 0) return undefined; + return m.takeSlice(m.ptrAt(m.item), m.outLen(), Int32Array); + }, + kitchen_sink_kitchen_StreamChunksIterator_destroy: (h) => q.kitchen_sink_kitchen_StreamChunksIterator_destroy(m.handle(h)), + kitchen_sink_kitchen_sample_with: (a0, a1) => { + const h1 = m.some(a1); + const c1 = h1 ? m.u32(a1) : 0; + const r = x.kitchen_sink_kitchen_sample_with(m.register(a0), m.vtable('kitchen_sink_kitchen_Sampler', () => $vt_kitchen_sink_kitchen_Sampler(m)), h1 ? 1 : 0, c1, m.len, m.err); + m.check(); + return m.takeSlice(r, m.outLen(), Int16Array); + }, + kitchen_sink_kitchen_take_from_pantry: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + const r = x.kitchen_sink_kitchen_take_from_pantry(s0[0], s0[1], m.err); + m.check(); + return r; + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_parse_any: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + const r = x.kitchen_sink_kitchen_parse_any(s0[0], s0[1], m.err); + m.check(); + return r; + } finally { + m.unstage(s0); + } + }, kitchen_sink_kitchen_Gadget_new: (a0) => { - const r = x.kitchen_sink_kitchen_Gadget_new(m.i64(a0), m.err); + const c0 = m.i64(a0); + const r = x.kitchen_sink_kitchen_Gadget_new(c0, m.err); m.check(); return r; }, @@ -318,10 +541,18 @@ function $bind(m) { return m.takeStr(r, m.outLen()); }, kitchen_sink_kitchen_Gadget_poke: (self, a0) => { - const r = x.kitchen_sink_kitchen_Gadget_poke(m.handle(self), m.num(a0), m.err); + const c0 = m.i32(a0); + const r = x.kitchen_sink_kitchen_Gadget_poke(m.handle(self), c0, m.err); m.check(); return r; }, + kitchen_sink_kitchen_Gadget_readings: (self, a0) => { + const h0 = m.some(a0); + const c0 = h0 ? m.u32(a0) : 0; + const r = x.kitchen_sink_kitchen_Gadget_readings(m.handle(self), h0 ? 1 : 0, c0, m.len, m.err); + m.check(); + return m.takeSlice(r, m.outLen(), Float64Array); + }, kitchen_sink_kitchen_Gadget_version: () => { const r = x.kitchen_sink_kitchen_Gadget_version(m.len, m.err); m.check(); @@ -342,24 +573,17 @@ const $tokens = { destroy: (t) => $raw.kitchen_sink_cancel_token_destroy(t), }; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.kitchen_sink_debug_live(kind); -} +// The leak counters behind the package's `./debug` export. +$setLive((kind) => $raw.kitchen_sink_debug_live(kind)); -const kitchen$KitchenErrorsError = class KitchenErrorsError extends $Error {}; -const kitchen$NotFoundError = class NotFoundError extends kitchen$KitchenErrorsError { +const kitchen$KitchenError = class KitchenError extends $Error {}; +const kitchen$NotFoundError = class NotFoundError extends kitchen$KitchenError { static CODE = 1; constructor(message = 'Item not found') { super(1, message); } }; -const kitchen$InvalidInputError = class InvalidInputError extends kitchen$KitchenErrorsError { +const kitchen$InvalidInputError = class InvalidInputError extends kitchen$KitchenError { static CODE = 2; constructor(message = 'Invalid input') { super(2, message); @@ -371,11 +595,42 @@ const $codes$kitchen$KitchenErrors = new Map([ ]); const $payloads$kitchen$KitchenErrors = new Map(); function $from$kitchen$KitchenErrors(e) { - return $domain(e, $codes$kitchen$KitchenErrors, $payloads$kitchen$KitchenErrors); + return $domain(e, kitchen$KitchenError, $codes$kitchen$KitchenErrors, $payloads$kitchen$KitchenErrors); } const $fields$kitchen$KitchenErrors = new Map(); function $raise$kitchen$KitchenErrors(e) { - return $raise(e, kitchen$KitchenErrorsError, $codes$kitchen$KitchenErrors, $fields$kitchen$KitchenErrors); + return $raise(e, kitchen$KitchenError, $codes$kitchen$KitchenErrors, $fields$kitchen$KitchenErrors); +} + +const kitchen$PantryError = class PantryError extends $Error {}; +const kitchen$OutOfStockError = class OutOfStockError extends kitchen$PantryError { + static CODE = 1; + constructor(fields, message = 'Out of stock') { + super(1, message); + this.item = fields.item; + } +}; +const kitchen$SpoiledError = class SpoiledError extends kitchen$PantryError { + static CODE = 2; + constructor(message = 'Spoiled') { + super(2, message); + } +}; +const $codes$kitchen$PantryError = new Map([ + [1, kitchen$OutOfStockError], + [2, kitchen$SpoiledError], +]); +const $payloads$kitchen$PantryError = new Map([ + [1, (r) => ({ item: r.readString() })], +]); +function $from$kitchen$PantryError(e) { + return $domain(e, kitchen$PantryError, $codes$kitchen$PantryError, $payloads$kitchen$PantryError); +} +const $fields$kitchen$PantryError = new Map([ + [1, (w, e) => { w.writeString(e.item); }], +]); +function $raise$kitchen$PantryError(e) { + return $raise(e, kitchen$PantryError, $codes$kitchen$PantryError, $fields$kitchen$PantryError); } const kitchen$Priority = Object.freeze({ @@ -435,20 +690,6 @@ function $w$kitchen$Gadget(w, v) { function $r$kitchen$Gadget(r) { return $adopt(kitchen$Gadget, $token(r.readU64())); } -/** `[Gadget]` */ -function $w_list_Gadget(w, v) { - w.writeList(v, $w$kitchen$Gadget); -} -function $r_list_Gadget(r) { - return r.readList($r$kitchen$Gadget); -} -/** `[Item]` */ -function $w_list_Item(w, v) { - w.writeList(v, $w$kitchen$Item); -} -function $r_list_Item(r) { - return r.readList($r$kitchen$Item); -} /** `[string]` */ function $w_list_string(w, v) { w.writeList(v, $W.String); @@ -456,13 +697,6 @@ function $w_list_string(w, v) { function $r_list_string(r) { return r.readList($R.String); } -/** `{string:i32}` */ -function $w_map_string_i32(w, v) { - w.writeMap(v, $WK.String, $W.I32); -} -function $r_map_string_i32(r) { - return r.readMap($R.String, $R.I32); -} /** `{string:string}` */ function $w_map_string_string(w, v) { w.writeMap(v, $WK.String, $W.String); @@ -470,6 +704,13 @@ function $w_map_string_string(w, v) { function $r_map_string_string(r) { return r.readMap($R.String, $R.String); } +/** `i64?` */ +function $w_opt_i64(w, v) { + w.writeOpt(v, $W.I64); +} +function $r_opt_i64(r) { + return r.readOpt($R.I64); +} /** `Gadget?` */ function $w_opt_Gadget(w, v) { w.writeOpt(v, $w$kitchen$Gadget); @@ -477,6 +718,13 @@ function $w_opt_Gadget(w, v) { function $r_opt_Gadget(r) { return r.readOpt($r$kitchen$Gadget); } +/** `[Gadget]` */ +function $w_list_Gadget(w, v) { + w.writeList(v, $w$kitchen$Gadget); +} +function $r_list_Gadget(r) { + return r.readList($r$kitchen$Gadget); +} /** `Item?` */ function $w_opt_Item(w, v) { w.writeOpt(v, $w$kitchen$Item); @@ -484,12 +732,26 @@ function $w_opt_Item(w, v) { function $r_opt_Item(r) { return r.readOpt($r$kitchen$Item); } -/** `i64?` */ -function $w_opt_i64(w, v) { - w.writeOpt(v, $W.I64); +/** `[Item]` */ +function $w_list_Item(w, v) { + w.writeList(v, $w$kitchen$Item); } -function $r_opt_i64(r) { - return r.readOpt($R.I64); +function $r_list_Item(r) { + return r.readList($r$kitchen$Item); +} +/** `{string:i32}` */ +function $w_map_string_i32(w, v) { + w.writeMap(v, $WK.String, $W.I32); +} +function $r_map_string_i32(r) { + return r.readMap($R.String, $R.I32); +} +/** `string?` */ +function $w_opt_string(w, v) { + w.writeOpt(v, $W.String); +} +function $r_opt_string(r) { + return r.readOpt($R.String); } const shared$ping = function ping() { @@ -507,11 +769,11 @@ function $adapt$kitchen$ReadyListener(impl) { impl.onReady($a0, $a1); }, on_item($a0, $a1) { - return $ret.Bool(impl.onItem($decode($a0, $r$kitchen$Item), $adopt(kitchen$Gadget, $a1)), 'ReadyListener.onItem'); + return $check.Bool(impl.onItem($decode($a0, $r$kitchen$Item), $adopt(kitchen$Gadget, $a1)), 'the return value of ReadyListener.onItem'); }, label() { try { - return $ret.String(impl.label(), 'ReadyListener.label'); + return $check.String(impl.label(), 'the return value of ReadyListener.label'); } catch ($e) { throw $raise$kitchen$KitchenErrors($e); } @@ -525,6 +787,29 @@ function $adapt$kitchen$ReadyListener(impl) { }; } +function $adapt$kitchen$Sampler(impl) { + $impl(impl, 'Sampler'); + return { + threshold($a0) { + return $opt(impl.threshold($a0), $check.F64, 'the return value of Sampler.threshold'); + }, + samples($a0, $a1) { + try { + return $slice.I16(impl.samples($a0, Array.from($a1)), 'the return value of Sampler.samples'); + } catch ($e) { + throw $raise$kitchen$PantryError($e); + } + }, + describe() { + try { + return $check.String(impl.describe(), 'the return value of Sampler.describe'); + } catch ($e) { + throw $untyped($e); + } + }, + }; +} + const kitchen$Gadget = class Gadget extends $Object { constructor(id) { super(); @@ -560,6 +845,16 @@ const kitchen$Gadget = class Gadget extends $Object { $unlend(this); } } + readings(limit) { + const $self = $lend(this, kitchen$Gadget); + try { + return Array.from($raw.kitchen_sink_kitchen_Gadget_readings($self, limit)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } static version() { try { return $raw.kitchen_sink_kitchen_Gadget_version(); @@ -752,6 +1047,138 @@ const kitchen$newOp = function newOp() { } }; +const kitchen$optI32Id = function optI32Id(x) { + try { + return $raw.kitchen_sink_kitchen_opt_i32_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$optBoolId = function optBoolId(x) { + try { + return $raw.kitchen_sink_kitchen_opt_bool_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$optPriority = function optPriority(p) { + try { + return $raw.kitchen_sink_kitchen_opt_priority(p); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$optMixed = function optMixed(base, scale, offset, label) { + try { + return $raw.kitchen_sink_kitchen_opt_mixed(base, scale, offset, $encode(label, $w_opt_string)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$sumF64 = function sumF64(xs) { + try { + return $raw.kitchen_sink_kitchen_sum_f64($slice.F64(xs, 'xs')); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$scaleI32 = function scaleI32(xs, factor) { + try { + return Array.from($raw.kitchen_sink_kitchen_scale_i32($slice.I32(xs, 'xs'), factor)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$idsU64 = function idsU64(ids) { + try { + return Array.from($raw.kitchen_sink_kitchen_ids_u64($slice.U64(ids, 'ids'))); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$mixedSlices = function mixedSlices(a, b, c, d, e, f) { + try { + return Array.from($raw.kitchen_sink_kitchen_mixed_slices($slice.I8(a, 'a'), $slice.I16(b, 'b'), $slice.U16(c, 'c'), $slice.U32(d, 'd'), $slice.I64(e, 'e'), $slice.F32(f, 'f'))); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$asyncMaybeCount = async function asyncMaybeCount() { + try { + return await $raw.kitchen_sink_kitchen_async_maybe_count(); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$asyncSamples = async function asyncSamples(n, $options) { + try { + return Array.from(await $cancellable($options?.signal, $tokens, ($t) => $raw.kitchen_sink_kitchen_async_samples(n, $t))); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$kitchen$stream_maybe = { + next: (h) => $raw.kitchen_sink_kitchen_StreamMaybeIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamMaybeIterator_destroy(h), + map: $fault, + convert: null, +}; +const kitchen$streamMaybe = function streamMaybe() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_maybe(), $it$kitchen$stream_maybe); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$kitchen$stream_chunks = { + next: (h) => $raw.kitchen_sink_kitchen_StreamChunksIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamChunksIterator_destroy(h), + map: $fault, + convert: (v) => Array.from(v), +}; +const kitchen$streamChunks = function streamChunks() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_chunks(), $it$kitchen$stream_chunks); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$sampleWith = function sampleWith(sampler, level) { + try { + return Array.from($raw.kitchen_sink_kitchen_sample_with($adapt$kitchen$Sampler(sampler), level)); + } catch ($e) { + throw $from$kitchen$PantryError($e); + } +}; + +const kitchen$takeFromPantry = function takeFromPantry(item) { + try { + return $raw.kitchen_sink_kitchen_take_from_pantry(item); + } catch ($e) { + throw $from$kitchen$PantryError($e); + } +}; + +const kitchen$parseAny = function parseAny(text) { + try { + return $raw.kitchen_sink_kitchen_parse_any(text); + } catch ($e) { + throw $fault($e); + } +}; + const kitchen$nested$hello = function hello() { try { return $raw.kitchen_sink_kitchen_nested_hello(); @@ -765,9 +1192,12 @@ export const shared = Object.freeze({ }); export const kitchen = Object.freeze({ - KitchenErrorsError: kitchen$KitchenErrorsError, + KitchenError: kitchen$KitchenError, NotFoundError: kitchen$NotFoundError, InvalidInputError: kitchen$InvalidInputError, + PantryError: kitchen$PantryError, + OutOfStockError: kitchen$OutOfStockError, + SpoiledError: kitchen$SpoiledError, Priority: kitchen$Priority, Gadget: kitchen$Gadget, boolId: kitchen$boolId, @@ -791,6 +1221,21 @@ export const kitchen = Object.freeze({ doCancellable: kitchen$doCancellable, legacyOp: kitchen$legacyOp, newOp: kitchen$newOp, + optI32Id: kitchen$optI32Id, + optBoolId: kitchen$optBoolId, + optPriority: kitchen$optPriority, + optMixed: kitchen$optMixed, + sumF64: kitchen$sumF64, + scaleI32: kitchen$scaleI32, + idsU64: kitchen$idsU64, + mixedSlices: kitchen$mixedSlices, + asyncMaybeCount: kitchen$asyncMaybeCount, + asyncSamples: kitchen$asyncSamples, + streamMaybe: kitchen$streamMaybe, + streamChunks: kitchen$streamChunks, + sampleWith: kitchen$sampleWith, + takeFromPantry: kitchen$takeFromPantry, + parseAny: kitchen$parseAny, nested: Object.freeze({ hello: kitchen$nested$hello, }), diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__linear_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__linear_js.snap deleted file mode 100644 index 3359c8df..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__linear_js.snap +++ /dev/null @@ -1,519 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// The WebAssembly transport: loading the module, staging arguments in its -// linear memory, reading results and errors back, and installing JavaScript -// functions in its function table (async completions and callback -// interface vtables). The generated `index.js` builds one native entry point -// per C symbol on top of this. -// -// Byte runs from `{prefix}_alloc` have alignment 1, so every multi-byte -// read and write goes through a `DataView`, and the slots the producer -// writes through typed pointers (the error struct, `out_len`, iterator -// items, vtables) live in 8-byte-aligned blocks carved from the transport's -// own arenas. - -import { $Fault } from './runtime.js'; - -const $enc = new TextEncoder(); -const $dec = new TextDecoder(); - -function $expected(what, v) { - return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); -} - -function $bigint(v, bits, what) { - let b; - if (typeof v === 'bigint') { - b = v; - } else if (typeof v === 'number' && Number.isInteger(v)) { - b = BigInt(v); - } else { - throw $expected('a bigint', v); - } - if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); - return b; -} - -/** The size of each arena the transport carves its permanent blocks from. */ -const $ARENA = 4096; - -/** - * One loaded module's linear memory and runtime symbols. `x` holds the - * module's exports by C symbol name; `memory()` returns the current - * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the - * function table and returns its index. - */ -export class $Linear { - constructor(x, prefix, memory, fnptr) { - this.x = x; - this.memory = memory; - this.fnptr = fnptr; - this.allocFn = x[`${prefix}_alloc`]; - this.freeBytesFn = x[`${prefix}_free_bytes`]; - this.errorClearFn = x[`${prefix}_error_clear`]; - this.errorFreeFn = x[`${prefix}_error_free`]; - this.errorSetFn = x[`${prefix}_error_set`]; - this.errorSetPayloadFn = x[`${prefix}_error_set_payload`]; - if (typeof this.allocFn !== 'function' || typeof this.freeBytesFn !== 'function') { - throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_free_bytes`); - } - // Permanent blocks (the scratch slots and the vtables) come from - // arenas allocated with {prefix}_alloc and never freed; `arenas` counts - // them so the leak counter of byte runs can leave them out. - this.arenas = 0; - this.arenaNext = 0; - this.arenaEnd = 0; - // One scratch block reused by every call: the error slot (16 bytes), - // the out_len slot (4), and the iterator out_item slot (8). The producer - // only writes them as a call returns, so a call made from a callback in - // the middle of another call can reuse them safely. - const scratch = this.reserve(32); - this.err = scratch; - this.len = scratch + 16; - this.item = scratch + 24; - this.callbacks = new Map(); - this.nextCallback = 1; - this.pending = new Map(); - this.nextPending = 1; - this.completions = new Map(); - this.vtables = new Map(); - } - - bytes() { - const buffer = this.memory(); - if (buffer !== this.buffer) { - this.buffer = buffer; - this.u8 = new Uint8Array(buffer); - this.dv = new DataView(buffer); - } - return this.u8; - } - - view() { - this.bytes(); - return this.dv; - } - - /** A run of `size` bytes from {prefix}_alloc (`0` for an empty one). */ - alloc(size) { - if (size === 0) return 0; - const ptr = this.allocFn(size) >>> 0; - if (ptr === 0) throw new RangeError(`the WebAssembly module could not allocate ${size} bytes`); - return ptr; - } - - /** A permanent, zero-filled, 8-byte-aligned block of `size` bytes. */ - reserve(size) { - const need = Math.ceil(size / 8) * 8; - if (this.arenaNext + need > this.arenaEnd) { - const length = Math.max($ARENA, need + 8); - const base = this.alloc(length); - this.arenas++; - this.arenaNext = Math.ceil(base / 8) * 8; - this.arenaEnd = base + length; - } - const at = this.arenaNext; - this.arenaNext += need; - return at; - } - - /** - * A `{prefix}_debug_live(kind)` result as a bigint, leaving out the - * transport's own arenas from the count of byte runs (kind 4). - */ - live(raw, kind) { - const n = BigInt.asUintN(64, raw); - return kind === 4 && n > 0n ? n - BigInt(this.arenas) : n; - } - - // Argument checks and conversions for by-value slots. - num(v) { - if (typeof v !== 'number') throw $expected('a number', v); - return v; - } - - i64(v) { - return $bigint(v, BigInt.asIntN, 'a signed'); - } - - u64(v) { - return $bigint(v, BigInt.asUintN, 'an unsigned'); - } - - bool(v) { - if (typeof v !== 'boolean') throw $expected('a boolean', v); - return v ? 1 : 0; - } - - handle(v) { - if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); - return v; - } - - handleOpt(v) { - return v === null || v === undefined ? 0 : this.handle(v); - } - - // Staging: copy a string or bytes into a run for one call. A staged value - // is [ptr, len, size]; release it with unstage(). - str(s) { - if (typeof s !== 'string') throw $expected('a string', s); - if (s.length === 0) return [0, 0, 0]; - const size = s.length * 3; - const ptr = this.alloc(size); - const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); - return [ptr, written, size]; - } - - data(b) { - if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); - if (b.length === 0) return [0, 0, 0]; - const ptr = this.alloc(b.length); - this.bytes().set(b, ptr); - return [ptr, b.length, b.length]; - } - - unstage(staged) { - if (staged !== null && staged[2] !== 0) this.freeBytesFn(staged[0], staged[2]); - } - - // Results. - outLen() { - return this.view().getUint32(this.len, true); - } - - readStr(ptr, len) { - ptr >>>= 0; - return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); - } - - readData(ptr, len) { - ptr >>>= 0; - return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); - } - - takeStr(ptr, len) { - const s = this.readStr(ptr, len); - if (ptr !== 0) this.freeBytesFn(ptr, len); - return s; - } - - takeData(ptr, len) { - const b = this.readData(ptr, len); - if (ptr !== 0) this.freeBytesFn(ptr, len); - return b; - } - - cstr(ptr) { - ptr >>>= 0; - if (ptr === 0) return ''; - const u8 = this.bytes(); - let end = ptr; - while (u8[end] !== 0) end++; - return $dec.decode(u8.subarray(ptr, end)); - } - - fault(errPtr) { - const dv = this.view(); - const code = dv.getInt32(errPtr, true); - if (code === 0) return null; - const payloadPtr = dv.getUint32(errPtr + 8, true); - const payloadLen = dv.getUint32(errPtr + 12, true); - return new $Fault( - code, - this.cstr(dv.getUint32(errPtr + 4, true)), - payloadPtr === 0 ? null : this.readData(payloadPtr, payloadLen), - ); - } - - /** Throw the error the last call wrote to the scratch error slot. */ - check() { - if (this.view().getInt32(this.err, true) === 0) return; - const fault = this.fault(this.err); - this.errorClearFn(this.err); - throw fault; - } - - /** A module's contract table as a `BigUint64Array` of `id, hash` pairs. */ - contract(table) { - const ptr = table(this.len) >>> 0; - const n = this.outLen() * 2; - const dv = this.view(); - const out = new BigUint64Array(n); - for (let i = 0; i < n; i++) out[i] = dv.getBigUint64(ptr + i * 8, true); - return out; - } - - // Callback interfaces: implementations live in a table keyed by the - // integer passed as `ctx`, until the producer calls the vtable's `free`. - // An absent optional implementation is ctx 0 with a null vtable. - register(adapter) { - if (adapter === null) return 0; - const id = this.nextCallback++; - this.callbacks.set(id, adapter); - return id; - } - - adapter(id) { - const adapter = this.callbacks.get(id); - if (adapter === undefined) throw new Error('callback implementation was already released'); - return adapter; - } - - unregister(id) { - this.callbacks.delete(id); - } - - /** Stage `text` as a NUL-terminated C string (release it with unstage()). */ - cstrArg(text) { - const bytes = $enc.encode(text); - const ptr = this.alloc(bytes.length + 1); - this.bytes().set(bytes, ptr); - return [ptr, bytes.length, bytes.length + 1]; - } - - /** - * Report a callback implementation's exception through `errPtr`: a - * `$Fault` (a domain error a throwing method raised) with its code, - * message, and payload, anything else as code -4 with its message. - */ - foreign(errPtr, e) { - let code = -4; - let message; - let payload = null; - if (e instanceof $Fault) { - code = e.code; - message = e.message; - payload = e.payload; - } else { - message = e instanceof Error ? e.message || e.name : String(e); - } - const text = this.cstrArg(message); - try { - this.errorSetFn(errPtr, code, text[0]); - } finally { - this.unstage(text); - } - if (payload !== null && payload.length > 0) { - const staged = this.data(payload); - try { - this.errorSetPayloadFn(errPtr, staged[0], staged[1]); - } finally { - this.unstage(staged); - } - } - } - - /** - * Hand `bytes` (a `Uint8Array`) to the producer through a callback's - * `out_ptr`/`out_len` slots, as a run from {prefix}_alloc it adopts. - */ - give(outPtr, outLen, bytes) { - if (!(bytes instanceof Uint8Array)) throw $expected('a Uint8Array', bytes); - const run = this.alloc(bytes.length); - if (bytes.length > 0) this.bytes().set(bytes, run); - const dv = this.view(); - dv.setUint32(outPtr >>> 0, run, true); - dv.setUint32(outLen >>> 0, bytes.length, true); - } - - /** `give` for a string, as UTF-8. */ - giveStr(outPtr, outLen, s) { - if (typeof s !== 'string') throw $expected('a string', s); - this.give(outPtr, outLen, $enc.encode(s)); - } - - /** - * The static vtable of one callback interface, built on first use: the - * header (`size`, `flags`, then the `free` entry), then one table entry - * per method (`methods()` returns each as `[params, results, sig, fn]`). - */ - vtable(name, methods) { - let ptr = this.vtables.get(name); - if (ptr !== undefined) return ptr; - const entries = [[['i32'], [], 'vi', (ctx) => this.unregister(ctx)], ...methods()]; - const size = 8 + entries.length * 4; - ptr = this.reserve(size); - const dv = this.view(); - dv.setUint32(ptr, size, true); - dv.setUint32(ptr + 4, 0, true); - entries.forEach(([params, results, sig, fn], i) => { - dv.setUint32(ptr + 8 + i * 4, this.fnptr(params, results, sig, fn), true); - }); - this.vtables.set(name, ptr); - return ptr; - } - - /** `vtable`, or a null vtable for an absent optional implementation. */ - vtableOpt(adapter, name, methods) { - return adapter === null ? 0 : this.vtable(name, methods); - } - - /** - * Launch an async call. `call(callback, context)` invokes the launcher; - * the completion (one table entry per signature, shared by every call) - * settles the returned promise, converting the result slots with - * `convert`. On this single-threaded target the completion normally - * fires before the launcher returns. - */ - launch(params, results, sig, call, convert) { - let callback = this.completions.get(sig); - if (callback === undefined) { - const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); - callback = this.fnptr(params, results, sig, complete); - this.completions.set(sig, callback); - } - const id = this.nextPending++; - return new Promise((resolve, reject) => { - this.pending.set(id, { resolve, reject, convert }); - try { - call(callback, id); - } catch (e) { - this.pending.delete(id); - reject(e); - } - }); - } - - complete(id, errPtr, result) { - const pending = this.pending.get(id); - if (pending === undefined) return; - this.pending.delete(id); - try { - const fault = errPtr === 0 ? null : this.fault(errPtr >>> 0); - if (errPtr !== 0) this.errorFreeFn(errPtr); - if (fault !== null) { - pending.reject(fault); - } else { - pending.resolve(pending.convert(...result)); - } - } catch (e) { - pending.reject(e); - } - } -} - -/** A stand-in for the native entry points until `init()` has finished. */ -export function $unloaded(library) { - return new Proxy( - {}, - { - get() { - throw new Error(`${library}: call init() before using the bindings`); - }, - }, - ); -} - -const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; - -function $leb(n) { - const out = []; - do { - let b = n & 0x7f; - n >>>= 7; - if (n !== 0) b |= 0x80; - out.push(b); - } while (n !== 0); - return out; -} - -function $section(id, body) { - return [id, ...$leb(body.length), ...body]; -} - -/** - * Turn a JavaScript function into a WebAssembly function with the given - * signature: `WebAssembly.Function` where the engine has it, otherwise a - * tiny module that imports `fn` and re-exports it. - */ -function $wasmFunction(params, results, fn) { - if (typeof WebAssembly.Function === 'function') { - return new WebAssembly.Function({ parameters: params, results }, fn); - } - const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; - type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); - const bytes = new Uint8Array([ - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, - ...$section(1, [1, ...type]), - ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), - ...$section(7, [1, 1, 0x66, 0x00, 0x00]), - ]); - const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); - return instance.exports.f; -} - -async function $bytesOf(source, envVar, fallback) { - if (source === undefined || source === null) { - const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; - if (env) { - const { readFile } = await import('node:fs/promises'); - return readFile(env); - } - source = fallback; - } - if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { - return source; - } - if (typeof Response === 'function' && source instanceof Response) { - return source.arrayBuffer(); - } - const url = source instanceof URL ? source : new URL(String(source), fallback); - if (url.protocol === 'file:') { - const { readFile } = await import('node:fs/promises'); - return readFile(url); - } - const response = await fetch(url); - if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); - return response.arrayBuffer(); -} - -/** - * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a - * path, bytes, a `Response`, or a compiled module). Without a source it - * reads the file named by the `envVar` environment variable (on Node.js), - * else `fallback`. The module must export its memory and a growable - * function table (`__indirect_function_table`). - */ -export async function $loadWasm(source, prefix, envVar, fallback) { - const bytes = await $bytesOf(source, envVar, fallback); - const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); - const { exports } = await WebAssembly.instantiate(module, {}); - const table = exports.__indirect_function_table; - const fnptr = (params, results, sig, fn) => { - if (!(table instanceof WebAssembly.Table)) { - throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); - } - const index = table.grow(1); - table.set(index, $wasmFunction(params, results, fn)); - return index; - }; - return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); -} - -/** - * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` - * factory returns). Its exports carry a leading underscore, its memory is - * read through `HEAPU8`, and table entries are installed with - * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, - * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. - */ -export async function $loadEmscripten(module, prefix, symbols) { - const m = await module; - const x = {}; - for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; - const fnptr = (params, results, sig, fn) => { - if (typeof m.addFunction !== 'function') { - throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); - } - return m.addFunction(fn, sig); - }; - return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); -} - -// END linear.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap deleted file mode 100644 index e56dd31c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate", - "name": "kitchen_sink", - "version": "0.1.0", - "description": "Native bindings for kitchen_sink", - "type": "module", - "main": "index.js", - "types": "index.d.ts", - "exports": { - ".": { - "types": "./index.d.ts", - "default": "./index.js" - } - }, - "files": [ - "index.js", - "index.d.ts", - "runtime.js", - "linear.js" - ] -} diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__runtime_js.snap deleted file mode 100644 index 02cd16c0..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__runtime_js.snap +++ /dev/null @@ -1,636 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -// The runtime shared by every binding in this package: the error classes, -// the value-buffer codec, the object and iterator wrappers, cancellation, and -// the load-time contract check. The generated `index.js` imports it; the -// native transport (an N-API addon or a WebAssembly instance) never sees it, -// except for `$Fault`, the shape every native failure arrives in. - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export class KitchenSinkError extends Error { - constructor(code, message) { - super(message === undefined ? '' : message); - this.name = new.target.name; - this.code = code; - } -} - -/** The error a cancelled async call rejects with (code -5). */ -export class CancelledError extends KitchenSinkError { - constructor(message = 'cancelled') { - super(-5, message); - } -} - -/** - * A failure as the native transport reports it, before the generated - * wrappers map it onto an error class. `payload` holds the matched error - * code's fields as a value buffer, or `null`. - */ -export class $Fault { - constructor(code, message, payload) { - this.code = code; - this.message = message; - this.payload = payload === undefined ? null : payload; - } -} - -/** - * Map an exception raised by a native call onto the public error classes: - * a cancelled call becomes `CancelledError`, any other fault the root error, - * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a - * code -2 error. Anything else (a `TypeError` from argument checking, say) - * passes through unchanged. - */ -export function $fault(e) { - if (e instanceof $Fault) { - return e.code === -5 - ? new CancelledError(e.message || undefined) - : new KitchenSinkError(e.code, e.message); - } - if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { - return new KitchenSinkError(-2, 'the native library trapped: ' + e.message); - } - return e; -} - -/** - * Map a fault onto an error domain: `codes` maps each declared code to its - * class, and `payloads` maps each code that carries fields to the reader - * that decodes them (the class's constructor then takes the fields first). - * Codes outside the domain fall back to `$fault`. - */ -export function $domain(e, codes, payloads) { - if (e instanceof $Fault) { - const cls = codes.get(e.code); - if (cls !== undefined) { - const message = e.message || undefined; - const read = payloads.get(e.code); - if (read === undefined) return new cls(message); - return new cls(e.payload === null ? {} : $decode(e.payload, read), message); - } - } - return $fault(e); -} - -/** - * The fault a callback implementation reports for exception `e` from a - * method that throws the error domain `domain`: a declared code of the - * domain travels with its fields (`fields` maps each code that has any to - * the writer that encodes them); anything else is rethrown unchanged and - * reported as a code -4 failure. - */ -export function $raise(e, domain, codes, fields) { - if (e instanceof domain && codes.has(e.code)) { - const write = fields.get(e.code); - return new $Fault(e.code, e.message, write === undefined ? null : $encode(e, write)); - } - return e; -} - -function $malformed(what) { - return new KitchenSinkError(-3, 'malformed value buffer: ' + what); -} - -function $expected(what, v) { - return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); -} - -function $bigint(v) { - if (typeof v === 'bigint') return v; - if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); - throw $expected('a bigint', v); -} - -const $utf8 = new TextEncoder(); -const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); - -/** - * Writes the value-buffer wire format: little-endian, packed, no alignment. - * Strings and bytes are a u32 length then the bytes, optionals a presence - * byte then the value, lists a u32 count then the elements, and maps a u32 - * count then alternating keys and values. - */ -export class $Writer { - constructor() { - this.buf = new Uint8Array(64); - this.view = new DataView(this.buf.buffer); - this.len = 0; - } - - reserve(n) { - if (this.len + n <= this.buf.length) return; - let cap = this.buf.length * 2; - while (cap < this.len + n) cap *= 2; - const grown = new Uint8Array(cap); - grown.set(this.buf.subarray(0, this.len)); - this.buf = grown; - this.view = new DataView(grown.buffer); - } - - num(v) { - if (typeof v !== 'number') throw $expected('a number', v); - return v; - } - - big(v, bits) { - const b = $bigint(v); - if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); - return b; - } - - writeBool(v) { - if (typeof v !== 'boolean') throw $expected('a boolean', v); - this.reserve(1); - this.buf[this.len++] = v ? 1 : 0; - } - - writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } - writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } - writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } - writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } - writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } - writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } - writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } - writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } - writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } - writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } - - writeString(v) { - if (typeof v !== 'string') throw $expected('a string', v); - // UTF-8 never needs more than three bytes per UTF-16 code unit. - this.reserve(4 + v.length * 3); - const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); - this.view.setUint32(this.len, written, true); - this.len += 4 + written; - } - - writeBytes(v) { - if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); - this.reserve(4 + v.length); - this.view.setUint32(this.len, v.length, true); - this.buf.set(v, this.len + 4); - this.len += 4 + v.length; - } - - writeOpt(v, write) { - if (v === null || v === undefined) { - this.writeBool(false); - } else { - this.writeBool(true); - write(this, v); - } - } - - writeList(v, write) { - if (!Array.isArray(v)) throw $expected('an array', v); - this.writeU32(v.length); - for (const e of v) write(this, e); - } - - writeMap(v, writeKey, writeValue) { - if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); - const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); - this.writeU32(entries.length); - for (const [k, e] of entries) { - writeKey(this, k); - writeValue(this, e); - } - } - - finish() { - return this.buf.subarray(0, this.len); - } -} - -/** - * Reads the value-buffer wire format. Every read is bounds-checked; a - * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or - * trailing bytes throw a code -3 error. - */ -export class $Reader { - constructor(bytes) { - this.buf = bytes; - this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - this.pos = 0; - } - - take(n) { - if (n > this.buf.length - this.pos) throw $malformed('truncated'); - const at = this.pos; - this.pos += n; - return at; - } - - readBool() { - const b = this.buf[this.take(1)]; - if (b > 1) throw $malformed('invalid bool byte ' + b); - return b === 1; - } - - readI8() { return this.view.getInt8(this.take(1)); } - readU8() { return this.view.getUint8(this.take(1)); } - readI16() { return this.view.getInt16(this.take(2), true); } - readU16() { return this.view.getUint16(this.take(2), true); } - readI32() { return this.view.getInt32(this.take(4), true); } - readU32() { return this.view.getUint32(this.take(4), true); } - readI64() { return this.view.getBigInt64(this.take(8), true); } - readU64() { return this.view.getBigUint64(this.take(8), true); } - readF32() { return this.view.getFloat32(this.take(4), true); } - readF64() { return this.view.getFloat64(this.take(8), true); } - - readString() { - const n = this.readU32(); - const at = this.take(n); - try { - return $utf8Strict.decode(this.buf.subarray(at, at + n)); - } catch { - throw $malformed('invalid UTF-8'); - } - } - - readBytes() { - const n = this.readU32(); - const at = this.take(n); - return this.buf.slice(at, at + n); - } - - readOpt(read) { - return this.readBool() ? read(this) : null; - } - - // A count may exceed the remaining bytes when elements encode to zero - // bytes (a record without fields), so it is not checked up front; a - // truncated element still fails on its first read. - readList(read) { - const n = this.readU32(); - const out = []; - for (let i = 0; i < n; i++) out.push(read(this)); - return out; - } - - readMap(readKey, readValue) { - const n = this.readU32(); - const out = {}; - for (let i = 0; i < n; i++) { - const k = readKey(this); - out[k] = readValue(this); - } - return out; - } - - end() { - if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); - } -} - -/** Encode `v` with `write` into a fresh value buffer. */ -export function $encode(v, write) { - const w = new $Writer(); - write(w, v); - return w.finish(); -} - -/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ -export function $decode(bytes, read) { - const r = new $Reader(bytes); - const v = read(r); - r.end(); - return v; -} - -/** Element writers for the fixed-width primitives, strings, and bytes. */ -export const $W = Object.freeze({ - Bool: (w, v) => w.writeBool(v), - I8: (w, v) => w.writeI8(v), - I16: (w, v) => w.writeI16(v), - I32: (w, v) => w.writeI32(v), - I64: (w, v) => w.writeI64(v), - U8: (w, v) => w.writeU8(v), - U16: (w, v) => w.writeU16(v), - U32: (w, v) => w.writeU32(v), - U64: (w, v) => w.writeU64(v), - F32: (w, v) => w.writeF32(v), - F64: (w, v) => w.writeF64(v), - String: (w, v) => w.writeString(v), - Bytes: (w, v) => w.writeBytes(v), -}); - -/** - * Map key writers. Plain-object keys are always strings, so numeric, - * `bigint`, and `bool` keys convert first (a `Map`'s typed keys pass - * through the same conversions unchanged). C-style enum keys use `I32`. - */ -export const $WK = Object.freeze({ - Bool: (w, k) => w.writeBool(k === true || k === 'true'), - I8: (w, k) => w.writeI8(Number(k)), - I16: (w, k) => w.writeI16(Number(k)), - I32: (w, k) => w.writeI32(Number(k)), - I64: (w, k) => w.writeI64(BigInt(k)), - U8: (w, k) => w.writeU8(Number(k)), - U16: (w, k) => w.writeU16(Number(k)), - U32: (w, k) => w.writeU32(Number(k)), - U64: (w, k) => w.writeU64(BigInt(k)), - F32: (w, k) => w.writeF32(Number(k)), - F64: (w, k) => w.writeF64(Number(k)), - String: (w, k) => w.writeString(k), - Bytes: (w, k) => w.writeBytes(k), -}); - -/** Element readers for the fixed-width primitives, strings, and bytes. */ -export const $R = Object.freeze({ - Bool: (r) => r.readBool(), - I8: (r) => r.readI8(), - I16: (r) => r.readI16(), - I32: (r) => r.readI32(), - I64: (r) => r.readI64(), - U8: (r) => r.readU8(), - U16: (r) => r.readU16(), - U32: (r) => r.readU32(), - U64: (r) => r.readU64(), - F32: (r) => r.readF32(), - F64: (r) => r.readF64(), - String: (r) => r.readString(), - Bytes: (r) => r.readBytes(), -}); - -/** The key `using` declarations call, or a stand-in where it is missing. */ -export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); - -// Releases the native reference of a wrapper collected without close(). -// The held value never references the wrapper, so it can't keep it alive. -const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); - -function $release(o) { - const handle = o.$h; - o.$h = null; - o.constructor.$destroy(handle); -} - -/** - * The base of every interface class. An instance owns one strong reference - * to a native object. `close()` (or a `using` declaration) releases it; - * a wrapper that is garbage collected unclosed is released by a - * `FinalizationRegistry`. A `close()` that races an in-flight call (from a - * callback, or while an async method is pending) is deferred until that - * call returns, so the native object is never freed mid-call. - */ -export class $Object { - close() { - if (this.$closed !== false) return; - this.$closed = true; - $objects.unregister(this); - if (this.$calls === 0) $release(this); - } - - [$dispose]() { - this.close(); - } -} - -/** Bind a native handle (one strong reference) to wrapper `o`. */ -export function $own(o, handle) { - o.$h = handle; - o.$calls = 0; - o.$closed = false; - $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); - return o; -} - -/** Wrap a handle the native side handed over in a new `cls` instance. */ -export function $adopt(cls, handle) { - return $own(Object.create(cls.prototype), handle); -} - -/** `$adopt`, with `null` for an absent object. */ -export function $adoptOpt(cls, handle) { - return handle === null ? null : $adopt(cls, handle); -} - -/** - * Lend wrapper `o` (an instance of `cls`) to one native call and return its - * handle. Every `$lend` is paired with an `$unlend` when the call returns. - */ -export function $lend(o, cls) { - if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); - if (o.$closed !== false) throw new KitchenSinkError(-3, `${cls.name} used after close()`); - o.$calls++; - return o.$h; -} - -/** `$lend`, with `null` for an absent object. */ -export function $lendOpt(o, cls) { - return o === null || o === undefined ? null : $lend(o, cls); -} - -/** End one loan of `o`, completing a `close()` deferred by the loan. */ -export function $unlend(o) { - if (o === null || o === undefined) return; - if (--o.$calls === 0 && o.$closed) $release(o); -} - -/** - * A second strong reference to `o`'s native object, for an object token - * written into a value buffer (the native side adopts it). - */ -export function $clone(o, cls) { - const handle = $lend(o, cls); - try { - return cls.$clone(handle); - } finally { - $unlend(o); - } -} - -/** `$clone`, with `null` for an absent object. */ -export function $cloneOpt(o, cls) { - return o === null || o === undefined ? null : $clone(o, cls); -} - -const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); - -/** - * A lazy iterator over a native `iter`: each `next()` makes exactly one - * native call. The native iterator is destroyed exactly once: when it is - * exhausted or fails, by `return()` (which `for...of` calls on `break`), by - * `close()` or a `using` declaration, or when the iterator is collected. - * `spec` supplies `next`, `destroy`, `map` (the error mapping), and - * `convert` (or `null` when elements need no conversion). - */ -export class $Iterator { - constructor(handle, spec) { - this.$h = handle; - this.$spec = spec; - $iterators.register(this, { handle, destroy: spec.destroy }, this); - } - - next() { - if (this.$h === null) return { done: true, value: undefined }; - const spec = this.$spec; - let v; - try { - v = spec.next(this.$h); - } catch (e) { - this.close(); - throw spec.map(e); - } - if (v === undefined) { - this.close(); - return { done: true, value: undefined }; - } - return { done: false, value: spec.convert === null ? v : spec.convert(v) }; - } - - return(value) { - this.close(); - return { done: true, value }; - } - - close() { - const handle = this.$h; - if (handle === null) return; - this.$h = null; - $iterators.unregister(this); - this.$spec.destroy(handle); - } - - [Symbol.iterator]() { - return this; - } - - [$dispose]() { - this.close(); - } -} - -/** - * Run a cancellable launch. Without a signal, `launch(null)` runs as is. - * With one, a native cancel token is created and passed to `launch`, - * aborting the signal cancels it (an already-aborted signal cancels it - * before the launch, so the call completes with `CancelledError` at once), - * and the token is destroyed once the call settles. `tokens` supplies the - * native `create`, `cancel`, and `destroy`. - */ -export function $cancellable(signal, tokens, launch) { - if (signal === undefined || signal === null) return launch(null); - if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); - const token = tokens.create(); - const abort = () => tokens.cancel(token); - const done = () => { - signal.removeEventListener('abort', abort); - tokens.destroy(token); - }; - if (signal.aborted) { - abort(); - } else { - signal.addEventListener('abort', abort, { once: true }); - } - let pending; - try { - pending = launch(token); - } catch (e) { - done(); - throw e; - } - return pending.finally(done); -} - -/** Check a callback interface implementation. */ -export function $impl(impl, what) { - if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { - throw $expected(`a ${what} implementation`, impl); - } - return impl; -} - -function $num(v, where) { - if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); - return v; -} - -function $retBigint(v, where) { - if (typeof v === 'bigint') return v; - if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); - throw new TypeError(`${where} must return a bigint`); -} - -/** - * Checks for a callback method's return value, by wire primitive (plus - * `String` and `Bytes`). A value of the wrong type throws, which reports - * the call as a code -4 failure. - */ -export const $ret = Object.freeze({ - Bool(v, where) { - if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); - return v; - }, - I8: $num, - I16: $num, - I32: $num, - U8: $num, - U16: $num, - U32: $num, - F32: $num, - F64: $num, - I64: $retBigint, - U64: $retBigint, - String(v, where) { - if (typeof v !== 'string') throw new TypeError(`${where} must return a string`); - return v; - }, - Bytes(v, where) { - if (!(v instanceof Uint8Array)) throw new TypeError(`${where} must return a Uint8Array`); - return v; - }, -}); - -/** - * The load-time checks: the native library must implement ABI revision - * `abi`, and, for every top-level module, its contract table must hold each - * entry these bindings were generated with. `contract` lists - * `[module, [[id, hash, path], ...]]` per module; the native side returns - * a module's table as a `BigUint64Array` of `id, hash` pairs. Entries the - * library has and the bindings don't are fine. - */ -export function $verify(raw, library, prefix, abi, contract) { - const version = raw[`${prefix}_abi_version`]; - if (typeof version !== 'function') { - throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); - } - const found = Number(version()); - if (found !== abi) { - throw new Error( - `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, - ); - } - for (const [module, expected] of contract) { - const table = raw[`${prefix}_${module}_contract`]; - if (typeof table !== 'function') { - throw new Error(`${library}: the native library does not export ${prefix}_${module}_contract`); - } - const pairs = table(); - const hashes = new Map(); - for (let i = 0; i + 1 < pairs.length; i += 2) hashes.set(pairs[i], pairs[i + 1]); - for (const [id, hash, path] of expected) { - const got = hashes.get(id); - if (got === undefined) { - throw new Error(`${library}: ${path} is missing from the library`); - } - if (got !== hash) { - throw new Error(`${library}: ${path} changed since these bindings were generated`); - } - } - } -} - -// END runtime.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_d_ts.snap deleted file mode 100644 index bdae45d9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_d_ts.snap +++ /dev/null @@ -1,91 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class NestedModulesError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends NestedModulesError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof NestedModulesError; -/** - * Load the WebAssembly module and check that it matches these bindings. - * Every other export throws until the returned promise resolves. - */ -export declare function init( - source?: string | URL | BufferSource | WebAssembly.Module | Response, -): Promise; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -export declare namespace graphics { - /** The errors of the `graphics` error domain. */ - export class GraphicsError extends $Error { - constructor(code: number, message?: string); - } - /** coordinate out of bounds */ - export class OutOfBoundsError extends graphics.GraphicsError { - static readonly CODE: 1; - constructor(message?: string); - } - /** A measurement unit shared with nested submodules. */ - export enum Unit { - pixels = 0, - points = 1, - } - /** Top-level struct that references a struct defined one level deeper. */ - export interface Canvas { - width: number; - /** Reference down into a nested module */ - root: graphics.shapes.Shape; - } - /** Construct a Canvas. */ - export function newCanvas(width: number): graphics.Canvas; - export namespace shapes { - /** A struct one level deep that references both an ancestor enum and a deeper struct. */ - export interface Shape { - sides: number; - /** Reference up to the parent module's enum */ - unit: graphics.Unit; - /** Reference down into a deeper module */ - detail: graphics.shapes.geometry.Detail; - } - /** Construct a Shape. */ - export function makeShape(sides: number): graphics.shapes.Shape; - /** Nested function returning a struct owned by an ancestor module. */ - export function describe(): graphics.Canvas; - export namespace geometry { - /** A struct two levels deep. */ - export interface Detail { - area: number; - } - /** - * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - * @throws {graphics.GraphicsError} - */ - export function compute(shape: graphics.shapes.Shape): graphics.shapes.geometry.Detail; - } - } -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_js.snap deleted file mode 100644 index f1b39276..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_js.snap +++ /dev/null @@ -1,231 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - NestedModulesError as $Error, -} from './runtime.js'; -export { NestedModulesError, CancelledError } from './runtime.js'; -import { $loadWasm, $unloaded } from './linear.js'; - -let $raw = $unloaded('nested_modules'); -// Handles are linear-memory addresses; object tokens are u64s. -const $token = Number; -let $loading = null; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['graphics', [ - [0x2c29eef9327a6bd4n, 0x12983d5271651389n, 'graphics.Unit'], - [0x304088bbc28f0015n, 0x97e6e809913be6b6n, 'graphics.new_canvas'], - [0x49f8837532df9ac6n, 0x32e5b27e81ee0957n, 'graphics.Canvas'], - [0x55717fa027336b47n, 0xb2bd3a6bdcce48c8n, 'graphics.shapes.describe'], - [0x7626b91b8f1f53cdn, 0x867bbf7cf0f41c74n, 'graphics.shapes.geometry.compute'], - [0xb3efcb50d9a61ab7n, 0x3cb0207dfad2a149n, 'graphics.GraphicsError'], - [0xc62b16a9591b744bn, 0x7895610c8487eb1bn, 'graphics.shapes.Shape'], - [0xe13001f4983ccd35n, 0x16e7de070b4a4cfcn, 'graphics.shapes.geometry.Detail'], - [0xf3f658bcfc5dba62n, 0x2665ccb47356e746n, 'graphics.shapes.make_shape'], - ]], -]; - -/** - * Load the WebAssembly module (by default `nested_modules.wasm` next to this file, or - * the file named by `NESTED_MODULES_LIBRARY` on Node.js) and check that it matches - * these bindings. Every other export throws until the returned promise - * resolves. - */ -export function init(source) { - $loading ??= $loadWasm(source, 'nested_modules', 'NESTED_MODULES_LIBRARY', new URL('./nested_modules.wasm', import.meta.url)) - .then((m) => { - $raw = $bind(m); - try { - $verify($raw, 'nested_modules', 'nested_modules', 4, $contract); - } catch (e) { - $raw = $unloaded('nested_modules'); - throw e; - } - }) - .catch((e) => { - $loading = null; - throw e; - }); - return $loading; -} - -// The raw entry points, one per C symbol. -function $bind(m) { - const x = m.x; - return { - nested_modules_abi_version: () => x.nested_modules_abi_version() >>> 0, - nested_modules_debug_live: (kind) => m.live(x.nested_modules_debug_live(m.num(kind)), kind), - nested_modules_cancel_token_create: () => x.nested_modules_cancel_token_create(), - nested_modules_cancel_token_cancel: (t) => x.nested_modules_cancel_token_cancel(m.handle(t)), - nested_modules_cancel_token_destroy: (t) => x.nested_modules_cancel_token_destroy(m.handle(t)), - nested_modules_graphics_contract: () => m.contract(x.nested_modules_graphics_contract), - nested_modules_graphics_new_canvas: (a0) => { - const r = x.nested_modules_graphics_new_canvas(m.num(a0), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - nested_modules_graphics_shapes_make_shape: (a0) => { - const r = x.nested_modules_graphics_shapes_make_shape(m.num(a0), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - nested_modules_graphics_shapes_describe: () => { - const r = x.nested_modules_graphics_shapes_describe(m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - nested_modules_graphics_shapes_geometry_compute: (a0) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.nested_modules_graphics_shapes_geometry_compute(s0[0], s0[1], m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - } - }, - }; -} - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.nested_modules_debug_live(kind); -} - -const graphics$GraphicsError = class GraphicsError extends $Error {}; -const graphics$OutOfBoundsError = class OutOfBoundsError extends graphics$GraphicsError { - static CODE = 1; - constructor(message = 'coordinate out of bounds') { - super(1, message); - } -}; -const $codes$graphics$GraphicsError = new Map([ - [1, graphics$OutOfBoundsError], -]); -const $payloads$graphics$GraphicsError = new Map(); -function $from$graphics$GraphicsError(e) { - return $domain(e, $codes$graphics$GraphicsError, $payloads$graphics$GraphicsError); -} - -const graphics$Unit = Object.freeze({ - pixels: 0, - points: 1, - 0: 'pixels', - 1: 'points', -}); - -function $w$graphics$Canvas(w, v) { - w.writeI32(v.width); - $w$graphics$shapes$Shape(w, v.root); -} -function $r$graphics$Canvas(r) { - return { - width: r.readI32(), - root: $r$graphics$shapes$Shape(r), - }; -} -function $w$graphics$shapes$Shape(w, v) { - w.writeI32(v.sides); - w.writeI32(v.unit); - $w$graphics$shapes$geometry$Detail(w, v.detail); -} -function $r$graphics$shapes$Shape(r) { - return { - sides: r.readI32(), - unit: r.readI32(), - detail: $r$graphics$shapes$geometry$Detail(r), - }; -} -function $w$graphics$shapes$geometry$Detail(w, v) { - w.writeF64(v.area); -} -function $r$graphics$shapes$geometry$Detail(r) { - return { - area: r.readF64(), - }; -} - -const graphics$newCanvas = function newCanvas(width) { - try { - return $decode($raw.nested_modules_graphics_new_canvas(width), $r$graphics$Canvas); - } catch ($e) { - throw $fault($e); - } -}; - -const graphics$shapes$makeShape = function makeShape(sides) { - try { - return $decode($raw.nested_modules_graphics_shapes_make_shape(sides), $r$graphics$shapes$Shape); - } catch ($e) { - throw $fault($e); - } -}; - -const graphics$shapes$describe = function describe() { - try { - return $decode($raw.nested_modules_graphics_shapes_describe(), $r$graphics$Canvas); - } catch ($e) { - throw $fault($e); - } -}; - -const graphics$shapes$geometry$compute = function compute(shape) { - try { - return $decode($raw.nested_modules_graphics_shapes_geometry_compute($encode(shape, $w$graphics$shapes$Shape)), $r$graphics$shapes$geometry$Detail); - } catch ($e) { - throw $from$graphics$GraphicsError($e); - } -}; - -export const graphics = Object.freeze({ - GraphicsError: graphics$GraphicsError, - OutOfBoundsError: graphics$OutOfBoundsError, - Unit: graphics$Unit, - newCanvas: graphics$newCanvas, - shapes: Object.freeze({ - makeShape: graphics$shapes$makeShape, - describe: graphics$shapes$describe, - geometry: Object.freeze({ - compute: graphics$shapes$geometry$compute, - }), - }), -}); - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_d_ts.snap deleted file mode 100644 index d74e8799..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_d_ts.snap +++ /dev/null @@ -1,84 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -/// - -/** - * The root of every error these bindings throw. `code` is the ABI error - * code: positive for a declared domain error, negative for a runtime trap - * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - * implementation failed, -5 cancelled). - */ -export declare class ShapesError extends Error { - readonly code: number; - constructor(code: number, message?: string); -} -/** The error a cancelled async call rejects with (code -5). */ -export declare class CancelledError extends ShapesError { - constructor(message?: string); -} -/** The root error class, under a name no namespace member can shadow. */ -declare const $Error: typeof ShapesError; -/** - * Load the WebAssembly module and check that it matches these bindings. - * Every other export throws until the returned promise resolves. - */ -export declare function init( - source?: string | URL | BufferSource | WebAssembly.Module | Response, -): Promise; -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all. - * @internal - */ -export declare function __debugLive(kind: number): bigint; - -/** Rich (algebraic) enums and the expanded numeric primitives. */ -export declare namespace shapes { - /** An algebraic shape (sum type with associated data) */ - export type Shape = - /** The empty shape (a unit variant) */ - | { tag: 'Empty' } - /** A circle with a radius */ - | { tag: 'Circle'; radius: number } - /** An axis-aligned rectangle */ - | { tag: 'Rectangle'; width: number; height: number } - /** An integer-bounded shape (exercises narrow + wide ints) */ - | { tag: 'Bounded'; min: number; max: number; weight: bigint } - /** A labeled shape with a small count */ - | { tag: 'Labeled'; label: string; count: number }; - /** A plain C-style enum (no payloads) */ - export enum Channel { - Red = 0, - Green = 1, - Blue = 2, - } - /** One field per supported numeric primitive */ - export interface Numerics { - a: number; - b: number; - c: number; - d: bigint; - e: number; - f: number; - g: number; - h: bigint; - i: number; - j: number; - } - /** Render a shape to a string */ - export function describe(shape: shapes.Shape): string; - /** Scale a shape by a factor, returning a new shape */ - export function scale(shape: shapes.Shape, factor: number): shapes.Shape; - /** Sum a list of bytes into a wide integer (numerics smoke) */ - export function sumBytes(values: Uint8Array): bigint; - /** Build a Numerics struct from every numeric primitive */ - export function makeNumerics(a: number, b: number, c: number, d: bigint, e: number, f: number, g: number, h: bigint, i: number, j: number): shapes.Numerics; -} - -// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_js.snap deleted file mode 100644 index baaf6eff..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_js.snap +++ /dev/null @@ -1,259 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate - -import { - $Fault, - $fault, - $domain, - $raise, - $W, - $WK, - $R, - $encode, - $decode, - $Object, - $own, - $adopt, - $adoptOpt, - $lend, - $lendOpt, - $unlend, - $clone, - $cloneOpt, - $Iterator, - $cancellable, - $impl, - $ret, - $verify, - ShapesError as $Error, -} from './runtime.js'; -export { ShapesError, CancelledError } from './runtime.js'; -import { $loadWasm, $unloaded } from './linear.js'; - -let $raw = $unloaded('shapes'); -// Handles are linear-memory addresses; object tokens are u64s. -const $token = Number; -let $loading = null; - -// The declarations these bindings were generated with, per top-level -// module, as [id, hash, path]: the library's contract tables must hold -// each one (see $verify). -const $contract = [ - ['shapes', [ - [0x0d7b3eec49243e7an, 0x5f232311808e1b1fn, 'shapes.Channel'], - [0x183fa8198100860en, 0xe40c6993cb31d359n, 'shapes.describe'], - [0x2d1395eb837b848en, 0x42202a0c12f3a16en, 'shapes.sum_bytes'], - [0x7f9a5b87ba09b86bn, 0xc5b32fb0efbbaa17n, 'shapes.scale'], - [0xc8edd779240fda17n, 0x68e1be78fddb20e4n, 'shapes.Numerics'], - [0xd3d3881b2fd18a62n, 0xf24d0eb76a99d918n, 'shapes.make_numerics'], - [0xf010844afff5ac3cn, 0x50de28ae9b41ff15n, 'shapes.Shape'], - ]], -]; - -/** - * Load the WebAssembly module (by default `shapes.wasm` next to this file, or - * the file named by `SHAPES_LIBRARY` on Node.js) and check that it matches - * these bindings. Every other export throws until the returned promise - * resolves. - */ -export function init(source) { - $loading ??= $loadWasm(source, 'shapes', 'SHAPES_LIBRARY', new URL('./shapes.wasm', import.meta.url)) - .then((m) => { - $raw = $bind(m); - try { - $verify($raw, 'shapes', 'shapes', 4, $contract); - } catch (e) { - $raw = $unloaded('shapes'); - throw e; - } - }) - .catch((e) => { - $loading = null; - throw e; - }); - return $loading; -} - -// The raw entry points, one per C symbol. -function $bind(m) { - const x = m.x; - return { - shapes_abi_version: () => x.shapes_abi_version() >>> 0, - shapes_debug_live: (kind) => m.live(x.shapes_debug_live(m.num(kind)), kind), - shapes_cancel_token_create: () => x.shapes_cancel_token_create(), - shapes_cancel_token_cancel: (t) => x.shapes_cancel_token_cancel(m.handle(t)), - shapes_cancel_token_destroy: (t) => x.shapes_cancel_token_destroy(m.handle(t)), - shapes_shapes_contract: () => m.contract(x.shapes_shapes_contract), - shapes_shapes_describe: (a0) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.shapes_shapes_describe(s0[0], s0[1], m.len, m.err); - m.check(); - return m.takeStr(r, m.outLen()); - } finally { - m.unstage(s0); - } - }, - shapes_shapes_scale: (a0, a1) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.shapes_shapes_scale(s0[0], s0[1], m.num(a1), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - } finally { - m.unstage(s0); - } - }, - shapes_shapes_sum_bytes: (a0) => { - let s0 = null; - try { - s0 = m.data(a0); - const r = x.shapes_shapes_sum_bytes(s0[0], s0[1], m.err); - m.check(); - return BigInt.asUintN(64, r); - } finally { - m.unstage(s0); - } - }, - shapes_shapes_make_numerics: (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) => { - const r = x.shapes_shapes_make_numerics(m.num(a0), m.num(a1), m.num(a2), m.i64(a3), m.num(a4), m.num(a5), m.num(a6), m.u64(a7), m.num(a8), m.num(a9), m.len, m.err); - m.check(); - return m.takeData(r, m.outLen()); - }, - }; -} - -/** - * The native library's live-resource counters (0 objects, 1 callbacks, - * 2 iterators, 3 cancel tokens, 4 byte runs), for leak tests. Kind -1 is - * `1n` when the library counts at all (its `leak-check` feature), else - * every kind is `0n`. - */ -export function __debugLive(kind) { - return $raw.shapes_debug_live(kind); -} - -const shapes$Channel = Object.freeze({ - Red: 0, - Green: 1, - Blue: 2, - 0: 'Red', - 1: 'Green', - 2: 'Blue', -}); - -function $w$shapes$Numerics(w, v) { - w.writeI8(v.a); - w.writeI16(v.b); - w.writeI32(v.c); - w.writeI64(v.d); - w.writeU8(v.e); - w.writeU16(v.f); - w.writeU32(v.g); - w.writeU64(v.h); - w.writeF32(v.i); - w.writeF64(v.j); -} -function $r$shapes$Numerics(r) { - return { - a: r.readI8(), - b: r.readI16(), - c: r.readI32(), - d: r.readI64(), - e: r.readU8(), - f: r.readU16(), - g: r.readU32(), - h: r.readU64(), - i: r.readF32(), - j: r.readF64(), - }; -} -function $w$shapes$Shape(w, v) { - switch (v.tag) { - case 'Empty': - w.writeI32(0); - return; - case 'Circle': - w.writeI32(1); - w.writeF64(v.radius); - return; - case 'Rectangle': - w.writeI32(2); - w.writeF32(v.width); - w.writeF32(v.height); - return; - case 'Bounded': - w.writeI32(3); - w.writeI16(v.min); - w.writeI16(v.max); - w.writeU64(v.weight); - return; - case 'Labeled': - w.writeI32(4); - w.writeString(v.label); - w.writeU8(v.count); - return; - default: - throw new TypeError('unknown Shape tag: ' + (v && v.tag)); - } -} -function $r$shapes$Shape(r) { - const tag = r.readI32(); - switch (tag) { - case 0: return { tag: 'Empty' }; - case 1: return { tag: 'Circle', radius: r.readF64() }; - case 2: return { tag: 'Rectangle', width: r.readF32(), height: r.readF32() }; - case 3: return { tag: 'Bounded', min: r.readI16(), max: r.readI16(), weight: r.readU64() }; - case 4: return { tag: 'Labeled', label: r.readString(), count: r.readU8() }; - default: - throw new $Error(-3, 'unknown Shape tag: ' + tag); - } -} - -const shapes$describe = function describe(shape) { - try { - return $raw.shapes_shapes_describe($encode(shape, $w$shapes$Shape)); - } catch ($e) { - throw $fault($e); - } -}; - -const shapes$scale = function scale(shape, factor) { - try { - return $decode($raw.shapes_shapes_scale($encode(shape, $w$shapes$Shape), factor), $r$shapes$Shape); - } catch ($e) { - throw $fault($e); - } -}; - -const shapes$sumBytes = function sumBytes(values) { - try { - return $raw.shapes_shapes_sum_bytes(values); - } catch ($e) { - throw $fault($e); - } -}; - -const shapes$makeNumerics = function makeNumerics(a, b, c, d, e, f, g, h, i, j) { - try { - return $decode($raw.shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j), $r$shapes$Numerics); - } catch ($e) { - throw $fault($e); - } -}; - -export const shapes = Object.freeze({ - Channel: shapes$Channel, - describe: shapes$describe, - scale: shapes$scale, - sumBytes: shapes$sumBytes, - makeNumerics: shapes$makeNumerics, -}); - -// END index.js diff --git a/crates/weaveffi-fuzz/Cargo.toml b/crates/weaveffi-fuzz/Cargo.toml index 4903b894..3cb56cf8 100644 --- a/crates/weaveffi-fuzz/Cargo.toml +++ b/crates/weaveffi-fuzz/Cargo.toml @@ -33,13 +33,6 @@ test = false doc = false required-features = ["fuzzing"] -[[bin]] -name = "fuzz_parse_toml" -path = "fuzz_targets/parse_toml.rs" -test = false -doc = false -required-features = ["fuzzing"] - [[bin]] name = "fuzz_parse_type_ref" path = "fuzz_targets/parse_type_ref.rs" diff --git a/crates/weaveffi-fuzz/README.md b/crates/weaveffi-fuzz/README.md index 4be6bc36..c5b49c20 100644 --- a/crates/weaveffi-fuzz/README.md +++ b/crates/weaveffi-fuzz/README.md @@ -13,7 +13,6 @@ locally and in CI. | --- | --- | | `fuzz_parse_yaml` | `weaveffi_model::parse::parse_api_str(..., "yaml")` | | `fuzz_parse_json` | `weaveffi_model::parse::parse_api_str(..., "json")` | -| `fuzz_parse_toml` | `weaveffi_model::parse::parse_api_str(..., "toml")` | | `fuzz_parse_type_ref` | `weaveffi_model::ir::parse_type_ref` | | `fuzz_validate` | parses YAML, then runs `weaveffi_model::validate::validate` on success | | `fuzz_value_buffer` | `weaveffi::abi::decode_value` over several value-buffer shapes; a successful decode must re-encode to the same bytes | @@ -21,8 +20,8 @@ locally and in CI. Seed inputs for each target live in `fuzz/seeds//` and are committed to git. `seed_corpus.py

` copies them into a corpus directory and adds inputs derived from the snapshot fixtures in -`crates/weaveffi-cli/tests/fixtures/` (as YAML, JSON, and TOML documents, -and every type string they use); it needs `pip install pyyaml tomli-w`. The +`crates/weaveffi-cli/tests/fixtures/` (as YAML and JSON documents, and +every type string they use); it needs `pip install pyyaml`. The nightly CI run starts from that corpus. `corpus/`, `artifacts/`, and `coverage/` are generated at runtime and gitignored. diff --git a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_json/minimal.json b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_json/minimal.json index 9638515c..fddec7e4 100644 --- a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_json/minimal.json +++ b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_json/minimal.json @@ -1,5 +1,5 @@ { - "version": "0.11.0", + "version": "0.12.0", "modules": [ { "name": "m", diff --git a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_toml/minimal.toml b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_toml/minimal.toml deleted file mode 100644 index ee785de0..00000000 --- a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_toml/minimal.toml +++ /dev/null @@ -1,8 +0,0 @@ -version = "0.11.0" - -[[modules]] -name = "m" - -[[modules.functions]] -name = "noop" -params = [] diff --git a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_yaml/minimal.yml b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_yaml/minimal.yml index 04bd8781..72565c45 100644 --- a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_yaml/minimal.yml +++ b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_yaml/minimal.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" modules: - name: m functions: diff --git a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_validate/minimal.yml b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_validate/minimal.yml index 58a9d9d6..ee24635a 100644 --- a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_validate/minimal.yml +++ b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_validate/minimal.yml @@ -1,4 +1,4 @@ -version: "0.11.0" +version: "0.12.0" modules: - name: m functions: diff --git a/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs b/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs deleted file mode 100644 index f40aa89c..00000000 --- a/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![cfg_attr(fuzzing, no_main)] -#![allow(unsafe_code)] - -#[cfg(fuzzing)] -use libfuzzer_sys::fuzz_target; -#[cfg(fuzzing)] -use weaveffi_model::parse::parse_api_str; - -#[cfg(fuzzing)] -fuzz_target!(|data: &[u8]| { - if let Ok(s) = std::str::from_utf8(data) { - let _ = parse_api_str(s, "toml"); - } -}); - -#[cfg(not(fuzzing))] -fn main() {} diff --git a/crates/weaveffi-fuzz/seed_corpus.py b/crates/weaveffi-fuzz/seed_corpus.py index 7f2e3abc..8b1fe2f0 100644 --- a/crates/weaveffi-fuzz/seed_corpus.py +++ b/crates/weaveffi-fuzz/seed_corpus.py @@ -9,11 +9,10 @@ - fuzz_parse_yaml, fuzz_validate: each fixture as is - fuzz_parse_json: each fixture converted to JSON -- fuzz_parse_toml: each fixture converted to TOML (needs `tomli-w`) - fuzz_parse_type_ref: every distinct `type` and `return` string - fuzz_value_buffer: the committed seeds only -Needs PyYAML (`pip install pyyaml tomli-w`). +Needs PyYAML (`pip install pyyaml`). """ import json @@ -43,7 +42,7 @@ def type_strings(node, out): class FixtureLoader(yaml.SafeLoader): """Reads a plain `null` as the string "null", as the IDL parser does where it expects a string (`edge_cases` has a parameter named `null`). The - fixtures use no real nulls, which TOML couldn't express anyway.""" + fixtures use no real nulls.""" FixtureLoader.yaml_implicit_resolvers = { @@ -69,11 +68,6 @@ def main(): for fixture in FIXTURES: text = json.dumps(load(fixture), indent=2) (corpus / f"{fixture.stem}.json").write_text(text) - elif target == "fuzz_parse_toml": - import tomli_w - - for fixture in FIXTURES: - (corpus / f"{fixture.stem}.toml").write_text(tomli_w.dumps(load(fixture))) elif target == "fuzz_parse_type_ref": types = set() for fixture in FIXTURES: diff --git a/crates/weaveffi-fuzz/tests/seeds.rs b/crates/weaveffi-fuzz/tests/seeds.rs index 76b11bbc..c7560e29 100644 --- a/crates/weaveffi-fuzz/tests/seeds.rs +++ b/crates/weaveffi-fuzz/tests/seeds.rs @@ -51,13 +51,6 @@ fn parse_json_seed_is_well_formed() { assert_eq!(api.modules.len(), 1); } -#[test] -fn parse_toml_seed_is_well_formed() { - let s = seed("fuzz_parse_toml", "minimal.toml"); - let api = parse_api_str(&s, "toml").expect("seed must parse as TOML"); - assert_eq!(api.modules.len(), 1); -} - #[test] fn parse_type_ref_seed_is_well_formed() { let s = seed("fuzz_parse_type_ref", "minimal.txt"); @@ -91,15 +84,6 @@ fn parse_json_target_does_not_panic_on_garbage() { } } -#[test] -fn parse_toml_target_does_not_panic_on_garbage() { - for data in [&b""[..], b"\xff", b"=", b"version = ["] { - if let Ok(s) = std::str::from_utf8(data) { - let _ = parse_api_str(s, "toml"); - } - } -} - #[test] fn parse_type_ref_target_does_not_panic_on_garbage() { for data in [&b""[..], b"[", b"{string:", b"iter<", b"handle<"] { diff --git a/crates/weaveffi-macros/src/codegen/async_fns.rs b/crates/weaveffi-macros/src/codegen/async_fns.rs index bbbb8aba..47b5116d 100644 --- a/crates/weaveffi-macros/src/codegen/async_fns.rs +++ b/crates/weaveffi-macros/src/codegen/async_fns.rs @@ -1,13 +1,13 @@ //! Thunk emission for `async fn` exports: the completion-callback typedef and //! the launcher. //! -//! This is the producer half of the completion contract stated by -//! [`weaveffi_model::plan::AsyncProtocol`]: the callback fires exactly once, -//! from an arbitrary producer thread, and everything it delivers is *owned by -//! the consumer*. A non-null `err` is heap-boxed and released with -//! `{prefix}_error_free`; a string, bytes, or buffered result is a -//! `(result_ptr, result_len)` run released with `{prefix}_free_bytes`; an -//! object result transfers one strong reference. +//! This is the producer half of the async completion contract (see +//! [`weaveffi_model::plan`]): the callback fires exactly once, from an +//! arbitrary producer thread, and everything it delivers is *owned by the +//! consumer*. A non-null `err` is heap-boxed and released with +//! `{prefix}_error_free`; a string, bytes, typed-array, or buffered result +//! is a `(result_ptr, result_len)` run released with `{prefix}_free_bytes`; +//! an object result transfers one strong reference. //! //! The launcher hands three closures to [`weaveffi::abi::launch_async`]: one //! that lifts the inputs into owned values on the caller's thread (under @@ -20,10 +20,11 @@ use proc_macro2::TokenStream; use quote::quote; use weaveffi_model::model::{AsyncBinding, FnBinding}; +use weaveffi_model::plan::ResultPass; -use super::helpers::{ctype_to_rust, fn_slots, ident, thunk_attrs, CallTarget, UserSig}; -use super::lift::{lift_param, lower_async_result, Lifts}; -use super::sync::throws; +use super::helpers::{ctype_to_rust, fn_slots, ident, slot, thunk_attrs, CallTarget, UserSig}; +use super::lift::{lift_param, lower_async_result, lower_custom_ret, slot_owners, Lifts}; +use super::sync::report_error; /// Generate the completion-callback typedef and the launcher for an /// `async fn`. @@ -34,8 +35,14 @@ pub(crate) fn gen_async_function( target: &CallTarget, prefix: &str, ) -> syn::Result { - let is_object_ret = f.ret.as_ref().is_some_and(|t| t.interface_name().is_some()); - let object = user.ret_object(); + let object = match a.result { + ResultPass::Object { .. } => user.ret_object(), + _ => None, + }; + let result_slot = match &a.result { + ResultPass::Object { result, .. } => Some(result.name.as_str()), + _ => None, + }; let cb_ty = ident(&a.callback_type); let cb_slots: Vec = a .callback_params @@ -43,8 +50,8 @@ pub(crate) fn gen_async_function( .map(|p| { // The result slot of an object return is spelled with the // producer's pointee so `super::T` stays in scope. - match (p.name.as_str(), &object) { - ("result", Some(obj)) if is_object_ret => quote!(*mut #obj), + match (&object, result_slot) { + (Some(obj), Some(r)) if r == p.name => quote!(*mut #obj), _ => ctype_to_rust(&p.ty, prefix), } }) @@ -55,8 +62,8 @@ pub(crate) fn gen_async_function( pub type #cb_ty = extern "C" fn(#(#cb_slots),*); }; - let launch_sym = ident(&a.launch.symbol); - let launch_params = fn_slots(&a.launch.params, &f.params, user, prefix)?; + let launch_sym = ident(&f.abi.symbol); + let launch_params = fn_slots(&f.abi.params, &slot_owners(&f.params), user, prefix)?; // Every input is owned before the future is spawned: the consumer may // free or reuse its arguments (and release its object references) the @@ -71,39 +78,40 @@ pub(crate) fn gen_async_function( // consumer may destroy it at any time) and hands one handle to the // producer, as its final parameter, and one to the runtime, which drops // the future and completes with the cancelled code when it fires. - let (cancel_pre, cancel) = if f.cancellable { - lifts.args.push(quote!(__wv_cancel_arg)); - ( - quote! { - let __wv_cancel = ::weaveffi::abi::CancelToken::from_raw(cancel_token); - let __wv_cancel_arg = ::std::clone::Clone::clone(&__wv_cancel); - }, - quote!(::std::option::Option::Some(__wv_cancel)), - ) - } else { - (TokenStream::new(), quote!(::std::option::Option::None)) + let (cancel_pre, cancel) = match &a.cancel_token { + Some(token) => { + let token = slot(&token.name); + lifts.args.push(quote!(__wv_cancel_arg)); + ( + quote! { + let __wv_cancel = ::weaveffi::abi::CancelToken::from_raw(#token); + let __wv_cancel_arg = ::std::clone::Clone::clone(&__wv_cancel); + }, + quote!(::std::option::Option::Some(__wv_cancel)), + ) + } + None => (TokenStream::new(), quote!(::std::option::Option::None)), }; let lifted = lifts.finish(); let call = target.call(&f.name, &lifts.args); - let output = if throws(f) { - quote! { - #call.await.map_err(|__wv_e| ::weaveffi::abi::FfiError::from_report(&__wv_e)) - } - } else { - quote!(::std::result::Result::Ok(#call.await)) + let output = match report_error(f, user) { + Some(report) => quote!(#call.await.map_err(|__wv_e| #report)), + None => quote!(::std::result::Result::Ok(#call.await)), }; - let lower = match &f.ret { - Some(_) => { - let lowered = lower_async_result(f.ret.as_ref(), object.as_ref()); - quote!(move |__wv_val| #lowered) - } - None => quote!(move |()| ()), + let val = quote!(__wv_val); + let custom = lower_custom_ret(user, &val); + let lowered = lower_async_result(&a.result, object.as_ref()); + let lower = match a.result { + ResultPass::Void => quote!(move |()| ()), + _ => quote!(move |__wv_val| { #custom #lowered }), }; let slot_count = a.callback_params.len() - 2; let slots: Vec = (0..slot_count) .map(|i| ident(&format!("__wv_r{i}"))) .collect(); + let callback = slot("callback"); + let context = slot("context"); let attrs = thunk_attrs(); Ok(quote! { @@ -111,7 +119,7 @@ pub(crate) fn gen_async_function( #attrs pub unsafe extern "C" fn #launch_sym(#(#launch_params),*) { - let __wv_ctx = context as usize; + let __wv_ctx = #context as usize; ::weaveffi::abi::launch_async( move || unsafe { #lifted @@ -120,7 +128,7 @@ pub(crate) fn gen_async_function( }, #lower, move |__wv_err, (#(#slots,)*)| { - callback(__wv_ctx as *mut ::std::ffi::c_void, __wv_err #(, #slots)*) + #callback(__wv_ctx as *mut ::std::ffi::c_void, __wv_err #(, #slots)*) }, ); } diff --git a/crates/weaveffi-macros/src/codegen/callbacks.rs b/crates/weaveffi-macros/src/codegen/callbacks.rs index 19b9745b..0be3f08a 100644 --- a/crates/weaveffi-macros/src/codegen/callbacks.rs +++ b/crates/weaveffi-macros/src/codegen/callbacks.rs @@ -3,73 +3,122 @@ //! `(ctx, vtable)` pair, and the `CallbackInterface` impl that lets thunks //! lift `Arc` parameters. //! -//! This is the producer half of the contract stated by -//! [`weaveffi_model::plan::CallbackProtocol`]: each method lowers its -//! arguments (strings, bytes, and buffers are borrowed for the call as -//! `(ptr, len)`; objects transfer one strong reference the consumer adopts), -//! calls the entry with `(ctx, args…, [out_ptr, out_len,] &mut err)`, adopts -//! whatever the consumer returned, and then checks `err`. Every method -//! returns `Result`, so a consumer failure is an -//! `Err` and nothing unwinds. +//! This is the producer half of the callback contract (see +//! [`weaveffi_model::plan`]): each method lowers its arguments as their +//! [`ArgPass`] names them (strings, bytes, arrays, and buffers are borrowed +//! for the call; objects transfer one strong reference the consumer adopts), +//! calls the entry with `(ctx, args…, [return out slots,] &mut err)`, adopts +//! whatever the consumer returned (as its [`CallbackRetPass`] says), and +//! then checks `err`. A method returns `Result` with +//! `E: From`, so a consumer failure is an `Err` and nothing +//! unwinds; when the method throws a declared domain, the consumer's codes +//! of that domain arrive as its typed variants. +//! +//! A method that returns a value first checks +//! [`ForeignCallback::check_thread`](weaveffi::abi::ForeignCallback::check_thread), +//! so a thread-affine vtable's value-returning methods fail (without being +//! called) off the thread that passed the vtable. use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, quote_spanned}; use syn::spanned::Spanned as _; -use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding}; -use weaveffi_model::ty::{Family, Ty}; +use weaveffi_model::abi::CType; +use weaveffi_model::model::{ + CallbackInterfaceBinding, CallbackMethodBinding, CallbackParamBinding, +}; +use weaveffi_model::plan::{ArgPass, CallbackRetPass, ErrorStrategy}; -use super::helpers::{ident, ret_arrow_for, slot_type_for, UserSig}; +use super::custom::{CustomScope, LiftSite}; +use super::helpers::{ + ctype_to_rust, ident, pointee, ret_arrow_for, slot_type_for, SlotOwners, UserSig, +}; use super::unsupported; +/// The object slots of a callback method's parameters. +fn slot_owners(params: &[CallbackParamBinding]) -> SlotOwners<'_> { + SlotOwners { + objects: params + .iter() + .filter_map(|p| match &p.pass { + ArgPass::Object { slot, .. } => Some((p.name.as_str(), slot.name.as_str())), + _ => None, + }) + .collect(), + vtables: Vec::new(), + } +} + /// Build the lowering for one callback-method argument as `(preamble, C /// arguments)`. The Rust parameter is whatever the producer's trait /// declares; everything but an object is borrowed for the duration of the /// vtable call. fn lower_callback_arg( - pb: &ParamBinding, + pb: &CallbackParamBinding, user: &UserSig<'_>, ) -> syn::Result<(TokenStream, Vec)> { let n = ident(&pb.name); - let is_ref = user.param_is_ref(&pb.name); - let none = TokenStream::new(); - // Borrow the value whether it arrived owned or borrowed. - let borrowed = if is_ref { quote!(#n) } else { quote!(&#n) }; - let ptr = ident(&format!("__wv_{}_ptr", pb.name)); - let len = ident(&format!("__wv_{}_len", pb.name)); - let pair = |slots: TokenStream| { - ( - quote!(let (#ptr, #len) = #slots;), - vec![quote!(#ptr), quote!(#len)], - ) + let mut pre = TokenStream::new(); + // Borrow the value whether it arrived owned or borrowed. A value whose + // type mentions a custom type is first converted to its repr. + let (borrowed, is_slice) = match user.param_shape(&pb.name) { + Some(shape) => { + let repr = ident(&format!("__wv_{}_repr", pb.name)); + let source = if user.param_is_ref(&pb.name) { + quote!(#n) + } else { + quote!(&#n) + }; + let converted = shape.lower(source); + pre.extend(quote!(let #repr = #converted;)); + (quote!(&#repr), false) + } + None if user.param_is_ref(&pb.name) => (quote!(#n), user.param_is_slice(&pb.name)), + None => (quote!(&#n), false), }; - Ok(match pb.ty.family() { - Family::Direct => match &pb.ty { - Ty::Enum(_) => ( - none, - vec![quote!(::weaveffi::abi::CEnum::to_i32(#borrowed))], - ), - _ => (none, vec![if is_ref { quote!(*#n) } else { quote!(#n) }]), - }, - Family::String => pair(quote!(::weaveffi::abi::str_slots(&#n))), - Family::Bytes => pair(quote!(::weaveffi::abi::byte_slots(&#n))), + let tmp = |suffix: &str| ident(&format!("__wv_{}_{suffix}", pb.name)); + let args = match &pb.pass { + ArgPass::Direct { .. } => vec![quote!(::weaveffi::abi::Scalar::to_abi(#borrowed))], + ArgPass::OptDirect { .. } => { + let (has, value) = (tmp("has"), tmp("value")); + pre.extend(quote! { + let (#has, #value) = + ::weaveffi::abi::opt_slots(::std::option::Option::as_ref(#borrowed)); + }); + vec![quote!(#has), quote!(#value)] + } + ArgPass::Slice { .. } => { + let arr = tmp("arr"); + pre.extend(quote! { + let #arr = ::weaveffi::abi::Scalar::abi_slice(&(#borrowed)[..]); + }); + vec![quote!(#arr.as_ptr()), quote!(#arr.len())] + } + ArgPass::String { .. } => { + let text = tmp("text"); + pre.extend(quote!(let #text = ::weaveffi::abi::Text::as_text(#borrowed);)); + vec![quote!(#text.as_ptr()), quote!(#text.len())] + } + ArgPass::Bytes { .. } => { + let (ptr, len) = (tmp("ptr"), tmp("len")); + pre.extend(quote!(let (#ptr, #len) = ::weaveffi::abi::byte_slots(#borrowed);)); + vec![quote!(#ptr), quote!(#len)] + } // A buffered argument is encoded into a local value buffer the // consumer borrows for the call. - Family::Buffer => { - let tmp = ident(&format!("__wv_{}_buf", pb.name)); - let value = if user.param_is_slice(&pb.name) { - quote!(&<[_]>::to_vec(#n)) + ArgPass::Buffer { .. } => { + let buf = tmp("buf"); + let value = if is_slice { + quote!(&<[_]>::to_vec(#borrowed)) } else { borrowed }; - ( - quote!(let #tmp = ::weaveffi::abi::encode_value(#value);), - vec![quote!(#tmp.as_ptr()), quote!(#tmp.len())], - ) + pre.extend(quote!(let #buf = ::weaveffi::abi::encode_value(#value);)); + vec![quote!(#buf.as_ptr()), quote!(#buf.len())] } // An object transfers one strong reference the consumer adopts (and // eventually `_destroy`s). The producer must hold it as an `Arc`, // since only an `Arc` allocation can carry a reference count. - Family::Object { nullable } => { + ArgPass::Object { nullable, .. } => { if !user.param_wants_arc(&pb.name) { return Err(unsupported( user.param_span(&pb.name), @@ -79,73 +128,108 @@ fn lower_callback_arg( )); } let obj = user.param_object(&pb.name); - let arg = if nullable { + vec![if *nullable { quote!(::weaveffi::abi::lower_object_opt::<#obj>(::std::clone::Clone::clone(#borrowed))) } else { quote!(::weaveffi::abi::lower_object::<#obj>(::std::sync::Arc::clone(#borrowed))) - }; - (none, vec![arg]) + }] } - Family::Callback { .. } | Family::Iterator => { + ArgPass::Callback { .. } => { return Err(unsupported( user.param_span(&pb.name), &pb.name, "callback-interface parameter type", )) } - }) + }; + Ok((pre, args)) } -/// The statements adopting the vtable entry's return (`__wv_ret`, or the -/// `out_ptr`/`out_len` run) as `__wv_value: Result`. -fn adopt_callback_ret(ret: Option<&Ty>, user: &UserSig<'_>) -> TokenStream { - let Some(ty) = ret else { - return quote!(let __wv_value = ::std::result::Result::Ok(__wv_ret);); - }; - let value = match ty.family() { - Family::Direct => match ty { - Ty::Enum(_) => { - let et = user.ret_object().unwrap_or_else(|| quote!(_)); - quote!(::weaveffi::abi::callback_ret_enum::<#et>(__wv_ret)) - } - _ => quote!(::std::result::Result::Ok(__wv_ret)), - }, - Family::Object { nullable: false } => { - quote!(unsafe { ::weaveffi::abi::callback_ret_object(__wv_ret) }) - } - Family::Object { nullable: true } => { - quote!(::std::result::Result::Ok(unsafe { - ::weaveffi::abi::callback_ret_object_opt(__wv_ret) - })) - } - Family::String => { - quote!(unsafe { ::weaveffi::abi::callback_ret_string(__wv_out_ptr, __wv_out_len) }) - } - Family::Bytes => { - quote!(unsafe { ::weaveffi::abi::callback_ret_bytes(__wv_out_ptr, __wv_out_len) }) +/// The out-slot locals a method's return needs (declared before the call), +/// the C arguments pointing at them, and the expression adopting the return +/// (`__wv_ret` plus the out slots) as a `Result<_, ForeignError>`. +fn callback_ret( + m: &CallbackMethodBinding, + prefix: &str, +) -> (TokenStream, Vec, TokenStream) { + let local = |name: &str| ident(&format!("__wv_{name}")); + match &m.ret_pass { + CallbackRetPass::Void => ( + TokenStream::new(), + vec![], + quote!(::std::result::Result::Ok(__wv_ret)), + ), + CallbackRetPass::Direct => ( + TokenStream::new(), + vec![], + quote!(::weaveffi::abi::callback_ret_scalar(__wv_ret)), + ), + CallbackRetPass::OptDirect { out_value } => { + let v = local(&out_value.name); + let ty = ctype_to_rust(pointee(&out_value.ty), prefix); + ( + quote!(let mut #v: #ty = ::weaveffi::abi::Sentinel::sentinel();), + vec![quote!(&mut #v)], + quote!(::weaveffi::abi::callback_ret_opt(__wv_ret, #v)), + ) } - Family::Buffer => { - let ty = user.ret_value_type().unwrap_or_else(|| quote!(_)); - quote!(unsafe { - ::weaveffi::abi::callback_ret_buffer::<#ty>(__wv_out_ptr, __wv_out_len) - }) + CallbackRetPass::Slice { + out_ptr, out_len, .. + } => { + let (p, l) = (local(&out_ptr.name), local(&out_len.name)); + let ty = ctype_to_rust(pointee(&out_ptr.ty), prefix); + ( + quote! { + let mut #p: #ty = ::std::ptr::null_mut(); + let mut #l: usize = 0; + }, + vec![quote!(&mut #p), quote!(&mut #l)], + quote!(unsafe { ::weaveffi::abi::callback_ret_slice(#p, #l) }), + ) } - Family::Callback { .. } | Family::Iterator => { - unreachable!("validation never admits {ty} as a callback return") + CallbackRetPass::String { out_ptr, out_len } + | CallbackRetPass::Bytes { out_ptr, out_len } + | CallbackRetPass::Buffer { out_ptr, out_len } => { + let (p, l) = (local(&out_ptr.name), local(&out_len.name)); + let f = match &m.ret_pass { + CallbackRetPass::String { .. } => quote!(callback_ret_text), + CallbackRetPass::Bytes { .. } => quote!(callback_ret_bytes), + _ => quote!(callback_ret_buffer), + }; + ( + quote! { + let mut #p: *mut u8 = ::std::ptr::null_mut(); + let mut #l: usize = 0; + }, + vec![quote!(&mut #p), quote!(&mut #l)], + quote!(unsafe { ::weaveffi::abi::#f(#p, #l) }), + ) } - }; - quote!(let __wv_value = #value;) + CallbackRetPass::Object { + nullable: false, .. + } => ( + TokenStream::new(), + vec![], + quote!(unsafe { ::weaveffi::abi::callback_ret_object(__wv_ret) }), + ), + CallbackRetPass::Object { nullable: true, .. } => ( + TokenStream::new(), + vec![], + quote!(::std::result::Result::Ok(unsafe { + ::weaveffi::abi::callback_ret_object_opt(__wv_ret) + })), + ), + } } -/// Emit one method of the foreign wrapper's trait impl. `domain` is the path -/// of the error domain in scope, which a `throws` method's failures are -/// checked against. +/// Emit one method of the foreign wrapper's trait impl. fn gen_foreign_method( m: &CallbackMethodBinding, sig: &syn::Signature, - domain: Option<&TokenStream>, + customs: CustomScope<'_>, + prefix: &str, ) -> syn::Result { - let user = UserSig::new(sig, None); + let user = UserSig::new(sig, None, customs); let field = ident(&m.name); let mut pre = TokenStream::new(); let mut c_args: Vec = Vec::new(); @@ -154,35 +238,53 @@ fn gen_foreign_method( pre.extend(p); c_args.extend(args); } - let run = m - .ret - .as_ref() - .is_some_and(|t| matches!(t.family(), Family::String | Family::Bytes | Family::Buffer)); - if run { - pre.extend(quote! { - let mut __wv_out_ptr: *mut u8 = ::std::ptr::null_mut(); - let mut __wv_out_len: usize = 0; - }); - c_args.push(quote!(&mut __wv_out_ptr)); - c_args.push(quote!(&mut __wv_out_len)); - } - let adopt = adopt_callback_ret(m.ret.as_ref(), &user); - let status = match (m.throws, domain) { - (true, Some(domain)) => { - quote!(::weaveffi::abi::callback_status_in::<#domain>(&__wv_err)?;) + let (ret_pre, ret_args, adopt) = callback_ret(m, prefix); + pre.extend(ret_pre); + c_args.extend(ret_args); + + let (err_ty, err_span) = user + .ret_error_spelled() + .unwrap_or_else(|| (quote!(::weaveffi::ForeignError), sig.span())); + let status = match &m.error { + ErrorStrategy::Domain(_) => quote_spanned! {err_span=> + ::weaveffi::abi::callback_status_in::<#err_ty>(&__wv_err)?; + }, + ErrorStrategy::Untyped | ErrorStrategy::Trap => quote_spanned! {err_span=> + ::weaveffi::abi::callback_status::<#err_ty>(&__wv_err)?; + }, + }; + let lowered_ty = user.ret_lowered_type().unwrap_or_else(|| quote!(())); + let custom = match user.ret_shape() { + Some(shape) => { + let convert = shape.lift(quote!(__wv_repr), LiftSite::Returned); + quote!(let __wv_value = __wv_value.and_then(|__wv_repr| #convert);) } - (true, None) => { - return Err(syn::Error::new( - sig.span(), - "weaveffi: a #[weaveffi::throws] callback method needs a #[weaveffi::error] \ - domain in this module or a parent module", - )) + None => TokenStream::new(), + }; + let returns_value = !matches!(m.abi.ret, CType::Void) || !m.ret_pass.out_slots().is_empty(); + // Every conversion into the producer's error type is spanned on it, so + // a missing `From` is reported there. + let from = quote_spanned!(err_span=> ::weaveffi::abi::convert_foreign::<#err_ty>); + let thread_check = if returns_value { + quote! { + if let ::std::result::Result::Err(__wv_e) = self.0.check_thread() { + return ::std::result::Result::Err(#from(__wv_e)); + } } - (false, _) => quote!(::weaveffi::abi::callback_status(&__wv_err)?;), + } else { + TokenStream::new() }; Ok(quote! { - #[allow(unsafe_code, unused_variables, unused_mut, clippy::let_unit_value, clippy::unit_arg)] + #[allow( + unsafe_code, + unused_variables, + unused_mut, + clippy::let_unit_value, + clippy::unit_arg, + clippy::useless_conversion + )] #sig { + #thread_check #pre let mut __wv_err = ::weaveffi::abi::FfiError::default(); // SAFETY: the vtable and `ctx` are live for the life of the @@ -192,9 +294,10 @@ fn gen_foreign_method( }; // The consumer's return is adopted (and released on failure) // before its status is checked. - #adopt + let __wv_value: ::std::result::Result<#lowered_ty, ::weaveffi::ForeignError> = #adopt; #status - __wv_value + #custom + __wv_value.map_err(|__wv_e| #from(__wv_e)) } }) } @@ -214,7 +317,7 @@ fn gen_foreign_method( pub(crate) fn gen_callback_interface( cb: &CallbackInterfaceBinding, item: &syn::ItemTrait, - domain: Option<&TokenStream>, + customs: CustomScope<'_>, prefix: &str, ) -> syn::Result { let vt = ident(&cb.vtable_tag); @@ -240,16 +343,19 @@ pub(crate) fn gen_callback_interface( ), ) })?; - let user = UserSig::new(sig, None); + let user = UserSig::new(sig, None, customs); let field = ident(&m.name); + let owners = slot_owners(&m.params); let slots = m - .abi_params + .abi + .params .iter() - .map(|p| slot_type_for(p, &m.params, &user, prefix)) + .map(|p| slot_type_for(p, &owners, &user, prefix)) .collect::>>()?; - let arrow = ret_arrow_for(&m.abi_ret, m.ret.as_ref(), &user, prefix); + let object_ret = matches!(m.ret_pass, CallbackRetPass::Object { .. }); + let arrow = ret_arrow_for(&m.abi.ret, object_ret, &user, prefix); fields.push(quote!(pub #field: unsafe extern "C" fn(#(#slots),*) #arrow,)); - methods.push(gen_foreign_method(m, sig, domain)?); + methods.push(gen_foreign_method(m, sig, customs, prefix)?); } let vtable_doc = format!( diff --git a/crates/weaveffi-macros/src/codegen/contract.rs b/crates/weaveffi-macros/src/codegen/contract.rs index 96f4ad18..a2de303a 100644 --- a/crates/weaveffi-macros/src/codegen/contract.rs +++ b/crates/weaveffi-macros/src/codegen/contract.rs @@ -1,15 +1,17 @@ //! The `{prefix}_{module}_contract` export of a top-level module. //! //! The entries come from [`weaveffi_model::contract::entries`] over the -//! validated model, exactly as the CLI computes them for the bindings. Each +//! validated model, exactly as the CLI computes them for the bindings: one +//! per declaration, plus one per error code and one per callback-interface +//! method. Each //! entry carries the `#[cfg]` of its declaration and of every module and //! interface around it; the table is compacted in constant context to the //! entries whose `cfg!` holds, so it lists exactly what this build exports. +use crate::extract::SourceMap; use proc_macro2::TokenStream; use quote::quote; use weaveffi_model::model::{contract_symbol, Model, ModuleBinding}; -use weaveffi_model::rust::SourceMap; use super::helpers::ident; @@ -52,12 +54,12 @@ pub(crate) fn gen_contract( #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #sym(out_len: *mut usize) -> *const ::weaveffi::abi::ContractEntry { + pub unsafe extern "C" fn #sym(__wv_out_len: *mut usize) -> *const ::weaveffi::abi::ContractEntry { const __WV_ALL: [(bool, ::weaveffi::abi::ContractEntry); #n] = [#(#entries),*]; const __WV_LEN: usize = ::weaveffi::abi::contract_len(&__WV_ALL); static __WV_TABLE: [::weaveffi::abi::ContractEntry; __WV_LEN] = ::weaveffi::abi::contract_compact(__WV_ALL); - unsafe { ::weaveffi::abi::contract_table(&__WV_TABLE, out_len) } + unsafe { ::weaveffi::abi::contract_table(&__WV_TABLE, __wv_out_len) } } } } diff --git a/crates/weaveffi-macros/src/codegen/custom.rs b/crates/weaveffi-macros/src/codegen/custom.rs new file mode 100644 index 00000000..5727896e --- /dev/null +++ b/crates/weaveffi-macros/src/codegen/custom.rs @@ -0,0 +1,295 @@ +//! Custom types: the hidden `weaveffi::abi::Custom` impl each +//! `#[weaveffi::custom]` alias gets, and the converters that turn a value +//! whose type mentions custom types into its repr and back. +//! +//! A custom type crosses as its repr, so the thunks lift and lower the repr +//! with the ordinary per-family machinery and convert at the boundary. A +//! converter follows the written type's shape (`Option`, `Vec` or a slice, +//! `HashMap` or `BTreeMap`) down to the custom types inside it, so +//! `Vec>` converts element by element. + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; + +use crate::extract::{CustomDef, Customs}; + +use super::helpers::ident; + +/// The name of the hidden marker type implementing `Custom` for the custom +/// type `name`. +fn marker_name(name: &str) -> syn::Ident { + ident(&format!("__WvCustom_{name}")) +} + +/// The hidden `Custom` impl for one custom type, emitted in the module that +/// declares it. +pub(crate) fn gen_custom(def: &CustomDef) -> TokenStream { + let marker = marker_name(&def.name); + let name = ident(&def.name); + let (repr, lift, lower) = (&def.repr, &def.lift, &def.lower); + quote! { + #[doc(hidden)] + #[allow(non_camel_case_types)] + pub struct #marker; + + impl ::weaveffi::abi::Custom for #marker { + type Repr = #repr; + type Value = #name; + fn lift( + __wv_repr: Self::Repr, + ) -> ::std::result::Result { + (#lift)(__wv_repr).map_err(|__wv_e| ::std::string::ToString::to_string(&__wv_e)) + } + fn lower(__wv_value: &Self::Value) -> Self::Repr { + (#lower)(__wv_value) + } + } + } +} + +/// The tree's custom types as seen from one module, which knows the +/// relative path to each one's marker type. +#[derive(Clone, Copy)] +pub(crate) struct CustomScope<'a> { + customs: &'a Customs, + here: &'a [String], +} + +impl<'a> CustomScope<'a> { + pub(crate) fn new(customs: &'a Customs, here: &'a [String]) -> Self { + Self { customs, here } + } + + /// The path from this module to the marker type of `def`. + fn marker_path(&self, def: &CustomDef) -> TokenStream { + let common = self + .here + .iter() + .zip(&def.module) + .take_while(|(a, b)| a == b) + .count(); + let ups = std::iter::repeat_n(quote!(super::), self.here.len() - common); + let downs = def.module[common..].iter().map(|m| { + let m = ident(m); + quote!(#m::) + }); + let marker = marker_name(&def.name); + quote!(#(#ups)* #(#downs)* #marker) + } + + /// The shape of `ty` when it mentions a custom type, else `None`. + pub(crate) fn shape(&self, ty: &syn::Type) -> Option { + if self.customs.is_empty() { + return None; + } + let shape = self.shape_of(ty); + shape.has_custom().then_some(shape) + } + + fn shape_of(&self, ty: &syn::Type) -> Shape { + let plain = || Shape::Plain(ty.to_token_stream()); + match ty { + syn::Type::Reference(r) => match r.elem.as_ref() { + syn::Type::Slice(s) => Shape::Vec(Box::new(self.shape_of(&s.elem))), + other => self.shape_of(other), + }, + syn::Type::Paren(p) => self.shape_of(&p.elem), + syn::Type::Path(p) if p.qself.is_none() => { + let Some(seg) = p.path.segments.last() else { + return plain(); + }; + let name = seg.ident.to_string(); + if let (Some(def), syn::PathArguments::None) = + (self.customs.get(&name), &seg.arguments) + { + return Shape::Custom(self.marker_path(def)); + } + let args: Vec<&syn::Type> = match &seg.arguments { + syn::PathArguments::AngleBracketed(a) => a + .args + .iter() + .filter_map(|g| match g { + syn::GenericArgument::Type(t) => Some(t), + _ => None, + }) + .collect(), + _ => Vec::new(), + }; + match (name.as_str(), args.as_slice()) { + ("Option", [inner]) => Shape::Option(Box::new(self.shape_of(inner))), + ("Vec", [inner]) => Shape::Vec(Box::new(self.shape_of(inner))), + ("HashMap" | "BTreeMap", [k, v]) => { + let mut path = p.path.clone(); + if let Some(last) = path.segments.last_mut() { + last.arguments = syn::PathArguments::None; + } + Shape::Map( + path.to_token_stream(), + Box::new(self.shape_of(k)), + Box::new(self.shape_of(v)), + ) + } + _ => plain(), + } + } + _ => plain(), + } + } +} + +/// How a written type maps onto its repr: identity, a custom type, or a +/// container of shapes. +#[derive(Clone)] +pub(crate) enum Shape { + /// No custom type inside: the type as written. + Plain(TokenStream), + /// A custom type, by the path to its marker type. + Custom(TokenStream), + /// `Option`. + Option(Box), + /// `Vec` or `&[T]`. + Vec(Box), + /// A map, by its path without generics. + Map(TokenStream, Box, Box), +} + +/// How a custom type's `lift` failure is reported where a converter runs. +#[derive(Clone, Copy)] +pub(crate) enum LiftSite<'a> { + /// A parameter (or async parameter): a marshalling `FfiError` naming + /// it. + Param(&'a syn::LitStr), + /// A field read out of a value buffer: a `BufferDecodeError`. + Buffer, + /// A callback method's return: a marshalling `ForeignError`. + Returned, +} + +impl Shape { + fn has_custom(&self) -> bool { + match self { + Shape::Plain(_) => false, + Shape::Custom(_) => true, + Shape::Option(s) | Shape::Vec(s) => s.has_custom(), + Shape::Map(_, k, v) => k.has_custom() || v.has_custom(), + } + } + + /// The owned repr type (every custom type replaced by its repr, every + /// slice by a `Vec`). + pub(crate) fn repr_ty(&self) -> TokenStream { + match self { + Shape::Plain(t) => t.clone(), + Shape::Custom(m) => quote!(<#m as ::weaveffi::abi::Custom>::Repr), + Shape::Option(s) => { + let inner = s.repr_ty(); + quote!(::std::option::Option<#inner>) + } + Shape::Vec(s) => { + let inner = s.repr_ty(); + quote!(::std::vec::Vec<#inner>) + } + Shape::Map(path, k, v) => { + let (k, v) = (k.repr_ty(), v.repr_ty()); + quote!(#path<#k, #v>) + } + } + } + + /// An expression converting the owned repr `value` into the producer's + /// type, as a `Result` whose error `site` decides. + pub(crate) fn lift(&self, value: TokenStream, site: LiftSite<'_>) -> TokenStream { + self.lift_at(value, site, 0) + } + + fn lift_at(&self, value: TokenStream, site: LiftSite<'_>, depth: usize) -> TokenStream { + let x = format_ident!("__wv_x{}", depth); + match self { + Shape::Plain(_) => quote!(::std::result::Result::Ok(#value)), + Shape::Custom(m) => match site { + LiftSite::Param(lit) => { + quote!(::weaveffi::abi::lift_custom_param::<#m>(#value, #lit)) + } + LiftSite::Buffer => quote!(::weaveffi::abi::lift_custom_buffered::<#m>(#value)), + LiftSite::Returned => { + quote!(::weaveffi::abi::lift_custom_returned::<#m>(#value)) + } + }, + Shape::Option(s) => { + let inner = s.lift_at(quote!(#x), site, depth + 1); + quote! { + match #value { + ::std::option::Option::Some(#x) => { + (#inner).map(::std::option::Option::Some) + } + ::std::option::Option::None => { + ::std::result::Result::Ok(::std::option::Option::None) + } + } + } + } + Shape::Vec(s) => { + let inner = s.lift_at(quote!(#x), site, depth + 1); + quote! { + ::std::iter::IntoIterator::into_iter(#value) + .map(|#x| #inner) + .collect::<::std::result::Result<::std::vec::Vec<_>, _>>() + } + } + Shape::Map(path, k, v) => { + let (kx, vx) = ( + format_ident!("__wv_k{}", depth), + format_ident!("__wv_v{}", depth), + ); + let (kl, vl) = ( + format_ident!("__wv_kl{}", depth), + format_ident!("__wv_vl{}", depth), + ); + let key = k.lift_at(quote!(#kx), site, depth + 1); + let val = v.lift_at(quote!(#vx), site, depth + 1); + quote! { + ::std::iter::IntoIterator::into_iter(#value) + .map(|(#kx, #vx)| { + (#key).and_then(|#kl| (#val).map(|#vl| (#kl, #vl))) + }) + .collect::<::std::result::Result<#path<_, _>, _>>() + } + } + } + } + + /// An expression converting `value`, a reference to the producer's + /// type, into the owned repr. + pub(crate) fn lower(&self, value: TokenStream) -> TokenStream { + self.lower_at(value, 0) + } + + fn lower_at(&self, value: TokenStream, depth: usize) -> TokenStream { + let x = format_ident!("__wv_x{}", depth); + match self { + Shape::Plain(_) => quote!(::std::clone::Clone::clone(#value)), + Shape::Custom(m) => quote!(<#m as ::weaveffi::abi::Custom>::lower(#value)), + Shape::Option(s) => { + let inner = s.lower_at(quote!(#x), depth + 1); + quote!(::std::option::Option::as_ref(#value).map(|#x| #inner)) + } + Shape::Vec(s) => { + let inner = s.lower_at(quote!(#x), depth + 1); + quote! { + (#value).iter().map(|#x| #inner).collect::<::std::vec::Vec<_>>() + } + } + Shape::Map(path, k, v) => { + let (kx, vx) = ( + format_ident!("__wv_k{}", depth), + format_ident!("__wv_v{}", depth), + ); + let key = k.lower_at(quote!(#kx), depth + 1); + let val = v.lower_at(quote!(#vx), depth + 1); + quote! { + (#value).iter().map(|(#kx, #vx)| (#key, #val)).collect::<#path<_, _>>() + } + } + } + } +} diff --git a/crates/weaveffi-macros/src/codegen/diagnostics.rs b/crates/weaveffi-macros/src/codegen/diagnostics.rs index 1cc0d69f..3a4e3dfa 100644 --- a/crates/weaveffi-macros/src/codegen/diagnostics.rs +++ b/crates/weaveffi-macros/src/codegen/diagnostics.rs @@ -6,8 +6,8 @@ //! extraction's [`SourceMap`] records the span of every such path, so the //! error lands on the item, member, or written type at fault. +use crate::extract::SourceMap; use proc_macro2::Span; -use weaveffi_model::rust::SourceMap; use weaveffi_model::validate::{Found, ValidationError}; /// What to look for under an error's declaration path. @@ -109,11 +109,8 @@ fn hint(error: &ValidationError) -> Option<&'static str> { use ValidationError as E; Some(match error { E::UnsupportedPrimitive { .. } => { - "use `u64` or `i64` for sizes and counts (their width is the same on every \ - platform), or `String` for a `char`" - } - E::ThrowsWithoutErrorDomain { .. } => { - "declare a #[weaveffi::error] enum in this module or a parent module" + "use `u64`, `i64`, `usize`, or `isize` (which cross as `u64` and `i64`), or a \ + #[weaveffi::custom] type with a `String` repr for wider integers" } _ => return None, }) diff --git a/crates/weaveffi-macros/src/codegen/enums.rs b/crates/weaveffi-macros/src/codegen/enums.rs index 04a9d333..80232f58 100644 --- a/crates/weaveffi-macros/src/codegen/enums.rs +++ b/crates/weaveffi-macros/src/codegen/enums.rs @@ -1,4 +1,4 @@ -//! Codegen for enums: the [`weaveffi::abi::CEnum`] implementation of a +//! Codegen for enums: the [`weaveffi::abi::Scalar`] implementation of a //! C-style enum, and the [`weaveffi::abi::BufferValue`] implementation of //! every enum. //! @@ -12,14 +12,20 @@ use proc_macro2::TokenStream; use quote::quote; use weaveffi_model::model::EnumBinding; +use super::custom::CustomScope; use super::helpers::ident; +use super::records::{field_codec, field_type, FieldCodec}; -/// Generate the surface for one enum: `CEnum` plus a `BufferValue` impl for +/// Generate the surface for one enum: `Scalar` plus a `BufferValue` impl for /// a C-style enum, or the tag-and-fields `BufferValue` impl for a rich /// (algebraic) enum. -pub(crate) fn gen_enum(e: &EnumBinding) -> TokenStream { +pub(crate) fn gen_enum( + e: &EnumBinding, + item: Option<&syn::ItemEnum>, + customs: CustomScope<'_>, +) -> TokenStream { if e.is_rich() { - return gen_rich_enum(e); + return gen_rich_enum(e, item, customs); } let ty = ident(&e.name); let from_arms = e.variants.iter().map(|v| { @@ -33,14 +39,15 @@ pub(crate) fn gen_enum(e: &EnumBinding) -> TokenStream { quote!(Self::#vident => #value,) }); quote! { - impl ::weaveffi::abi::CEnum for #ty { - fn from_i32(value: i32) -> ::std::option::Option { - match value { + impl ::weaveffi::abi::Scalar for #ty { + type Abi = i32; + fn from_abi(__wv_value: i32) -> ::std::option::Option { + match __wv_value { #(#from_arms)* _ => ::std::option::Option::None, } } - fn to_i32(&self) -> i32 { + fn to_abi(&self) -> i32 { match self { #(#to_arms)* } @@ -64,54 +71,80 @@ pub(crate) fn gen_enum(e: &EnumBinding) -> TokenStream { } } +/// The codecs of a variant's fields, reached through the bindings its +/// pattern introduces (each a `&T`). +fn variant_codecs( + fields: &[weaveffi_model::model::FieldBinding], + written: Option<&syn::Fields>, + customs: CustomScope<'_>, +) -> Vec { + fields + .iter() + .map(|f| { + let binding = field_local(&f.name); + field_codec("e!(#binding), field_type(written, &f.name), customs) + }) + .collect() +} + +/// The local a variant's field `name` binds to in a generated pattern: +/// `__wv_f_{name}`, so a constant the producer declared with the field's +/// name can't turn the binding into a constant pattern. +pub(crate) fn field_local(name: &str) -> syn::Ident { + quote::format_ident!("__wv_f_{}", name) +} + +/// The written fields of the variant `name` of `item`. +pub(crate) fn variant_fields<'a>( + item: Option<&'a syn::ItemEnum>, + name: &str, +) -> Option<&'a syn::Fields> { + item? + .variants + .iter() + .find(|v| v.ident == name) + .map(|v| &v.fields) +} + /// Generate the `BufferValue` impl for a rich (algebraic) enum: the write /// side emits the active variant's tag then its fields in declaration order; /// the read side dispatches on the tag and reconstructs the variant. -fn gen_rich_enum(e: &EnumBinding) -> TokenStream { +fn gen_rich_enum( + e: &EnumBinding, + item: Option<&syn::ItemEnum>, + customs: CustomScope<'_>, +) -> TokenStream { let ty = ident(&e.name); - let len_arms = e.variants.iter().map(|v| { - let vident = ident(&v.name); - if v.fields.is_empty() { - quote!(Self::#vident => 4,) - } else { - let bindings: Vec = v.fields.iter().map(|f| ident(&f.name)).collect(); - quote! { - Self::#vident { #(#bindings),* } => { - 4 #(+ ::weaveffi::abi::BufferValue::encoded_len(#bindings))* - } - } - } - }); - let write_arms = e.variants.iter().map(|v| { + let mut len_arms = Vec::new(); + let mut write_arms = Vec::new(); + let mut read_arms = Vec::new(); + for v in &e.variants { let value = v.value; let vident = ident(&v.name); let bindings: Vec = v.fields.iter().map(|f| ident(&f.name)).collect(); + let locals: Vec = v.fields.iter().map(|f| field_local(&f.name)).collect(); + let codecs = variant_codecs(&v.fields, variant_fields(item, &v.name), customs); let pattern = if bindings.is_empty() { quote!(Self::#vident) } else { - quote!(Self::#vident { #(#bindings),* }) + quote!(Self::#vident { #(#bindings: #locals),* }) }; - quote! { + let lens = codecs.iter().map(|c| &c.len); + let writes = codecs.iter().map(|c| &c.write); + let reads = codecs.iter().map(|c| &c.read); + len_arms.push(quote!(#pattern => 4 #(+ #lens)*,)); + write_arms.push(quote! { #pattern => { __wv_w.write_i32(#value); - #(::weaveffi::abi::BufferValue::write_value(#bindings, __wv_w);)* + #(#writes)* } - } - }); - let read_arms = e.variants.iter().map(|v| { - let value = v.value; - let vident = ident(&v.name); - if v.fields.is_empty() { + }); + read_arms.push(if bindings.is_empty() { quote!(#value => Self::#vident,) } else { - let names: Vec = v.fields.iter().map(|f| ident(&f.name)).collect(); - quote! { - #value => Self::#vident { - #(#names: ::weaveffi::abi::BufferValue::read_value(__wv_r)?),* - }, - } - } - }); + quote!(#value => Self::#vident { #(#bindings: #reads),* },) + }); + } quote! { #[allow(unsafe_code, unused_unsafe)] diff --git a/crates/weaveffi-macros/src/codegen/errors.rs b/crates/weaveffi-macros/src/codegen/errors.rs new file mode 100644 index 00000000..29b702d5 --- /dev/null +++ b/crates/weaveffi-macros/src/codegen/errors.rs @@ -0,0 +1,218 @@ +//! Codegen for error domains: the [`weaveffi::abi::ErrorDomain`] +//! implementation of each `#[weaveffi::error]` enum, and (unless the enum +//! says `no_display`) its `Display` and `std::error::Error`. +//! +//! The domain maps each variant to its declared code, serializes a +//! variant's fields into the error's value-buffer payload, and decodes a +//! code (and payload) back into the variant, which is how a callback method +//! that throws the domain receives the consumer's typed error. Its message +//! is its `Display` output: the variant's `#[weaveffi(message = "...")]` +//! template, which may name the variant's fields in braces (`"key not +//! found: {key}"`), else its documented default message. + +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use weaveffi_model::model::ErrorBinding; + +use super::custom::CustomScope; +use super::enums::{field_local, variant_fields}; +use super::helpers::ident; +use super::records::{field_codec, field_type}; +use crate::extract::{default_message, error_no_display, variant_message}; + +/// Generate the error-domain surface of one `#[weaveffi::error]` enum. +/// +/// # Errors +/// +/// Returns an error for a malformed `#[weaveffi(...)]` or +/// `#[weaveffi::error(...)]` attribute. +pub(crate) fn gen_error_domain( + eb: &ErrorBinding, + item: &syn::ItemEnum, + customs: CustomScope<'_>, +) -> syn::Result { + let ty = ident(&eb.name); + let user_ty = &item.ident; + let pattern = |c: &weaveffi_model::model::ErrorCodeBinding| { + let v = ident(&c.name); + if c.fields.is_empty() { + quote!(Self::#v) + } else { + quote!(Self::#v { .. }) + } + }; + let code_arms = eb.codes.iter().map(|c| { + let pat = pattern(c); + let value = c.value; + quote!(#pat => #value,) + }); + let mut payload_arms = Vec::new(); + let mut read_arms = Vec::new(); + for c in &eb.codes { + let v = ident(&c.name); + let value = c.value; + let written = variant_fields(Some(item), &c.name); + if c.fields.is_empty() { + read_arms.push(quote!(#value => Self::#v,)); + continue; + } + let bindings: Vec = c.fields.iter().map(|f| ident(&f.name)).collect(); + let locals: Vec = c.fields.iter().map(|f| field_local(&f.name)).collect(); + let codecs: Vec<_> = c + .fields + .iter() + .zip(&locals) + .map(|(f, b)| field_codec("e!(#b), field_type(written, &f.name), customs)) + .collect(); + let lens = codecs.iter().map(|c| &c.len); + let writes = codecs.iter().map(|c| &c.write); + let reads = codecs.iter().map(|c| &c.read); + payload_arms.push(quote! { + Self::#v { #(#bindings: #locals),* } => { + let mut __wv_buf = + ::weaveffi::abi::BufferWriter::with_capacity(0 #(+ #lens)*); + let __wv_w = &mut __wv_buf; + #(#writes)* + __wv_buf.finish() + } + }); + read_arms.push(quote!(#value => Self::#v { #(#bindings: #reads),* },)); + } + let payload_fn = if payload_arms.is_empty() { + TokenStream::new() + } else { + quote! { + fn payload(&self) -> ::std::vec::Vec { + #[allow(unreachable_patterns)] + match self { + #(#payload_arms)* + _ => ::std::vec::Vec::new(), + } + } + } + }; + + // Spanned at the enum so a `no_display` enum without a `Display` impl + // gets one error that points at it. + let domain_impl = quote_spanned! {user_ty.span()=> + #[allow(unsafe_code, unused_unsafe)] + impl ::weaveffi::abi::ErrorDomain for #user_ty + }; + let mut out = quote! { + #domain_impl { + fn code(&self) -> i32 { + match self { + #(#code_arms)* + } + } + #payload_fn + unsafe fn read_code( + __wv_code: i32, + __wv_r: &mut ::weaveffi::abi::BufferReader<'_>, + ) -> ::std::result::Result< + ::std::option::Option, + ::weaveffi::abi::BufferDecodeError, + > { + // SAFETY: forwarded from the caller. + ::std::result::Result::Ok(::std::option::Option::Some(unsafe { + match __wv_code { + #(#read_arms)* + _ => return ::std::result::Result::Ok(::std::option::Option::None), + } + })) + } + } + }; + + if !error_no_display(&item.attrs)? { + let mut display_arms = Vec::new(); + for c in &eb.codes { + let v = ident(&c.name); + let variant = item.variants.iter().find(|x| x.ident == c.name); + let attrs = variant.map_or(&[][..], |x| &x.attrs[..]); + let bindings: Vec = c.fields.iter().map(|f| ident(&f.name)).collect(); + let locals: Vec = c.fields.iter().map(|f| field_local(&f.name)).collect(); + let pat = if bindings.is_empty() { + quote!(Self::#v) + } else { + quote!(Self::#v { #(#bindings: #locals),* }) + }; + let body = match variant_message(attrs)? { + Some(template) => { + // Each field the template names is passed as a named + // argument, so it's interpolated from the binding. + let named = placeholders(&template.value()); + let args = c + .fields + .iter() + .filter(|f| named.contains(&f.name)) + .map(|f| { + let name = ident(&f.name); + let local = field_local(&f.name); + quote!(, #name = #local) + }); + quote!(::std::write!(__wv_f, #template #(#args)*)) + } + None => { + let message = default_message(attrs, &c.name)?; + quote!(__wv_f.write_str(#message)) + } + }; + display_arms.push(quote! { + #[allow(unused_variables)] + #pat => #body, + }); + } + let error_impl = quote_spanned! {user_ty.span()=> + impl ::std::error::Error for #user_ty {} + }; + out.extend(quote! { + impl ::std::fmt::Display for #ty { + fn fmt(&self, __wv_f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#display_arms)* + } + } + } + + #error_impl + }); + } + Ok(out) +} + +/// The names a format template's `{name}` and `{name:spec}` placeholders +/// refer to (`{{` escapes skipped). +fn placeholders(template: &str) -> Vec { + let mut out = Vec::new(); + let mut chars = template.chars().peekable(); + while let Some(c) = chars.next() { + if c != '{' { + continue; + } + if chars.peek() == Some(&'{') { + chars.next(); + continue; + } + let name: String = + std::iter::from_fn(|| chars.next_if(|c| *c == '_' || c.is_alphanumeric())).collect(); + if !name.is_empty() && !out.contains(&name) { + out.push(name); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::placeholders; + + #[test] + fn placeholders_name_the_interpolated_fields() { + assert_eq!( + placeholders("{key} at {at:?}, {{literal}} {key} {0}"), + ["key", "at", "0"] + ); + assert!(placeholders("no fields").is_empty()); + } +} diff --git a/crates/weaveffi-macros/src/codegen/foreign.rs b/crates/weaveffi-macros/src/codegen/foreign.rs index 89f4dc43..8ca70543 100644 --- a/crates/weaveffi-macros/src/codegen/foreign.rs +++ b/crates/weaveffi-macros/src/codegen/foreign.rs @@ -47,7 +47,7 @@ impl Finder<'_> { /// Visit the parts of `item` that reach the IR: exported signatures, /// record and enum fields, and callback-interface methods (never bodies). fn exported_item(&mut self, item: &syn::Item) { - use weaveffi_model::rust::has_marker; + use crate::extract::has_marker; match item { syn::Item::Fn(f) if has_marker(&f.attrs, "export") => self.visit_signature(&f.sig), syn::Item::Struct(s) if has_marker(&s.attrs, "record") => self.visit_fields(&s.fields), diff --git a/crates/weaveffi-macros/src/codegen/helpers.rs b/crates/weaveffi-macros/src/codegen/helpers.rs index 0f5f5e8b..0e3e1d0d 100644 --- a/crates/weaveffi-macros/src/codegen/helpers.rs +++ b/crates/weaveffi-macros/src/codegen/helpers.rs @@ -1,19 +1,36 @@ //! Shared rendering helpers: identifiers, C-type spelling, ABI slot lists, //! call targets, the producer-signature reader, and `#[cfg]` wrapping. +//! +//! Every thunk parameter and local is named `__wv_*` (a C slot `name_ptr` is +//! the Rust parameter `__wv_name_ptr`), so nothing the producer declares in +//! the module (a constant, a type, a function) can collide with them. +//! Parameter names don't affect the C ABI. use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::{format_ident, quote, ToTokens}; use syn::spanned::Spanned as _; use syn::Ident; use weaveffi_model::abi::{AbiParam, CType, ConstPos}; -use weaveffi_model::model::ParamBinding; -use weaveffi_model::ty::Ty; + +use super::custom::{CustomScope, Shape}; /// Make a call-site identifier from a string. pub(crate) fn ident(name: &str) -> Ident { Ident::new(name, Span::call_site()) } +/// The Rust name of the C slot `name` in a thunk: `__wv_{name}`. +pub(crate) fn slot(name: &str) -> Ident { + format_ident!("__wv_{}", name) +} + +/// The Rust name of a lifted parameter `name` in a thunk: `__wv_p_{name}` +/// (distinct from every slot name, so a parameter called `out_len` can't +/// shadow the slot `__wv_out_len`). +pub(crate) fn local(name: &str) -> Ident { + format_ident!("__wv_p_{}", name) +} + // ── C type -> Rust FFI type ────────────────────────────────────────────── /// Render a [`CType`] as the Rust spelling a producer thunk uses. @@ -64,11 +81,13 @@ pub(crate) fn ctype_to_rust(ct: &CType, prefix: &str) -> TokenStream { } } -/// Render one ABI slot as `name: ty`. -pub(crate) fn slot_tokens(p: &AbiParam, prefix: &str) -> TokenStream { - let n = ident(&p.name); - let t = ctype_to_rust(&p.ty, prefix); - quote!(#n: #t) +/// The pointee of a pointer C type (an out slot's value type), or the type +/// itself. +pub(crate) fn pointee(ct: &CType) -> &CType { + match ct { + CType::Ptr { pointee, .. } => pointee, + other => other, + } } /// Render the `-> T` return clause for a lowered symbol (empty for `void`). @@ -120,10 +139,10 @@ pub(crate) fn name_lit(name: &str) -> syn::LitStr { /// /// The model knows every type *semantically* (`Ty::Interface("Store")`) /// but not how the producer spelled it (`&Store`, `Arc`, -/// `Option>`). Thunks are emitted inside the producer's -/// module, so reusing the written path keeps parent-module types in scope, and -/// the wrapper (`&` vs `Arc`) decides whether the object is borrowed or -/// retained for the call. +/// `Option>`, `usize`, a custom type). Thunks are emitted +/// inside the producer's module, so reusing the written path keeps +/// parent-module types in scope, and the wrapper (`&` vs `Arc`) decides +/// whether the object is borrowed or retained for the call. /// /// Inside an interface's `impl` block the producer may write `Self`; the thunk /// is a free function, so every type this view hands out has `Self` replaced @@ -132,22 +151,34 @@ pub(crate) fn name_lit(name: &str) -> syn::LitStr { pub(crate) struct UserSig<'a> { sig: &'a syn::Signature, self_ty: Option<&'a Ident>, + customs: CustomScope<'a>, } impl<'a> UserSig<'a> { - pub(crate) fn new(sig: &'a syn::Signature, self_ty: Option<&'a Ident>) -> Self { - Self { sig, self_ty } + pub(crate) fn new( + sig: &'a syn::Signature, + self_ty: Option<&'a Ident>, + customs: CustomScope<'a>, + ) -> Self { + Self { + sig, + self_ty, + customs, + } } - /// Spell `ty` for use in a free-function thunk, substituting `Self`. - fn spell(&self, ty: &syn::Type) -> TokenStream { - let tokens = ty.to_token_stream(); + /// Spell `tokens` for use in a free-function thunk, substituting `Self`. + pub(crate) fn spell_tokens(&self, tokens: TokenStream) -> TokenStream { match self.self_ty { Some(self_ty) => replace_self(tokens, self_ty), None => tokens, } } + fn spell(&self, ty: &syn::Type) -> TokenStream { + self.spell_tokens(ty.to_token_stream()) + } + /// The producer's source type for the parameter named `name`. pub(crate) fn param_type(&self, name: &str) -> Option<&'a syn::Type> { self.sig.inputs.iter().find_map(|arg| { @@ -169,14 +200,6 @@ impl<'a> UserSig<'a> { } } - /// The written return type as tokens, to anchor a diagnostic on it. - pub(crate) fn ret_span(&self) -> TokenStream { - match &self.sig.output { - syn::ReturnType::Type(_, ty) => ty.to_token_stream(), - syn::ReturnType::Default => self.sig.ident.to_token_stream(), - } - } - /// Whether the parameter is written as a shared reference (`&T`), so the /// call lends the lifted value instead of moving it. pub(crate) fn param_is_ref(&self, name: &str) -> bool { @@ -199,6 +222,18 @@ impl<'a> UserSig<'a> { } } + /// Whether the parameter is written `&[P]` for a primitive `P` the + /// typed-array family can lend without copying (every slice element but + /// `usize` and `isize`, whose C element type differs). + pub(crate) fn param_is_borrowed_slice(&self, name: &str) -> bool { + const LENDABLE: &[&str] = &["i8", "i16", "i32", "i64", "u16", "u32", "u64", "f32", "f64"]; + LENDABLE.iter().any(|p| self.param_is_borrowed(name, p)) + && matches!( + self.param_type(name), + Some(syn::Type::Reference(r)) if matches!(r.elem.as_ref(), syn::Type::Slice(_)) + ) + } + /// The span of the parameter's written type (or the function name), the /// place a type error about it should point. pub(crate) fn param_type_span(&self, name: &str) -> Span { @@ -206,21 +241,41 @@ impl<'a> UserSig<'a> { .map_or_else(|| self.sig.ident.span(), syn::spanned::Spanned::span) } + /// The custom-type shape of a parameter, when its type mentions one. + pub(crate) fn param_shape(&self, name: &str) -> Option { + self.param_type(name).and_then(|t| self.customs.shape(t)) + } + /// The producer's spelling of a by-value parameter type with any `&` - /// removed (`&Contact` is `Contact`, `&[Item]` is `Vec`), for a - /// lift that decodes an owned value. + /// removed (`&Contact` is `Contact`, `&[Item]` is `Vec`, `&str` + /// is `String`), for a lift that produces an owned value. pub(crate) fn param_owned(&self, name: &str) -> Option { let ty = self.param_type(name)?; - Some(match ty { + Some(self.owned(ty)) + } + + fn owned(&self, ty: &syn::Type) -> TokenStream { + match ty { syn::Type::Reference(r) => match r.elem.as_ref() { syn::Type::Slice(slice) => { let elem = self.spell(&slice.elem); quote!(::std::vec::Vec<#elem>) } + syn::Type::Path(p) if p.path.is_ident("str") => quote!(::std::string::String), other => self.spell(other), }, other => self.spell(other), - }) + } + } + + /// The owned type a parameter is lifted as: its repr when it mentions a + /// custom type (see [`param_owned`](Self::param_owned)), else its owned + /// spelling. + pub(crate) fn param_lifted(&self, name: &str) -> Option { + match self.param_shape(name) { + Some(shape) => Some(self.spell_tokens(shape.repr_ty())), + None => self.param_owned(name), + } } /// Whether the parameter is written as a slice reference (`&[T]`). @@ -231,11 +286,6 @@ impl<'a> UserSig<'a> { ) } - /// The producer's return type with `Result` peeled, spelled for a thunk. - pub(crate) fn ret_value_type(&self) -> Option { - self.ret_syn().map(|t| self.spell(t)) - } - /// The span of the written return type (or the function name). pub(crate) fn ret_type_span(&self) -> Span { match &self.sig.output { @@ -244,10 +294,51 @@ impl<'a> UserSig<'a> { } } - /// Whether the parameter's type (under any `&` and `Option`) is an - /// `Arc<..>`, meaning the producer wants to retain the object. - pub(crate) fn param_wants_arc(&self, name: &str) -> bool { - self.param_type(name).is_some_and(mentions_arc) + /// The written return type as tokens, to anchor a diagnostic on it. + pub(crate) fn ret_span(&self) -> TokenStream { + match &self.sig.output { + syn::ReturnType::Type(_, ty) => ty.to_token_stream(), + syn::ReturnType::Default => self.sig.ident.to_token_stream(), + } + } + + /// The written error type `E` of a `Result` return. + pub(crate) fn ret_error(&self) -> Option<&'a syn::Type> { + crate::extract::result_error(&self.sig.output) + } + + /// The producer's error type spelled for a thunk, and the span to point + /// a bound on it at. + pub(crate) fn ret_error_spelled(&self) -> Option<(TokenStream, Span)> { + self.ret_error().map(|e| (self.spell(e), e.span())) + } + + /// The producer's return type with `Result` peeled. + fn ret_syn(&self) -> Option<&'a syn::Type> { + match &self.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(peel_result(ty)), + } + } + + /// The producer's return type with `Result` peeled, spelled for a thunk. + pub(crate) fn ret_value_type(&self) -> Option { + self.ret_syn().map(|t| self.spell(t)) + } + + /// The custom-type shape of the (`Result`-peeled) return, when it + /// mentions one. + pub(crate) fn ret_shape(&self) -> Option { + self.ret_syn().and_then(|t| self.customs.shape(t)) + } + + /// The type the return is lowered as: its repr when it mentions a custom + /// type, else its spelling. + pub(crate) fn ret_lowered_type(&self) -> Option { + match self.ret_shape() { + Some(shape) => Some(self.spell_tokens(shape.repr_ty())), + None => self.ret_value_type(), + } } /// The producer's spelling of the object type behind an interface @@ -259,6 +350,12 @@ impl<'a> UserSig<'a> { .map(|t| self.spell(t)) } + /// Whether the parameter's type (under any `&` and `Option`) is an + /// `Arc<..>`, meaning the producer wants to retain the object. + pub(crate) fn param_wants_arc(&self, name: &str) -> bool { + self.param_type(name).is_some_and(mentions_arc) + } + /// The `dyn Trait` behind a callback-interface parameter written as /// `Arc`. /// @@ -277,14 +374,6 @@ impl<'a> UserSig<'a> { callback_dyn(ty) } - /// The producer's return type with `Result` peeled. - fn ret_syn(&self) -> Option<&'a syn::Type> { - match &self.sig.output { - syn::ReturnType::Default => None, - syn::ReturnType::Type(_, ty) => Some(weaveffi_model::rust::peel_result(ty)), - } - } - /// The producer's spelling of the object type behind an interface return /// (`Result`, `Option`, and `Arc` peeled). pub(crate) fn ret_object(&self) -> Option { @@ -294,18 +383,6 @@ impl<'a> UserSig<'a> { .map(|t| self.spell(t)) } - /// The object type behind the element of an `Iter` return, when `X` is - /// an object (`Arc` or `Option>`). - pub(crate) fn iter_elem_object(&self) -> Option { - let elem = peel_wrappers(self.iter_elem()?); - matches!(elem, syn::Type::Path(_)).then(|| self.spell(elem)) - } - - /// The element type `X` of an `Iter` return, spelled for a thunk. - pub(crate) fn iter_elem_type(&self) -> Option { - self.iter_elem().map(|t| self.spell(t)) - } - /// The element type `X` of an `Iter` return. fn iter_elem(&self) -> Option<&'a syn::Type> { let syn::Type::Path(p) = self.ret_syn()? else { @@ -321,6 +398,23 @@ impl<'a> UserSig<'a> { } } + /// The element type `X` of an `Iter` return, spelled for a thunk. + pub(crate) fn iter_elem_type(&self) -> Option { + self.iter_elem().map(|t| self.spell(t)) + } + + /// The custom-type shape of an `Iter` return's element. + pub(crate) fn iter_elem_shape(&self) -> Option { + self.iter_elem().and_then(|t| self.customs.shape(t)) + } + + /// The object type behind the element of an `Iter` return, when `X` is + /// an object (`Arc` or `Option>`). + pub(crate) fn iter_elem_object(&self) -> Option { + let elem = peel_wrappers(self.iter_elem()?); + matches!(elem, syn::Type::Path(_)).then(|| self.spell(elem)) + } + /// Whether the method receiver is `self: Arc` rather than `&self`. pub(crate) fn receiver_is_arc(&self) -> bool { self.sig @@ -329,6 +423,22 @@ impl<'a> UserSig<'a> { } } +/// Peel `Result` to its `T`, returning any other type unchanged. +pub(crate) fn peel_result(ty: &syn::Type) -> &syn::Type { + if let syn::Type::Path(p) = ty { + if let Some(seg) = p.path.segments.last() { + if seg.ident == "Result" { + if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { + if let Some(syn::GenericArgument::Type(ok)) = args.args.first() { + return ok; + } + } + } + } + } + ty +} + /// Replace every `Self` identifier token in `tokens` with `self_ty`. fn replace_self(tokens: TokenStream, self_ty: &Ident) -> TokenStream { tokens @@ -437,53 +547,55 @@ fn callback_dyn(ty: &syn::Type) -> syn::Result { // ── slot spelling that honors the producer's types ─────────────────────── -/// Render one ABI slot as `name: ty`, spelling object and callback-interface -/// pointers with the producer's own types. +/// Which binding a slot list belongs to, for spelling object and vtable +/// slots with the producer's own types. +pub(crate) struct SlotOwners<'a> { + /// `(param name, object slot name)` pairs of object parameters. + pub(crate) objects: Vec<(&'a str, &'a str)>, + /// `(param name, vtable slot name)` pairs of callback parameters. + pub(crate) vtables: Vec<(&'a str, &'a str)>, +} + +/// Render one ABI slot as `__wv_{name}: ty`, spelling object and +/// callback-interface pointers with the producer's own types. /// -/// An object slot lowered from `params` renders as `*const ` -/// so a `super::T` stays in scope where the thunk lands; a callback vtable -/// slot renders as `*const ::Vtable`, which -/// resolves through the impl the trait's own module emitted. Every other slot -/// renders from its C type. The implicit method receiver slot is named `self` -/// at the C level, which is not a legal Rust parameter name, so it renders as -/// `__wv_self`. +/// An object slot renders as `*const ` (or `*mut` for a +/// callback method's adopted object) so a `super::T` stays in scope where +/// the thunk lands; a callback vtable slot renders as +/// `*const ::Vtable`, which resolves through +/// the impl the trait's own module emitted. Every other slot renders from +/// its C type. pub(crate) fn slot_type_for( p: &AbiParam, - params: &[ParamBinding], + owners: &SlotOwners<'_>, user: &UserSig<'_>, prefix: &str, ) -> syn::Result { - let n = if p.name == "self" { - ident("__wv_self") - } else { - ident(&p.name) - }; - for pb in params { - if pb.ty.interface_name().is_some() && pb.name == p.name { - if let Some(obj) = user.param_object(&pb.name) { - // Borrowed top-level parameters are `const T*`; a callback - // method's object slot transfers ownership and is `T*`. - let owned = matches!( - &p.ty, - CType::Ptr { - konst: ConstPos::None, - .. - } - ); - return Ok(if owned { - quote!(#n: *mut #obj) - } else { - quote!(#n: *const #obj) - }); - } - } - if pb.ty.callback_interface_name().is_some() && p.name == format!("{}_vtable", pb.name) { - let dyn_ty = user.param_callback(&pb.name)?; - return Ok(quote!( - #n: *const <#dyn_ty as ::weaveffi::abi::CallbackInterface>::Vtable - )); + let n = slot(&p.name); + if let Some((param, _)) = owners.objects.iter().find(|(_, s)| *s == p.name) { + if let Some(obj) = user.param_object(param) { + // Borrowed top-level parameters are `const T*`; a callback + // method's object slot transfers ownership and is `T*`. + let owned = matches!( + &p.ty, + CType::Ptr { + konst: ConstPos::None, + .. + } + ); + return Ok(if owned { + quote!(#n: *mut #obj) + } else { + quote!(#n: *const #obj) + }); } } + if let Some((param, _)) = owners.vtables.iter().find(|(_, s)| *s == p.name) { + let dyn_ty = user.param_callback(param)?; + return Ok(quote!( + #n: *const <#dyn_ty as ::weaveffi::abi::CallbackInterface>::Vtable + )); + } let t = ctype_to_rust(&p.ty, prefix); Ok(quote!(#n: #t)) } @@ -491,25 +603,25 @@ pub(crate) fn slot_type_for( /// Render the slot list for a lowered signature (see [`slot_type_for`]). pub(crate) fn fn_slots( abi_params: &[AbiParam], - params: &[ParamBinding], + owners: &SlotOwners<'_>, user: &UserSig<'_>, prefix: &str, ) -> syn::Result> { abi_params .iter() - .map(|p| slot_type_for(p, params, user, prefix)) + .map(|p| slot_type_for(p, owners, user, prefix)) .collect() } -/// Render the `-> T` return clause, spelling an object return with the -/// producer's own type. +/// The `-> T` return clause, spelling an object return with the producer's +/// own type. pub(crate) fn ret_arrow_for( ret: &CType, - ret_ty: Option<&Ty>, + object_ret: bool, user: &UserSig<'_>, prefix: &str, ) -> TokenStream { - if ret_ty.is_some_and(|t| t.interface_name().is_some()) { + if object_ret { if let Some(obj) = user.ret_object() { return quote!(-> *mut #obj); } @@ -521,11 +633,11 @@ pub(crate) fn ret_arrow_for( /// spelling an object return with the producer's own type. pub(crate) fn ret_type_for( ret: &CType, - ret_ty: Option<&Ty>, + object_ret: bool, user: &UserSig<'_>, prefix: &str, ) -> TokenStream { - if ret_ty.is_some_and(|t| t.interface_name().is_some()) { + if object_ret { if let Some(obj) = user.ret_object() { return quote!(*mut #obj); } @@ -544,10 +656,13 @@ pub(crate) fn thunk_attrs() -> TokenStream { unsafe_code, unused_unsafe, deprecated, + non_snake_case, clippy::missing_safety_doc, clippy::needless_return, clippy::let_unit_value, - clippy::unit_arg + clippy::unit_arg, + clippy::too_many_arguments, + clippy::useless_conversion )] } } diff --git a/crates/weaveffi-macros/src/codegen/interfaces.rs b/crates/weaveffi-macros/src/codegen/interfaces.rs index 27dcc712..73c24414 100644 --- a/crates/weaveffi-macros/src/codegen/interfaces.rs +++ b/crates/weaveffi-macros/src/codegen/interfaces.rs @@ -19,7 +19,7 @@ use super::helpers::{ident, thunk_attrs}; /// methods and release references from any thread, and async methods hold /// an `Arc` across a spawn; /// * `{c_tag}_clone`, which returns a new strong reference to the same object -/// (the plan's `RetPass::Object::clone_symbol`); +/// (the clone symbol a callback's object return names); /// * `{c_tag}_destroy`, which releases one strong reference; the object drops /// with the last one. A panicking user `Drop` is swallowed (there is no /// `out_err` slot to report through, and a destructor must not take down the @@ -42,13 +42,13 @@ pub(crate) fn gen_interface_lifecycle(i: &InterfaceBinding) -> TokenStream { }; #attrs - pub unsafe extern "C" fn #clone_sym(ptr: *const #ty) -> *mut #ty { - unsafe { ::weaveffi::abi::object_clone(ptr) } + pub unsafe extern "C" fn #clone_sym(__wv_ptr: *const #ty) -> *mut #ty { + unsafe { ::weaveffi::abi::object_clone(__wv_ptr) } } #attrs - pub unsafe extern "C" fn #destroy_sym(ptr: *mut #ty) { - unsafe { ::weaveffi::abi::object_destroy(ptr) } + pub unsafe extern "C" fn #destroy_sym(__wv_ptr: *mut #ty) { + unsafe { ::weaveffi::abi::object_destroy(__wv_ptr) } } } } diff --git a/crates/weaveffi-macros/src/codegen/iterators.rs b/crates/weaveffi-macros/src/codegen/iterators.rs index edda0a7c..2fab6436 100644 --- a/crates/weaveffi-macros/src/codegen/iterators.rs +++ b/crates/weaveffi-macros/src/codegen/iterators.rs @@ -1,20 +1,21 @@ //! Thunk emission for `iter` functions: the launcher / `_next` / //! `_destroy` trio. //! -//! This is the producer half of the pull contract stated by -//! [`weaveffi_model::plan::IteratorProtocol`]: each `_next` call yields exactly -//! one element the consumer then owns (and releases per the protocol's -//! `elem` pass), and `_destroy` releases the handle exactly once. Errors from -//! the launcher follow the owning function's -//! [`ErrorStrategy`](weaveffi_model::plan::ErrorStrategy); `_next` fails only -//! on a null handle or a concurrent or re-entrant `_next`. +//! This is the producer half of the iterator pull contract (see +//! [`weaveffi_model::plan`]): each `_next` call yields exactly one element +//! the consumer then owns (written to the slots its +//! [`ItemPass`](weaveffi_model::plan::ItemPass) names), and `_destroy` +//! releases the handle exactly once. Errors from the launcher follow the +//! owning function's error strategy; `_next` fails only on a null handle or +//! out slot, or a concurrent or re-entrant `_next`. use proc_macro2::TokenStream; use quote::quote; use weaveffi_model::model::{FnBinding, IteratorBinding}; +use weaveffi_model::plan::ItemPass; -use super::helpers::{fn_slots, ident, slot_tokens, thunk_attrs, CallTarget, UserSig}; -use super::lift::lower_ret; +use super::helpers::{ctype_to_rust, fn_slots, ident, slot, thunk_attrs, CallTarget, UserSig}; +use super::lift::{lower_item, slot_owners}; use super::sync::{checked_call, sync_lifts}; use super::unsupported; @@ -22,7 +23,7 @@ use super::unsupported; /// `iter`. The producer returns a `weaveffi::Iter` (optionally wrapped in /// `Result`); the launcher boxes it behind a /// [`weaveffi::abi::IterHandle`], `_next` pulls one element and lowers it -/// through `out_item`, and `_destroy` drops the handle. +/// into its out slots, and `_destroy` drops the handle. pub(crate) fn gen_iterator_function( f: &FnBinding, it: &IteratorBinding, @@ -42,16 +43,15 @@ pub(crate) fn gen_iterator_function( let handle = quote!(::weaveffi::abi::IterHandle<#elem>); // The object pointee for an element that is an interface (`Arc` or // `Option>`), spelled the producer's way. - let elem_object: Option = if it.elem.interface_name().is_some() { - user.iter_elem_object() - } else { - None + let elem_object: Option = match it.item { + ItemPass::Object { .. } => user.iter_elem_object(), + _ => None, }; let attrs = thunk_attrs(); // ── launcher: lift inputs, run the producer's fn, box the iterator ── - let launch_sym = ident(&it.launch.symbol); - let launch_params = fn_slots(&it.launch.params, &f.params, user, prefix)?; + let launch_sym = ident(&f.abi.symbol); + let launch_params = fn_slots(&f.abi.params, &slot_owners(&f.params), user, prefix)?; let lifts = sync_lifts(f, user, target)?; let lifted = lifts.finish(); let call = checked_call(f, target.call(&f.name, &lifts.args), user); @@ -59,7 +59,7 @@ pub(crate) fn gen_iterator_function( #attrs pub unsafe extern "C" fn #launch_sym(#(#launch_params),*) -> *mut #handle { unsafe { - ::weaveffi::abi::call_sync(out_err, move || unsafe { + ::weaveffi::abi::call_sync(__wv_out_err, move || unsafe { #lifted ::std::result::Result::Ok(::weaveffi::abi::iter_into_raw(#call)) }) @@ -67,50 +67,62 @@ pub(crate) fn gen_iterator_function( } }; - // ── next: pull one element, lower it into `out_item`, return 1/0 ── + // ── next: pull one element, lower it into its out slots, return 1/0 ── let next_sym = ident(&it.next.symbol); // The first slot is the opaque handle (spelled with the real Rust type); - // the rest (`out_item`, any item out-params, `out_err`) lower straight - // from the model, except an object `out_item`, which uses the producer's - // pointee spelling. + // the item slots and `out_err` lower straight from the model, except an + // object `out_item`, which uses the producer's pointee spelling. + let object_slot = match &it.item { + ItemPass::Object { out_item, .. } => Some(out_item.name.as_str()), + _ => None, + }; let rest_params: Vec = it.next.params[1..] .iter() - .map(|p| match (&elem_object, p.name.as_str()) { - (Some(obj), "out_item") => quote!(out_item: *mut *mut #obj), - _ => slot_tokens(p, prefix), + .map(|p| { + let n = slot(&p.name); + match (&elem_object, object_slot) { + (Some(obj), Some(o)) if o == p.name => quote!(#n: *mut *mut #obj), + _ => { + let t = ctype_to_rust(&p.ty, prefix); + quote!(#n: #t) + } + } }) .collect(); - let item = lower_ret(&it.elem, "e!(__wv_item), elem_object.as_ref()); + let iter = slot(&it.next.params[0].name); + let convert = match user.iter_elem_shape() { + Some(shape) => { + let lowered = shape.lower(quote!(&__wv_item)); + quote!(let __wv_item = #lowered;) + } + None => TokenStream::new(), + }; + let (checks, write) = lower_item(&it.item, elem_object.as_ref()); let next = quote! { #attrs - pub unsafe extern "C" fn #next_sym(iter: *const #handle, #(#rest_params),*) -> i32 { + pub unsafe extern "C" fn #next_sym(#iter: *const #handle, #(#rest_params),*) -> i32 { unsafe { - ::weaveffi::abi::call_sync(out_err, move || unsafe { - if out_item.is_null() { - return ::std::result::Result::Err(::weaveffi::abi::FfiError::new( - ::weaveffi::abi::MARSHAL_ERROR_CODE, - "out_item is null", - )); - } - ::std::result::Result::Ok(match ::weaveffi::abi::iter_next(iter)? { + ::weaveffi::abi::call_sync(__wv_out_err, move || unsafe { + #checks + match ::weaveffi::abi::iter_next(#iter)? { ::std::option::Option::Some(__wv_item) => { - *out_item = #item; - 1 + #convert + #write + ::std::result::Result::Ok(1) } - ::std::option::Option::None => 0, - }) + ::std::option::Option::None => ::std::result::Result::Ok(0), + } }) } } }; - // ── destroy: drop the handle exactly once, per the iterator protocol's - // handle-lifecycle clause. ── + // ── destroy: drop the handle exactly once. ── let destroy_sym = ident(&it.destroy_symbol); let destroy = quote! { #attrs - pub unsafe extern "C" fn #destroy_sym(iter: *mut #handle) { - unsafe { ::weaveffi::abi::iter_destroy(iter) } + pub unsafe extern "C" fn #destroy_sym(__wv_iter: *mut #handle) { + unsafe { ::weaveffi::abi::iter_destroy(__wv_iter) } } }; diff --git a/crates/weaveffi-macros/src/codegen/lift.rs b/crates/weaveffi-macros/src/codegen/lift.rs index 358ca948..75e5cdbd 100644 --- a/crates/weaveffi-macros/src/codegen/lift.rs +++ b/crates/weaveffi-macros/src/codegen/lift.rs @@ -1,19 +1,28 @@ //! Parameter lifting and return lowering: one runtime call per parameter -//! and per return, chosen by the value's family. +//! and per return, chosen by the passing contract the model stores on each +//! binding ([`ArgPass`], [`RetPass`], [`ResultPass`], [`ItemPass`]). //! //! Every lift evaluates to a `Result` bound to the parameter's -//! name. A thunk lifts **all** of its parameters first and only then unwraps -//! them in order ([`Lifts::finish`]), so when one fails the others, already -//! adopted (a callback context, a buffer's object tokens), are dropped and -//! released rather than leaked. The runtime functions behind each lift live -//! in `weaveffi::abi::marshal`; this module only picks one. +//! local (`__wv_p_{name}`). A thunk lifts **all** of its parameters first and +//! only then unwraps them in order ([`Lifts::finish`]), so when one fails +//! the others, already adopted (a callback context, a buffer's object +//! tokens), are dropped and released rather than leaked. The runtime +//! functions behind each lift live in `weaveffi::abi::marshal`; this module +//! only picks one and names its slots. +//! +//! A lift is annotated with the type the producer's function takes (its +//! written spelling, `&` removed), so a generic runtime lift resolves to +//! that type: `usize` from a `u64` slot, a `char` from a string, a C-style +//! enum from an `i32`. A parameter whose type mentions a custom type is +//! lifted as its repr, then converted. use proc_macro2::TokenStream; use quote::{quote, quote_spanned}; use weaveffi_model::model::ParamBinding; -use weaveffi_model::ty::{Family, Prim, Ty}; +use weaveffi_model::plan::{ArgPass, ItemPass, ResultPass, RetPass}; -use super::helpers::{ident, name_lit, UserSig}; +use super::custom::LiftSite; +use super::helpers::{local, name_lit, slot, SlotOwners, UserSig}; use super::unsupported; /// The lifted parameters of one thunk. @@ -37,11 +46,9 @@ impl Lifts { } } - fn push(&mut self, name: &syn::Ident, lift: Option, arg: TokenStream) { - if let Some(lift) = lift { - self.lifts.extend(quote!(let #name = #lift;)); - self.unwraps.extend(quote!(let #name = #name?;)); - } + fn push(&mut self, name: &syn::Ident, lift: TokenStream, arg: TokenStream) { + self.lifts.extend(quote!(let #name = #lift;)); + self.unwraps.extend(quote!(let #name = #name?;)); self.args.push(arg); } @@ -52,64 +59,125 @@ impl Lifts { } } +/// The object and vtable slots of a callable's parameters, for spelling +/// them with the producer's types. +pub(crate) fn slot_owners(params: &[ParamBinding]) -> SlotOwners<'_> { + let mut owners = SlotOwners { + objects: Vec::new(), + vtables: Vec::new(), + }; + for p in params { + match &p.pass { + ArgPass::Object { slot, .. } => owners.objects.push((&p.name, &slot.name)), + ArgPass::Callback { vtable, .. } => owners.vtables.push((&p.name, &vtable.name)), + _ => {} + } + } + owners +} + /// Lift one parameter into `lifts`. With `owned`, the value must outlive the -/// call (an async launcher's future): strings and bytes are copied and -/// objects retained even when the producer borrows them, and the argument -/// lends the owned value. +/// call (an async launcher's future): strings, bytes, and arrays are copied +/// and objects retained even when the producer borrows them, and the +/// argument lends the owned value. pub(crate) fn lift_param( lifts: &mut Lifts, pb: &ParamBinding, user: &UserSig<'_>, owned: bool, ) -> syn::Result<()> { - let name = ident(&pb.name); + let name = local(&pb.name); let lit = name_lit(&pb.name); let span = user.param_type_span(&pb.name); let is_ref = user.param_is_ref(&pb.name); - let lent = if is_ref { - quote!(&#name) - } else { - quote!(#name) + let shape = user.param_shape(&pb.name); + let lent = || { + if user.param_is_slice(&pb.name) { + quote!(&#name[..]) + } else if is_ref { + quote!(&#name) + } else { + quote!(#name) + } + }; + // The type the lift produces: the repr of a custom type, else the + // producer's owned spelling (`_` when unknown, left to inference). + let lifted = user.param_lifted(&pb.name).unwrap_or_else(|| quote!(_)); + let typed = |call: TokenStream| { + quote_spanned!(span=> { + let __wv_lifted: ::std::result::Result<#lifted, ::weaveffi::abi::FfiError> = #call; + __wv_lifted + }) }; - let ptr = ident(&format!("{}_ptr", pb.name)); - let len = ident(&format!("{}_len", pb.name)); - match pb.ty.family() { - Family::Direct => match &pb.ty { - Ty::Enum(_) => { - let et = user.param_object(&pb.name).unwrap_or_else(|| quote!(_)); - let lift = quote_spanned!(span=> ::weaveffi::abi::lift_enum::<#et>(#name, #lit)); - lifts.push(&name, Some(lift), lent); + let (lift, arg) = match &pb.pass { + ArgPass::Direct { slot: s } => { + let s = slot(&s.name); + ( + typed(quote!(::weaveffi::abi::lift_scalar_param(#s, #lit))), + lent(), + ) + } + ArgPass::OptDirect { has, value, .. } => { + let (h, v) = (slot(&has.name), slot(&value.name)); + ( + typed(quote!(::weaveffi::abi::lift_opt_param(#h, #v, #lit))), + lent(), + ) + } + ArgPass::Slice { ptr, len, .. } => { + let (p, l) = (slot(&ptr.name), slot(&len.name)); + if !owned && shape.is_none() && user.param_is_borrowed_slice(&pb.name) { + ( + quote_spanned!(span=> ::weaveffi::abi::lift_slice_param(#p, #l, #lit)), + quote!(#name), + ) + } else { + ( + typed(quote!(::weaveffi::abi::lift_slice_vec_param(#p, #l, #lit))), + lent(), + ) } - _ => lifts.push(&name, None, lent), - }, - Family::String | Family::Bytes => { - let (elem, borrow, copy) = if matches!(pb.ty, Ty::Prim(Prim::String)) { - ("str", quote!(lift_str_param), quote!(lift_string_param)) + } + ArgPass::String { ptr, len } => { + let (p, l) = (slot(&ptr.name), slot(&len.name)); + if !owned && shape.is_none() && user.param_is_borrowed(&pb.name, "str") { + ( + quote_spanned!(span=> ::weaveffi::abi::lift_str_param(#p, #l, #lit)), + quote!(#name), + ) } else { - ("u8", quote!(lift_slice_param), quote!(lift_bytes_param)) - }; - // `&str` and `&[u8]` borrow the caller's bytes for the call - // without copying; any other spelling gets an owned copy, lent - // when written as a reference. - let borrowed = !owned && user.param_is_borrowed(&pb.name, elem); - let f = if borrowed { borrow } else { copy }; - let lift = quote_spanned!(span=> ::weaveffi::abi::#f(#ptr, #len, #lit)); - let arg = if borrowed { quote!(#name) } else { lent }; - lifts.push(&name, Some(lift), arg); - } - Family::Buffer => { - let ty = user.param_owned(&pb.name).unwrap_or_else(|| quote!(_)); - let lift = - quote_spanned!(span=> ::weaveffi::abi::lift_buffer_param::<#ty>(#ptr, #len, #lit)); - let arg = if user.param_is_slice(&pb.name) { - quote!(&#name[..]) + ( + typed(quote!(::weaveffi::abi::lift_text_param(#p, #l, #lit))), + lent(), + ) + } + } + ArgPass::Bytes { ptr, len } => { + let (p, l) = (slot(&ptr.name), slot(&len.name)); + if !owned && shape.is_none() && user.param_is_borrowed(&pb.name, "u8") { + ( + quote_spanned!(span=> ::weaveffi::abi::lift_byte_slice_param(#p, #l, #lit)), + quote!(#name), + ) } else { - lent - }; - lifts.push(&name, Some(lift), arg); + ( + typed(quote!(::weaveffi::abi::lift_bytes_param(#p, #l, #lit))), + lent(), + ) + } + } + ArgPass::Buffer { ptr, len } => { + let (p, l) = (slot(&ptr.name), slot(&len.name)); + ( + typed(quote!(::weaveffi::abi::lift_buffer_param(#p, #l, #lit))), + lent(), + ) } - Family::Object { nullable } => { + ArgPass::Object { + slot: s, nullable, .. + } => { + let s = slot(&s.name); let obj = user.param_object(&pb.name).ok_or_else(|| { unsupported( user.param_span(&pb.name), @@ -128,100 +196,211 @@ pub(crate) fn lift_param( } let retain = wants_arc || owned; let f = match (nullable, retain) { - (false, false) => quote!(lift_object_param::<#obj>(#name, #lit)), - (false, true) => quote!(lift_object_arc_param::<#obj>(#name, #lit)), - (true, false) => quote!(lift_object_opt_param::<#obj>(#name)), - (true, true) => quote!(lift_object_arc_opt_param::<#obj>(#name)), + (false, false) => quote!(lift_object_param::<#obj>(#s, #lit)), + (false, true) => quote!(lift_object_arc_param::<#obj>(#s, #lit)), + (true, false) => quote!(lift_object_opt_param::<#obj>(#s)), + (true, true) => quote!(lift_object_arc_opt_param::<#obj>(#s)), }; - let lift = quote_spanned!(span=> ::weaveffi::abi::#f); // A borrowed lift already *is* the `&T` (or `Option<&T>`) the // producer takes; a retained one lends its `Arc` when the // producer borrows. let arg = match (nullable, wants_arc, retain) { - (_, true, _) => lent, + (_, true, _) => lent(), (false, false, false) | (true, false, false) => quote!(#name), (false, false, true) => quote!(&#name), (true, false, true) => quote!(#name.as_deref()), }; - lifts.push(&name, Some(lift), arg); + (quote_spanned!(span=> ::weaveffi::abi::#f), arg) } - Family::Callback { nullable } => { + ArgPass::Callback { + ctx, + vtable, + nullable, + .. + } => { let dyn_ty = user.param_callback(&pb.name)?; - let ctx = ident(&format!("{}_ctx", pb.name)); - let vtable = ident(&format!("{}_vtable", pb.name)); - let f = if nullable { + let (c, v) = (slot(&ctx.name), slot(&vtable.name)); + let f = if *nullable { quote!(lift_callback_opt_param) } else { quote!(lift_callback_param) }; - let lift = quote_spanned!(span=> ::weaveffi::abi::#f::<#dyn_ty>(#ctx, #vtable, #lit)); - lifts.push(&name, Some(lift), lent); + ( + quote_spanned!(span=> ::weaveffi::abi::#f::<#dyn_ty>(#c, #v, #lit)), + lent(), + ) } - Family::Iterator => { - return Err(unsupported( - user.param_span(&pb.name), - &pb.name, - "parameter type", - )) + }; + + let lift = match &shape { + Some(shape) => { + let owned_ty = user.param_owned(&pb.name).unwrap_or_else(|| quote!(_)); + let convert = shape.lift(quote!(__wv_repr), LiftSite::Param(&lit)); + quote! { + { + let __wv_custom: ::std::result::Result<#owned_ty, ::weaveffi::abi::FfiError> = + (#lift).and_then(|__wv_repr| #convert); + __wv_custom + } + } } - } + None => lift, + }; + lifts.push(&name, lift, arg); Ok(()) } -/// The expression lowering the owned value `value` of type `ty` into a sync -/// return (or an iterator element): the C return value, with a string, -/// bytes, or buffer written as a producer-allocated run whose length goes to -/// `out_len`. `object` is the producer's spelling of an object return's -/// pointee, which pins the `Arc` a `Self` return converts into. -pub(crate) fn lower_ret(ty: &Ty, value: &TokenStream, object: Option<&TokenStream>) -> TokenStream { +/// Convert a returned value (`value`, the producer's type) to the repr it's +/// lowered as, when its type mentions a custom type. +pub(crate) fn lower_custom_ret(user: &UserSig<'_>, value: &TokenStream) -> TokenStream { + match user.ret_shape() { + Some(shape) => { + let converted = shape.lower(quote!(&#value)); + quote!(let #value = #converted;) + } + None => TokenStream::new(), + } +} + +/// The expression lowering the owned value `value` into a sync return's C +/// return value, writing any out slots. `object` is the producer's spelling +/// of an object return's pointee, which pins the `Arc` a `Self` return +/// converts into. +pub(crate) fn lower_ret( + pass: &RetPass, + value: &TokenStream, + object: Option<&TokenStream>, +) -> TokenStream { let typed = |call: TokenStream| match object { Some(obj) => quote!({ let __wv_p: *mut #obj = #call; __wv_p }), None => call, }; - match ty.family() { - Family::Direct => match ty { - Ty::Enum(_) => quote!(::weaveffi::abi::CEnum::to_i32(&#value)), - _ => value.clone(), - }, - Family::String => quote!(::weaveffi::abi::lower_string_ret(#value, out_len)), - Family::Bytes => quote!(::weaveffi::abi::lower_bytes_ret(#value, out_len)), - Family::Buffer => quote!(::weaveffi::abi::lower_buffer_ret(&#value, out_len)), - Family::Object { nullable: false } => typed(quote!(::weaveffi::abi::lower_object(#value))), - Family::Object { nullable: true } => { - typed(quote!(::weaveffi::abi::lower_object_opt(#value))) + match pass { + RetPass::Void => quote!(#value), + RetPass::Direct => quote!(::weaveffi::abi::Scalar::to_abi(&#value)), + RetPass::OptDirect { out_value } => { + let out = slot(&out_value.name); + quote!(::weaveffi::abi::lower_opt_ret(#value, #out)) + } + RetPass::Slice { out_len, .. } => { + let out = slot(&out_len.name); + quote!(::weaveffi::abi::lower_slice_ret(&#value[..], #out)) + } + RetPass::String { out_len } => { + let out = slot(&out_len.name); + quote!(::weaveffi::abi::lower_string_ret(&#value, #out)) + } + RetPass::Bytes { out_len } => { + let out = slot(&out_len.name); + quote!(::weaveffi::abi::lower_bytes_ret(&#value, #out)) } - Family::Callback { .. } | Family::Iterator => { - unreachable!("validation never admits {ty} as a value return") + RetPass::Buffer { out_len } => { + let out = slot(&out_len.name); + quote!(::weaveffi::abi::lower_buffer_ret(&#value, #out)) } + RetPass::Object { + nullable: false, .. + } => typed(quote!(::weaveffi::abi::lower_object(#value))), + RetPass::Object { nullable: true, .. } => { + typed(quote!(::weaveffi::abi::lower_object_opt(#value))) + } + // The iterator launcher boxes the value itself. + RetPass::Iterator(_) => quote!(#value), } } /// The closure body lowering an async result `__wv_val` into the completion /// callback's result slots, as a tuple (`()` for a `void` function). -pub(crate) fn lower_async_result(ty: Option<&Ty>, object: Option<&TokenStream>) -> TokenStream { - let Some(ty) = ty else { - return quote!(()); - }; +pub(crate) fn lower_async_result(pass: &ResultPass, object: Option<&TokenStream>) -> TokenStream { let typed = |call: TokenStream| match object { Some(obj) => quote!({ let __wv_p: *mut #obj = #call; (__wv_p,) }), None => quote!((#call,)), }; - match ty.family() { - Family::Direct => match ty { - Ty::Enum(_) => quote!((::weaveffi::abi::CEnum::to_i32(&__wv_val),)), - _ => quote!((__wv_val,)), - }, - Family::String => quote!(::weaveffi::abi::string_run(__wv_val)), - Family::Bytes => quote!(::weaveffi::abi::bytes_run(__wv_val)), - Family::Buffer => quote!(::weaveffi::abi::buffer_run(&__wv_val)), - Family::Object { nullable: false } => { - typed(quote!(::weaveffi::abi::lower_object(__wv_val))) - } - Family::Object { nullable: true } => { + match pass { + ResultPass::Void => quote!(()), + ResultPass::Direct { .. } => quote!((::weaveffi::abi::Scalar::to_abi(&__wv_val),)), + ResultPass::OptDirect { .. } => quote!(::weaveffi::abi::opt_run(__wv_val)), + ResultPass::Slice { .. } => quote!(::weaveffi::abi::slice_run(&__wv_val[..])), + ResultPass::String { .. } => quote!(::weaveffi::abi::string_run(&__wv_val)), + ResultPass::Bytes { .. } => quote!(::weaveffi::abi::bytes_run(&__wv_val)), + ResultPass::Buffer { .. } => quote!(::weaveffi::abi::buffer_run(&__wv_val)), + ResultPass::Object { + nullable: false, .. + } => typed(quote!(::weaveffi::abi::lower_object(__wv_val))), + ResultPass::Object { nullable: true, .. } => { typed(quote!(::weaveffi::abi::lower_object_opt(__wv_val))) } - Family::Callback { .. } | Family::Iterator => { - unreachable!("validation never admits {ty} as an async result") - } } } + +/// The statements checking `_next`'s out slots are non-null (run before an +/// element is pulled, so a bad call doesn't lose one), and the statements +/// writing an element `__wv_item` to them. +pub(crate) fn lower_item( + pass: &ItemPass, + object: Option<&TokenStream>, +) -> (TokenStream, TokenStream) { + let null_check = pass.slots().into_iter().map(|p| { + let s = slot(&p.name); + let message = format!("{} is null", p.name); + quote! { + if #s.is_null() { + return ::std::result::Result::Err(::weaveffi::abi::FfiError::new( + ::weaveffi::abi::MARSHAL_ERROR_CODE, + #message, + )); + } + } + }); + let typed = |call: TokenStream| match object { + Some(obj) => quote!({ let __wv_p: *mut #obj = #call; __wv_p }), + None => call, + }; + let write = match pass { + ItemPass::Direct { out_item } => { + let o = slot(&out_item.name); + quote!(*#o = ::weaveffi::abi::Scalar::to_abi(&__wv_item);) + } + ItemPass::OptDirect { out_has, out_item } => { + let (h, o) = (slot(&out_has.name), slot(&out_item.name)); + quote!(*#h = ::weaveffi::abi::lower_opt_ret(__wv_item, #o);) + } + ItemPass::Slice { + out_item, out_len, .. + } => { + let (o, l) = (slot(&out_item.name), slot(&out_len.name)); + quote!(*#o = ::weaveffi::abi::lower_slice_ret(&__wv_item[..], #l);) + } + ItemPass::String { out_item, out_len } => { + let (o, l) = (slot(&out_item.name), slot(&out_len.name)); + quote!(*#o = ::weaveffi::abi::lower_string_ret(&__wv_item, #l);) + } + ItemPass::Bytes { out_item, out_len } => { + let (o, l) = (slot(&out_item.name), slot(&out_len.name)); + quote!(*#o = ::weaveffi::abi::lower_bytes_ret(&__wv_item, #l);) + } + ItemPass::Buffer { out_item, out_len } => { + let (o, l) = (slot(&out_item.name), slot(&out_len.name)); + quote!(*#o = ::weaveffi::abi::lower_buffer_ret(&__wv_item, #l);) + } + ItemPass::Object { + out_item, + nullable: false, + .. + } => { + let o = slot(&out_item.name); + let v = typed(quote!(::weaveffi::abi::lower_object(__wv_item))); + quote!(*#o = #v;) + } + ItemPass::Object { + out_item, + nullable: true, + .. + } => { + let o = slot(&out_item.name); + let v = typed(quote!(::weaveffi::abi::lower_object_opt(__wv_item))); + quote!(*#o = #v;) + } + }; + (quote!(#(#null_check)*), write) +} diff --git a/crates/weaveffi-macros/src/codegen/mod.rs b/crates/weaveffi-macros/src/codegen/mod.rs index ac92f465..61933499 100644 --- a/crates/weaveffi-macros/src/codegen/mod.rs +++ b/crates/weaveffi-macros/src/codegen/mod.rs @@ -2,31 +2,35 @@ //! thunks. //! //! The flow mirrors the rest of WeaveFFI: extract the annotated module tree -//! to the IR via [`weaveffi_model::rust`], validate it with the shared -//! validator (tolerating names declared in another tree, which must be -//! records or rich enums) to build the canonical [`Model`], then render -//! each lowered symbol. A validation error becomes a compile error on the -//! offending item ([`diagnostics`]). Signatures come straight from the model -//! (so they match the generated header by construction); the bodies call -//! the runtime's per-family lifting and lowering functions (`weaveffi::abi`), -//! so every `unsafe` operation has one audited home. +//! to the IR ([`crate::extract`]), validate it with the shared validator +//! (passing the names declared in another tree, which must be records or +//! rich enums) to build the canonical [`Model`], then render each lowered +//! symbol. A validation error becomes a compile error on the offending item +//! ([`diagnostics`]). Signatures come straight from the model (so they match +//! the generated header by construction) and the bodies follow the passing +//! contracts it stores on every binding, calling the runtime's lifting and +//! lowering functions (`weaveffi::abi`), so every `unsafe` operation has one +//! audited home. //! //! The emission is split by surface: [`sync`] for synchronous callables, //! [`async_fns`] for `async fn` launchers, [`iterators`] for `iter` trios, //! [`records`] and [`enums`] for the generated `BufferValue` serialization -//! impls of value types, [`interfaces`] for the object reference-count -//! symbols, [`callbacks`] for callback-interface vtables and foreign -//! wrappers, [`contract`] for the module's contract table, [`meta`] for the -//! library metadata the CLI reads the API from, and [`foreign`] for the -//! compile-time checks on types declared in another module tree. -//! [`helpers`] and [`lift`] hold the shared slot rendering and the -//! lift/lower dispatch. An item's `#[cfg]` wraps everything generated for it. +//! impls of value types, [`errors`] for error domains, [`interfaces`] for +//! the object reference-count symbols, [`callbacks`] for callback-interface +//! vtables and foreign wrappers, [`custom`] for custom types, [`contract`] +//! for the module's contract table, [`meta`] for the library metadata the +//! CLI reads the API from, and [`foreign`] for the compile-time checks on +//! types declared in another module tree. [`helpers`] and [`lift`] hold the +//! shared slot rendering and the lift/lower dispatch. An item's `#[cfg]` +//! wraps everything generated for it. mod async_fns; mod callbacks; mod contract; +mod custom; mod diagnostics; mod enums; +mod errors; mod foreign; mod helpers; mod interfaces; @@ -40,11 +44,13 @@ use std::collections::{BTreeSet, HashMap}; use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; -use weaveffi_model::ir::{Api, TypeRef, CURRENT_SCHEMA_VERSION}; -use weaveffi_model::model::{ErrorBinding, Model, ModuleBinding}; -use weaveffi_model::rust::SourceMap; +use weaveffi_model::ir::{Api, CURRENT_SCHEMA_VERSION}; +use weaveffi_model::model::ModuleBinding; use weaveffi_model::validate::{validate_scoped, Options}; +use crate::extract::{has_marker, is_weaveffi_attr, Customs, SourceMap}; + +use self::custom::CustomScope; pub(crate) use self::helpers::ident; use self::helpers::{cfg_wrap, CallTarget}; use self::sync::gen_function; @@ -68,37 +74,29 @@ pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { // 1. Extract the whole tree (it recurses into nested // `#[weaveffi::module]` submodules), recording where each declaration // came from and its `#[cfg]`. - let (module_ir, source) = weaveffi_model::rust::extract_module(item_mod)?; + let extraction = crate::extract::extract_module(item_mod)?; let prefix = prefix()?; // 2. Validate and build the model. A module tree is expanded in // isolation, so a type declared in a *sibling* tree (`orders` using - // `products::Product`) can't be resolved here; `foreign_names` - // accepts such names as records or rich enums, `unresolved` (below) - // lists them, and `foreign` asserts each one really is one. + // `products::Product`) can't be resolved here; such names are passed + // as foreign (records or rich enums), and `foreign` asserts each one + // really is one. The primitives no IDL type covers stay unknown, so + // validation reports them. let api = Api { version: CURRENT_SCHEMA_VERSION.to_string(), - modules: vec![module_ir], + modules: vec![extraction.module], }; let identity = weaveffi_model::pkg::Identity::named(&prefix); - let model = validate_scoped( - &api, - &identity, - Options { - foreign_names: true, - }, - ) - .map_err(|found| diagnostics::validation_errors(found, &source, item_mod.ident.span()))?; - let mut unresolved = BTreeSet::new(); - api.for_each_type_ref(&mut |ty| { - ty.walk(&mut |t| { - if let TypeRef::Named(name) = t { - if model.types.get(name).is_none() { - unresolved.insert(name.clone()); - } - } - }); - }); + let mut foreign_names = api.undeclared_type_names(); + foreign_names.retain(|n| !matches!(n.as_str(), "u128" | "i128")); + let options = Options { + foreign: foreign_names, + }; + let model = validate_scoped(&api, &identity, &options).map_err(|found| { + diagnostics::validation_errors(found, &extraction.source, item_mod.ident.span()) + })?; + let unresolved: BTreeSet = model.types.foreign().map(str::to_string).collect(); let by_path: HashMap, &ModuleBinding> = model .modules .iter() @@ -106,33 +104,82 @@ pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { .collect(); // 3. Rebuild the tree, injecting each module's thunks into its own body - // and stripping inner `#[weaveffi::module]` markers so nested modules - // expand here (with the right symbol path) instead of standalone. + // and stripping the WeaveFFI attributes so nested modules expand here + // (with the right symbol path) instead of standalone, and so a marker + // left to expand on its own is one used outside any module. let ctx = Expansion { - model: &model, tree: &api.modules[0], prefix: &prefix, by_path: &by_path, unresolved: &unresolved, - source: &source, + source: &extraction.source, + customs: &extraction.customs, }; let root = model .roots() .next() .ok_or_else(|| syn::Error::new_spanned(&item_mod.ident, "internal error: no root"))?; - let contract_fn = contract::gen_contract(&model, root, &source, &prefix); - ctx.rebuild_module(item_mod, &[], contract_fn) + let mut extra = contract::gen_contract(&model, root, &extraction.source, &prefix); + // Every module tree needs the runtime symbols; `export_runtime!()` + // defines this module at the crate root, so forgetting it (or calling it + // anywhere else) fails here, at the module's name. + let span = item_mod.ident.span(); + extra.extend(quote_spanned! {span=> + #[allow(unused_imports)] + use crate::__weaveffi_runtime as _; + }); + ctx.rebuild_module(item_mod, &[], extra) } /// What every module in one expansion shares. struct Expansion<'a> { - model: &'a Model, /// The extracted tree, which the library metadata describes. tree: &'a weaveffi_model::ir::Module, prefix: &'a str, by_path: &'a HashMap, &'a ModuleBinding>, unresolved: &'a BTreeSet, source: &'a SourceMap, + customs: &'a Customs, +} + +/// Remove every WeaveFFI attribute (markers and `#[weaveffi(...)]` helpers) +/// from an item the module macro re-emits, including those on its impl +/// items, trait items, and enum variants. +fn strip_item(item: &mut syn::Item) { + let strip = |attrs: &mut Vec| attrs.retain(|a| !is_weaveffi_attr(a)); + match item { + syn::Item::Fn(f) => strip(&mut f.attrs), + syn::Item::Struct(s) => { + strip(&mut s.attrs); + for field in s.fields.iter_mut() { + strip(&mut field.attrs); + } + } + syn::Item::Enum(e) => { + strip(&mut e.attrs); + for v in &mut e.variants { + strip(&mut v.attrs); + } + } + syn::Item::Trait(t) => { + strip(&mut t.attrs); + for ti in &mut t.items { + if let syn::TraitItem::Fn(f) = ti { + strip(&mut f.attrs); + } + } + } + syn::Item::Impl(i) => { + strip(&mut i.attrs); + for ii in &mut i.items { + if let syn::ImplItem::Fn(f) = ii { + strip(&mut f.attrs); + } + } + } + syn::Item::Type(t) => strip(&mut t.attrs), + _ => {} + } } impl Expansion<'_> { @@ -160,7 +207,7 @@ impl Expansion<'_> { ) })?; - let mut generated = self.render_symbols(mb, items, &item_mod.ident)?; + let mut generated = self.render_symbols(mb, items, &item_mod.ident, &segments)?; generated.extend(foreign::by_value_assertions(items, self.unresolved)); generated.extend(meta::gen_metadata( self.tree, @@ -169,23 +216,22 @@ impl Expansion<'_> { |names| self.cfg(mb, names), )); - // Pass items through verbatim, except nested `#[weaveffi::module]`s, - // which expand inline (recursively) with their marker stripped. + // Pass items through without their WeaveFFI attributes, except + // nested `#[weaveffi::module]`s, which expand inline (recursively). let mut body = TokenStream::new(); for item in items { if let syn::Item::Mod(child) = item { - if weaveffi_model::rust::has_marker(&child.attrs, "module") { + if has_marker(&child.attrs, "module") { body.extend(self.rebuild_module(child, &segments, TokenStream::new())?); continue; } } + let mut item = item.clone(); + strip_item(&mut item); body.extend(quote!(#item)); } - let attrs = item_mod - .attrs - .iter() - .filter(|a| !weaveffi_model::rust::has_marker(std::slice::from_ref(a), "module")); + let attrs = item_mod.attrs.iter().filter(|a| !is_weaveffi_attr(a)); let vis = &item_mod.vis; let mod_token = &item_mod.mod_token; let name = &item_mod.ident; @@ -211,35 +257,23 @@ impl Expansion<'_> { out } - /// The path, relative to `mb`, of the error domain in scope there. - fn domain_path(&self, mb: &ModuleBinding) -> Option { - let domain = self.model.error_domain(mb)?; - let owner = self - .model - .modules - .iter() - .find(|m| m.errors.as_ref().is_some_and(|e| e.name == domain.name))?; - let ups = mb.segments.len() - owner.segments.len(); - let supers = std::iter::repeat_n(quote!(super::), ups); - let name = ident(&domain.name); - Some(quote!(#(#supers)* #name)) - } - /// Render every C ABI symbol for one lowered module binding. /// /// `items` are the syn items directly in that module's body; they are - /// indexed so body marshalling can read reference-ness and `Result` - /// returns from the producer's signatures. `mod_ident` anchors - /// module-level diagnostics. + /// indexed so body marshalling can read the producer's written types and + /// `Result` returns. `mod_ident` anchors module-level diagnostics. fn render_symbols( &self, mb: &ModuleBinding, items: &[syn::Item], mod_ident: &syn::Ident, + segments: &[String], ) -> syn::Result { let prefix = self.prefix; + let customs = CustomScope::new(self.customs, segments); let mut fns: HashMap = HashMap::new(); let mut enums: HashMap = HashMap::new(); + let mut structs: HashMap = HashMap::new(); let mut traits: HashMap = HashMap::new(); // Interface member signatures, keyed by `(type name, fn name)` across // all inherent `impl` blocks of the type. @@ -252,6 +286,9 @@ impl Expansion<'_> { syn::Item::Enum(e) => { enums.insert(e.ident.to_string(), e); } + syn::Item::Struct(s) => { + structs.insert(s.ident.to_string(), s); + } syn::Item::Trait(t) => { traits.insert(t.ident.to_string(), t); } @@ -277,27 +314,37 @@ impl Expansion<'_> { }; let mut generated = TokenStream::new(); - if let Some(eb) = &mb.errors { + for def in self.customs.values().filter(|d| d.module == segments) { + generated.extend(custom::gen_custom(def)); + } + for eb in &mb.errors { let item = enums .get(&eb.name) .ok_or_else(|| missing("error domain", &eb.name))?; generated.extend(cfg_wrap( &self.cfg(mb, &[&eb.name]), - gen_error_domain(eb, item), + errors::gen_error_domain(eb, item, customs)?, )); } for e in &mb.enums { - generated.extend(cfg_wrap(&self.cfg(mb, &[&e.name]), enums::gen_enum(e))); + let item = enums.get(&e.name).copied(); + generated.extend(cfg_wrap( + &self.cfg(mb, &[&e.name]), + enums::gen_enum(e, item, customs), + )); } for s in &mb.structs { - generated.extend(cfg_wrap(&self.cfg(mb, &[&s.name]), records::gen_record(s))); + let item = structs.get(&s.name).copied(); + generated.extend(cfg_wrap( + &self.cfg(mb, &[&s.name]), + records::gen_record(s, item, customs), + )); } - let domain = self.domain_path(mb); for c in &mb.callback_interfaces { let item = traits .get(&c.name) .ok_or_else(|| missing("callback interface", &c.name))?; - let code = callbacks::gen_callback_interface(c, item, domain.as_ref(), prefix)?; + let code = callbacks::gen_callback_interface(c, item, customs, prefix)?; generated.extend(cfg_wrap(&self.cfg(mb, &[&c.name]), code)); } for i in &mb.interfaces { @@ -311,7 +358,7 @@ impl Expansion<'_> { let sig = member_sigs .get(&(i.name.clone(), m.name.clone())) .ok_or_else(|| missing("interface member", &m.name))?; - let code = gen_function(m, sig, &target, prefix)?; + let code = gen_function(m, sig, &target, customs, prefix)?; generated.extend(cfg_wrap(&self.cfg(mb, &[&i.name, &m.name]), code)); } } @@ -324,7 +371,7 @@ impl Expansion<'_> { let sfn = fns .get(&f.name) .ok_or_else(|| missing("function", &f.name))?; - let code = gen_function(f, &sfn.sig, &CallTarget::Free, prefix)?; + let code = gen_function(f, &sfn.sig, &CallTarget::Free, customs, prefix)?; generated.extend(cfg_wrap(&self.cfg(mb, &[&f.name]), code)); } Ok(generated) @@ -340,121 +387,6 @@ fn impl_type_name(item_impl: &syn::ItemImpl) -> Option { p.path.segments.last().map(|s| s.ident.to_string()) } -/// Generate the [`ErrorReport`](weaveffi::abi::ErrorReport) and -/// [`ErrorDomain`](weaveffi::abi::ErrorDomain) implementations for a -/// module's `#[weaveffi::error]` enum: each variant maps to its declared -/// code, the message is the enum's `Display` output, and a payload -/// variant's fields are serialized into (and decoded from) the error's -/// value-buffer payload. This is what routes `Err(Domain::Case)` from a -/// throwing producer function to the matching C error constant, and a -/// callback's domain error back to `Domain::Case`. -fn gen_error_domain(eb: &ErrorBinding, item: &syn::ItemEnum) -> TokenStream { - let ty = ident(&eb.name); - // A payload-carrying variant matches with `{ .. }`; a unit variant by name. - let pattern = |c: &weaveffi_model::model::ErrorCodeBinding| { - let v = ident(&c.name); - if c.fields.is_empty() { - quote!(Self::#v) - } else { - quote!(Self::#v { .. }) - } - }; - let code_arms = eb.codes.iter().map(|c| { - let pat = pattern(c); - let value = c.value; - quote!(#pat => #value,) - }); - let payload_arms: Vec = eb - .codes - .iter() - .filter(|c| !c.fields.is_empty()) - .map(|c| { - let v = ident(&c.name); - let bindings: Vec = c.fields.iter().map(|f| ident(&f.name)).collect(); - quote! { - Self::#v { #(#bindings),* } => { - let mut __wv_w = ::weaveffi::abi::BufferWriter::with_capacity( - 0 #(+ ::weaveffi::abi::BufferValue::encoded_len(#bindings))* - ); - #(::weaveffi::abi::BufferValue::write_value(#bindings, &mut __wv_w);)* - __wv_w.finish() - } - } - }) - .collect(); - let payload_fn = if payload_arms.is_empty() { - TokenStream::new() - } else { - quote! { - fn payload(&self) -> ::std::vec::Vec { - match self { - #(#payload_arms)* - _ => ::std::vec::Vec::new(), - } - } - } - }; - let read_arms = eb.codes.iter().map(|c| { - let v = ident(&c.name); - let value = c.value; - if c.fields.is_empty() { - quote!(#value => Self::#v,) - } else { - let names: Vec = c.fields.iter().map(|f| ident(&f.name)).collect(); - quote! { - #value => Self::#v { - #(#names: ::weaveffi::abi::BufferValue::read_value(__wv_r)?),* - }, - } - } - }); - // Spanned at the enum so a missing `Display` impl is one error that - // points at it and names the requirement. - let user_ty = &item.ident; - let message = quote_spanned! {user_ty.span()=> - fn __weaveffi_error_domains_must_implement_display< - T: ::std::fmt::Display + ?::std::marker::Sized, - >( - e: &T, - ) -> ::std::string::String { - ::std::string::ToString::to_string(e) - } - __weaveffi_error_domains_must_implement_display(self) - }; - quote! { - impl ::weaveffi::abi::ErrorReport for #ty { - fn code(&self) -> i32 { - match self { - #(#code_arms)* - } - } - fn message(&self) -> ::std::string::String { - #message - } - #payload_fn - } - - #[allow(unsafe_code, unused_unsafe)] - impl ::weaveffi::abi::ErrorDomain for #ty { - unsafe fn read_code( - code: i32, - __wv_r: &mut ::weaveffi::abi::BufferReader<'_>, - ) -> ::std::result::Result< - ::std::option::Option, - ::weaveffi::abi::BufferDecodeError, - > { - // SAFETY: forwarded from the caller. - ::std::result::Result::Ok(::std::option::Option::Some(unsafe { - match code { - #(#read_arms)* - _ => return ::std::result::Result::Ok(::std::option::Option::None), - } - })) - } - } - } -} - /// Build an "unsupported" error for a type shape the macro can't marshal, /// pointing at `at` (the offending type's tokens). fn unsupported(at: TokenStream, what: &str, kind: &str) -> syn::Error { diff --git a/crates/weaveffi-macros/src/codegen/records.rs b/crates/weaveffi-macros/src/codegen/records.rs index f871d1c3..22162c11 100644 --- a/crates/weaveffi-macros/src/codegen/records.rs +++ b/crates/weaveffi-macros/src/codegen/records.rs @@ -1,6 +1,7 @@ //! Codegen for records: the generated [`weaveffi::abi::BufferValue`] //! implementation that serializes the producer's struct field by field in -//! declaration (wire) order. +//! declaration (wire) order, and the field codec records, rich enums, and +//! error payloads share. //! //! A record is a value type: it declares no C symbols of its own and crosses //! the ABI serialized inside a value buffer, so all a record needs from the @@ -11,27 +12,94 @@ use proc_macro2::TokenStream; use quote::quote; use weaveffi_model::model::StructBinding; +use super::custom::{CustomScope, LiftSite}; use super::helpers::ident; +/// How one field encodes: its `encoded_len` expression, its `write_value` +/// statement (writing to `__wv_w`), and its `read_value` expression +/// (reading from `__wv_r`, propagating with `?`). +pub(crate) struct FieldCodec { + pub(crate) len: TokenStream, + pub(crate) write: TokenStream, + pub(crate) read: TokenStream, +} + +/// The codec of a field reached through `access` (an expression of type +/// `&T`), declared with the written type `ty`. A field whose type mentions +/// a custom type encodes as its repr. +pub(crate) fn field_codec( + access: &TokenStream, + ty: Option<&syn::Type>, + customs: CustomScope<'_>, +) -> FieldCodec { + match ty.and_then(|t| customs.shape(t)) { + Some(shape) => { + let repr = shape.repr_ty(); + let lowered = shape.lower(access.clone()); + let lifted = shape.lift(quote!(__wv_repr), LiftSite::Buffer); + FieldCodec { + len: quote!(::weaveffi::abi::BufferValue::encoded_len(&#lowered)), + write: quote!(::weaveffi::abi::BufferValue::write_value(&#lowered, __wv_w);), + read: quote!({ + let __wv_repr: #repr = ::weaveffi::abi::BufferValue::read_value(__wv_r)?; + #lifted? + }), + } + } + None => FieldCodec { + len: quote!(::weaveffi::abi::BufferValue::encoded_len(#access)), + write: quote!(::weaveffi::abi::BufferValue::write_value(#access, __wv_w);), + read: quote!(::weaveffi::abi::BufferValue::read_value(__wv_r)?), + }, + } +} + +/// The written type of the named field `name` in `fields`. +pub(crate) fn field_type<'a>(fields: Option<&'a syn::Fields>, name: &str) -> Option<&'a syn::Type> { + fields? + .iter() + .find(|f| f.ident.as_ref().is_some_and(|i| i == name)) + .map(|f| &f.ty) +} + /// Generate the `BufferValue` and `ByValue` implementations for one record. -pub(crate) fn gen_record(s: &StructBinding) -> TokenStream { +pub(crate) fn gen_record( + s: &StructBinding, + item: Option<&syn::ItemStruct>, + customs: CustomScope<'_>, +) -> TokenStream { let rust_ty = ident(&s.name); let names: Vec = s.fields.iter().map(|f| ident(&f.name)).collect(); + let codecs: Vec = s + .fields + .iter() + .zip(&names) + .map(|(f, n)| { + field_codec( + "e!(&self.#n), + field_type(item.map(|i| &i.fields), &f.name), + customs, + ) + }) + .collect(); + let lens = codecs.iter().map(|c| &c.len); + let writes = codecs.iter().map(|c| &c.write); + let reads = codecs.iter().map(|c| &c.read); quote! { #[allow(unsafe_code, unused_unsafe)] impl ::weaveffi::abi::BufferValue for #rust_ty { fn encoded_len(&self) -> usize { - 0 #(+ ::weaveffi::abi::BufferValue::encoded_len(&self.#names))* + 0 #(+ #lens)* } fn write_value(&self, __wv_w: &mut ::weaveffi::abi::BufferWriter) { - #(::weaveffi::abi::BufferValue::write_value(&self.#names, __wv_w);)* + #(#writes)* } unsafe fn read_value( __wv_r: &mut ::weaveffi::abi::BufferReader<'_>, ) -> ::std::result::Result { // SAFETY: forwarded from the caller. ::std::result::Result::Ok(unsafe { - Self { #(#names: ::weaveffi::abi::BufferValue::read_value(__wv_r)?),* } + Self { #(#names: #reads),* } }) } } diff --git a/crates/weaveffi-macros/src/codegen/sync.rs b/crates/weaveffi-macros/src/codegen/sync.rs index 1e0c88fa..4cf8a306 100644 --- a/crates/weaveffi-macros/src/codegen/sync.rs +++ b/crates/weaveffi-macros/src/codegen/sync.rs @@ -3,34 +3,45 @@ use proc_macro2::TokenStream; use quote::{quote, quote_spanned}; -use weaveffi_model::model::{AbiFn, CallShape, FnBinding}; -use weaveffi_model::plan::ErrorStrategy; +use weaveffi_model::model::{CallShape, FnBinding}; +use weaveffi_model::plan::{ErrorStrategy, RetPass}; use super::async_fns::gen_async_function; +use super::custom::CustomScope; use super::helpers::{ fn_slots, ident, ret_arrow_for, ret_type_for, thunk_attrs, CallTarget, UserSig, }; use super::iterators::gen_iterator_function; -use super::lift::{lift_param, lower_ret, Lifts}; +use super::lift::{lift_param, lower_custom_ret, lower_ret, slot_owners, Lifts}; -/// Whether this callable routes typed domain errors through `out_err`, per -/// the plan's error contract ([`weaveffi_model::plan::ErrorStrategy`]). The -/// producer declared it by returning a `Result`, so the thunk propagates the -/// call's `Err` as the domain error. -pub(crate) fn throws(f: &FnBinding) -> bool { - f.error_strategy() == ErrorStrategy::Throws +/// The expression mapping the producer's `Err(e)` (bound to `__wv_e`) to the +/// `FfiError` the callable reports, per its error strategy: a declared +/// domain's own code, message, and payload, or (`throws any`) the generic +/// code with `e`'s `Display`. `None` for a callable that doesn't throw. +/// +/// The conversion is spanned on the producer's error type, so a missing +/// `Display` (for `throws any`) is reported there. +pub(crate) fn report_error(f: &FnBinding, user: &UserSig<'_>) -> Option { + let span = user + .ret_error_spelled() + .map_or_else(|| user.ret_type_span(), |(_, span)| span); + match &f.error { + ErrorStrategy::Trap => None, + ErrorStrategy::Domain(_) => Some(quote_spanned! {span=> + ::weaveffi::abi::FfiError::from_domain(&__wv_e) + }), + ErrorStrategy::Untyped => Some(quote_spanned! {span=> + ::weaveffi::abi::FfiError::untyped(&__wv_e) + }), + } } /// The producer call, with a throwing callable's `Err` turned into its /// `FfiError` and propagated with `?`. pub(crate) fn checked_call(f: &FnBinding, call: TokenStream, user: &UserSig<'_>) -> TokenStream { - if throws(f) { - let span = user.ret_type_span(); - quote_spanned! {span=> - #call.map_err(|__wv_e| ::weaveffi::abi::FfiError::from_report(&__wv_e))? - } - } else { - call + match report_error(f, user) { + Some(report) => quote!(#call.map_err(|__wv_e| #report)?), + None => call, } } @@ -55,13 +66,16 @@ pub(crate) fn gen_function( f: &FnBinding, sig: &syn::Signature, target: &CallTarget, + customs: CustomScope<'_>, prefix: &str, ) -> syn::Result { - let user = UserSig::new(sig, target.self_ty()); - match &f.shape { - CallShape::Sync(abi) => gen_sync_function(f, abi, &user, target, prefix), - CallShape::Iterator(it) => gen_iterator_function(f, it, &user, target, prefix), - CallShape::Async(a) => gen_async_function(f, a, &user, target, prefix), + let user = UserSig::new(sig, target.self_ty(), customs); + match (&f.shape, &f.ret_pass) { + (CallShape::Async(a), _) => gen_async_function(f, a, &user, target, prefix), + (CallShape::Sync, RetPass::Iterator(it)) => { + gen_iterator_function(f, it, &user, target, prefix) + } + (CallShape::Sync, _) => gen_sync_function(f, &user, target, prefix), } } @@ -70,30 +84,30 @@ pub(crate) fn gen_function( /// `weaveffi::abi::call_sync`, which catches panics and fills `out_err`. fn gen_sync_function( f: &FnBinding, - abi: &AbiFn, user: &UserSig<'_>, target: &CallTarget, prefix: &str, ) -> syn::Result { - let sym = ident(&abi.symbol); - let params = fn_slots(&abi.params, &f.params, user, prefix)?; - let arrow = ret_arrow_for(&abi.ret, f.ret.as_ref(), user, prefix); - let ret_ty = ret_type_for(&abi.ret, f.ret.as_ref(), user, prefix); + let sym = ident(&f.abi.symbol); + let params = fn_slots(&f.abi.params, &slot_owners(&f.params), user, prefix)?; + let object_ret = matches!(f.ret_pass, RetPass::Object { .. }); + let arrow = ret_arrow_for(&f.abi.ret, object_ret, user, prefix); + let ret_ty = ret_type_for(&f.abi.ret, object_ret, user, prefix); let lifts = sync_lifts(f, user, target)?; let lifted = lifts.finish(); let call = checked_call(f, target.call(&f.name, &lifts.args), user); - let lowered = match &f.ret { - Some(ty) => lower_ret(ty, "e!(__wv_ret), user.ret_object().as_ref()), - None => quote!(__wv_ret), - }; + let ret = quote!(__wv_ret); + let custom = lower_custom_ret(user, &ret); + let lowered = lower_ret(&f.ret_pass, &ret, user.ret_object().as_ref()); let attrs = thunk_attrs(); Ok(quote! { #attrs pub unsafe extern "C" fn #sym(#(#params),*) #arrow { unsafe { - ::weaveffi::abi::call_sync::<#ret_ty>(out_err, move || unsafe { + ::weaveffi::abi::call_sync::<#ret_ty>(__wv_out_err, move || unsafe { #lifted let __wv_ret = #call; + #custom ::std::result::Result::Ok(#lowered) }) } diff --git a/crates/weaveffi-model/src/rust.rs b/crates/weaveffi-macros/src/extract.rs similarity index 75% rename from crates/weaveffi-model/src/rust.rs rename to crates/weaveffi-macros/src/extract.rs index b120f2bd..0da55262 100644 --- a/crates/weaveffi-model/src/rust.rs +++ b/crates/weaveffi-macros/src/extract.rs @@ -2,61 +2,70 @@ //! //! This is the `#[weaveffi::module]` proc-macro's reader: it lowers a module //! tree to its IR ([`extract_module`]), which the macro validates, builds -//! the C ABI scaffolding from, and embeds in the library as metadata (see -//! [`meta`](crate::meta)). The CLI never reads Rust source; it reads that -//! metadata back out of the built library, so the API it generates bindings -//! for is, by construction, the one the library was compiled with. +//! the C ABI scaffolding from, and embeds in the library as metadata. The +//! CLI never reads Rust source; it reads that metadata back out of the built +//! library, so the API it generates bindings for is, by construction, the +//! one the library was compiled with. //! //! The extraction reports only what syntax alone rules out (a raw pointer, //! a tuple variant, a `#[cfg]` on a member); every other rule is the //! validator's. Alongside the IR, [`extract_module`] returns a [`SourceMap`] //! recording where each declaration came from and the `#[cfg]` attributes on //! it, so the macro can point a validation error at the offending item and -//! apply each item's `#[cfg]` to its generated code. +//! apply each item's `#[cfg]` to its generated code, and the tree's +//! [`Customs`] (the `#[weaveffi::custom]` type aliases). //! //! # The annotation scheme //! //! A `#[weaveffi::module]` on an inline `mod` marks an exported namespace. //! Inside it: //! -//! * `#[weaveffi::export]` on a `fn` exports a function. An `async fn` lowers to -//! an asynchronous symbol; a `fn -> Result` is fallible (`throws` in -//! the IDL; the return type is `T` and errors are reported through the ABI's -//! `out_err`). +//! * `#[weaveffi::export]` on a `fn` exports a function. An `async fn` lowers +//! to an asynchronous symbol. A `fn -> Result` is fallible: it +//! `throws: E` when `E` is a `#[weaveffi::error]` enum of the module tree, +//! and `throws: any` (an untyped error reported with `E`'s `Display`) +//! otherwise. //! * `#[weaveffi::interface]` on a `struct` declares an interface (opaque, -//! reference-counted object type). Its `impl` block's `pub fn`s become the -//! interface's members: an associated function returning `Self` (or -//! `Arc`) is a constructor, a `&self` or `self: Arc` function is -//! a method, and any other associated function is a static. +//! reference-counted object type). Its `impl` blocks' `pub fn`s become the +//! interface's members, except those marked `#[weaveffi::skip]`: an +//! associated function returning `Self` (or `Arc`) is a +//! constructor, a `&self` or `self: Arc` function is a method, and +//! any other associated function is a static. //! * `#[weaveffi::record]` on a `struct` declares a by-value record. //! * `#[weaveffi::enumeration]` on a `#[repr(i32)]` `enum` declares a C-style //! enum; an enum with data-carrying variants declares a rich enum. -//! * `#[weaveffi::error]` on an enum declares the module's error domain. +//! * `#[weaveffi::error]` on an enum declares an error domain (a module may +//! declare several). A variant's `#[weaveffi(message = "...")]` is its +//! message template. //! * `#[weaveffi::callback_interface]` on a `trait` declares a callback //! interface: a method set the consumer implements. Producers accept one as //! `Arc` (or `Option>`). Every method returns -//! `Result`, which maps to a `T` return; a -//! method marked `#[weaveffi::throws]` throws. -//! * `pub type Name = T;` in the tree is an alias: every use of `Name` is -//! extracted as `T`. +//! `Result`, which maps to a `T` return that throws `E` when `E` is a +//! domain of the tree and `any` otherwise. +//! * `#[weaveffi::custom(repr = R, lift = f, lower = g)]` on `pub type Name = +//! T;` declares a custom type: every use of `Name` is extracted as `R`. +//! * Any other `pub type Name = T;` in the tree is an alias: every use of +//! `Name` is extracted as `T`. //! -//! The type mapping mirrors the IDL: `String` (or `&str`) is a string, `Vec` -//! (or `&[u8]`) is a byte buffer, every integer primitive (including `u64`) is -//! the matching scalar, `Vec`, `Option`, and `HashMap`/`BTreeMap` are the -//! list, optional, and map types, `weaveffi::Iter` is an `iter` return, -//! `Arc` names the interface `T` (an object reference), `Arc` -//! names the callback interface `Trait`, and any other named path is a record, -//! enum, or interface resolved later. +//! The type mapping mirrors the IDL: `String` (or `&str`) is a string, +//! `Vec` (or `&[u8]`) is a byte buffer, every fixed-width integer +//! primitive is the matching scalar, `usize` and `isize` are `u64` and +//! `i64`, `char` is a `string`, `Vec`, `Option`, and +//! `HashMap`/`BTreeMap` are the list, optional, and map types, +//! `weaveffi::Iter` is an `iter` return, `Arc` names the interface +//! `T` (an object reference), `Arc` names the callback interface +//! `Trait`, and any other named path is a record, enum, or interface +//! resolved later. + +use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::collections::{BTreeMap, HashMap}; - -use crate::ir::{ - CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, - Module, Param, StructDef, StructField, TypeRef, -}; -use crate::ty::Prim; use proc_macro2::Span; use syn::spanned::Spanned; +use weaveffi_model::ir::{ + CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, + Module, Param, StructDef, StructField, Throws, TypeRef, +}; +use weaveffi_model::ty::Prim; /// Where each extracted declaration came from, keyed by its declaration /// path: module segments from the tree's root, then the declaration name, @@ -126,7 +135,7 @@ fn is_cfg(attr: &syn::Attribute) -> bool { fn no_member_cfg(attrs: &[syn::Attribute], what: &str, instead: &str) -> syn::Result<()> { match attrs.iter().find(|a| is_cfg(a)) { Some(attr) => Err(syn::Error::new( - attr.span(), + attr.path().span(), format!( "weaveffi: `#[cfg]` on {what} isn't supported, because the generated bindings \ can't follow it; {instead}" @@ -137,38 +146,144 @@ fn no_member_cfg(attrs: &[syn::Attribute], what: &str, instead: &str) -> syn::Re } /// The type aliases declared in a module tree (`pub type Id = u64;`), by -/// name. Generic aliases are left alone. +/// name. Generic aliases and custom types are left out. type Aliases = HashMap; -fn collect_aliases(item_mod: &syn::ItemMod, out: &mut Aliases) { +/// A `#[weaveffi::custom(repr = R, lift = f, lower = g)] pub type Name = +/// T;` declaration: `Name` crosses the ABI as `R`. +#[derive(Clone)] +pub struct CustomDef { + /// The alias name. + pub name: String, + /// The module segments (from the tree's root) of the module declaring + /// it. + pub module: Vec, + /// The repr type `R`, as written. + pub repr: syn::Type, + /// The `lift` expression: `fn(R) -> Result`. + pub lift: syn::Expr, + /// The `lower` expression: `fn(&T) -> R`. + pub lower: syn::Expr, +} + +/// The tree's custom types, by alias name. +pub type Customs = BTreeMap; + +/// Parse the arguments of `#[weaveffi::custom(...)]`. +fn parse_custom( + attr: &syn::Attribute, + item: &syn::ItemType, + module: &[String], +) -> syn::Result { + let (mut repr, mut lift, mut lower) = (None, None, None); + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("repr") { + repr = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("lift") { + lift = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("lower") { + lower = Some(meta.value()?.parse::()?); + } else { + return Err(meta.error( + "weaveffi: expected `repr = Type`, `lift = path`, or `lower = path` in \ + #[weaveffi::custom(...)]", + )); + } + Ok(()) + })?; + let missing = |what: &str| { + syn::Error::new( + attr.span(), + format!( + "weaveffi: #[weaveffi::custom] needs `{what}`; write \ + #[weaveffi::custom(repr = String, lift = parse_fn, lower = format_fn)]" + ), + ) + }; + if !item.generics.params.is_empty() { + return Err(syn::Error::new( + item.generics.span(), + "weaveffi: a #[weaveffi::custom] type alias can't be generic", + )); + } + Ok(CustomDef { + name: item.ident.to_string(), + module: module.to_vec(), + repr: repr.ok_or_else(|| missing("repr"))?, + lift: lift.ok_or_else(|| missing("lift"))?, + lower: lower.ok_or_else(|| missing("lower"))?, + }) +} + +/// What the extraction needs to know about the whole tree before it reads +/// any item: its aliases, custom types, and error domain names. +#[derive(Default)] +struct TreeNames { + aliases: Aliases, + customs: Customs, + domains: BTreeSet, +} + +fn collect_names( + item_mod: &syn::ItemMod, + parent: &[String], + out: &mut TreeNames, +) -> syn::Result<()> { let Some((_, items)) = &item_mod.content else { - return; + return Ok(()); }; + let path = at(parent, &[&item_mod.ident.to_string()]); for item in items { match item { - syn::Item::Type(t) if t.generics.params.is_empty() => { - out.insert(t.ident.to_string(), (*t.ty).clone()); + syn::Item::Type(t) => { + if let Some(attr) = t.attrs.iter().find(|a| is_marker(a, "custom")) { + let def = parse_custom(attr, t, &path)?; + out.customs.insert(def.name.clone(), def); + } else if t.generics.params.is_empty() { + out.aliases.insert(t.ident.to_string(), (*t.ty).clone()); + } + } + syn::Item::Enum(e) if has_marker(&e.attrs, "error") => { + out.domains.insert(e.ident.to_string()); } - syn::Item::Mod(m) if has_marker(&m.attrs, "module") => collect_aliases(m, out), + syn::Item::Mod(m) if has_marker(&m.attrs, "module") => collect_names(m, &path, out)?, _ => {} } } + Ok(()) } -/// The extraction context: the tree's aliases and the source map being +/// The extraction context: the tree's names and the source map being /// built. struct Extractor<'a> { - aliases: &'a Aliases, + names: &'a TreeNames, map: SourceMap, } -/// Match a WeaveFFI marker attribute by its final path segment. +/// Every attribute name the WeaveFFI macros define, which `#[weaveffi::module]` +/// strips from the items it re-emits. +pub const MARKERS: &[&str] = &[ + "module", + "export", + "interface", + "record", + "enumeration", + "error", + "callback_interface", + "cancellable", + "custom", + "skip", +]; + +/// Match a WeaveFFI marker attribute: `#[weaveffi::name]` (or +/// `#[::weaveffi::name]`), with or without arguments. /// -/// Accepts both the namespaced form (`#[weaveffi::record]`) and the bare form -/// (`#[record]`), so the macro's stripped re-emit and a hand-written attribute -/// resolve identically. -fn is_marker(attr: &syn::Attribute, name: &str) -> bool { - attr.path().segments.last().is_some_and(|s| s.ident == name) +/// Only the namespaced form counts, so an unrelated attribute that happens +/// to share a name (thiserror's `#[error("...")]`) is never mistaken for a +/// marker. +pub fn is_marker(attr: &syn::Attribute, name: &str) -> bool { + let segments = &attr.path().segments; + segments.len() == 2 && segments[0].ident == "weaveffi" && segments[1].ident == name } /// Whether any attribute in `attrs` is the WeaveFFI marker `name`. @@ -176,6 +291,77 @@ pub fn has_marker(attrs: &[syn::Attribute], name: &str) -> bool { attrs.iter().any(|a| is_marker(a, name)) } +/// Whether `attr` is a WeaveFFI marker or the `#[weaveffi(...)]` helper +/// attribute, either of which `#[weaveffi::module]` strips before +/// re-emitting an item. +pub fn is_weaveffi_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("weaveffi") || MARKERS.iter().any(|m| is_marker(attr, m)) +} + +/// The `#[weaveffi(message = "...")]` template on an error variant, if any. +/// +/// # Errors +/// +/// Returns an error for a malformed helper attribute. +pub fn variant_message(attrs: &[syn::Attribute]) -> syn::Result> { + let mut message = None; + for attr in attrs.iter().filter(|a| a.path().is_ident("weaveffi")) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("message") { + message = Some(meta.value()?.parse::()?); + Ok(()) + } else { + Err(meta + .error("weaveffi: the only option here is `#[weaveffi(message = \"...\")]`")) + } + })?; + } + Ok(message) +} + +/// The documented default message of an error variant: its doc comment's +/// first line, else its `#[weaveffi(message = "...")]` template, else its +/// name. +/// +/// # Errors +/// +/// Returns an error for a malformed `#[weaveffi(...)]` attribute. +pub fn default_message(attrs: &[syn::Attribute], name: &str) -> syn::Result { + if let Some(line) = extract_doc(attrs).and_then(|d| d.lines().next().map(str::to_string)) { + if !line.trim().is_empty() { + return Ok(line); + } + } + Ok(variant_message(attrs)?.map_or_else(|| name.to_string(), |m| m.value())) +} + +/// Whether an error enum opts out of the generated `Display` and +/// `std::error::Error` (`#[weaveffi::error(no_display)]`). +/// +/// # Errors +/// +/// Returns an error for an argument other than `no_display`. +pub fn error_no_display(attrs: &[syn::Attribute]) -> syn::Result { + let mut no_display = false; + for attr in attrs.iter().filter(|a| is_marker(a, "error")) { + if matches!(attr.meta, syn::Meta::Path(_)) { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("no_display") { + no_display = true; + Ok(()) + } else { + Err(meta.error( + "weaveffi: the only option of #[weaveffi::error] is `no_display` (implement \ + `Display` and `std::error::Error` yourself, with thiserror say)", + )) + } + })?; + } + Ok(no_display) +} + fn has_repr_i32(attrs: &[syn::Attribute]) -> bool { attrs.iter().any(|a| { a.path().is_ident("repr") && a.parse_args::().is_ok_and(|id| id == "i32") @@ -385,14 +571,16 @@ fn is_cancel_token(ty: &syn::Type) -> bool { } /// Map a Rust [`syn::Type`] onto the WeaveFFI [`TypeRef`] it represents, -/// substituting the tree's `aliases`. +/// substituting the tree's aliases and custom types. /// /// This is the canonical mapping every caller shares. Notable conventions: /// /// * `String` and `&str` are both the `string` type; `Vec` and `&[u8]` /// are both `bytes`. A reference is a producer-side calling convention (the /// thunk lifts an owned value and lends it), not an IDL distinction. -/// * every integer primitive maps to its matching scalar (`u64` included). +/// * every fixed-width integer primitive maps to its matching scalar; +/// `usize` and `isize` map to `u64` and `i64`, and `char` to `string`. +/// * a custom type maps to its repr. /// * `Vec` and `&[T]` are lists, `Option` is an optional, and /// `HashMap`/`BTreeMap` are maps. /// * `weaveffi::Iter` is an `iter` return. @@ -405,12 +593,12 @@ fn is_cancel_token(ty: &syn::Type) -> bool { /// Returns a spanned error for type syntax WeaveFFI cannot express across the /// FFI boundary (raw pointers, `Box`, tuples, a generic with the /// wrong arity, and so on). -fn type_ref_from_syn(ty: &syn::Type, aliases: &Aliases) -> syn::Result { - type_ref_at(ty, aliases, 0) +fn type_ref_from_syn(ty: &syn::Type, names: &TreeNames) -> syn::Result { + type_ref_at(ty, names, 0) } -fn type_ref_at(ty: &syn::Type, aliases: &Aliases, depth: usize) -> syn::Result { - let recurse = |t: &syn::Type| type_ref_at(t, aliases, depth); +fn type_ref_at(ty: &syn::Type, names: &TreeNames, depth: usize) -> syn::Result { + let recurse = |t: &syn::Type| type_ref_at(t, names, depth); match ty { syn::Type::Reference(r) => { if let syn::Type::Path(p) = r.elem.as_ref() { @@ -456,7 +644,9 @@ fn type_ref_at(ty: &syn::Type, aliases: &Aliases, depth: usize) -> syn::Result Ok(TypeRef::Prim(Prim::String)), + "String" | "char" => Ok(TypeRef::Prim(Prim::String)), + "usize" => Ok(TypeRef::Prim(Prim::U64)), + "isize" => Ok(TypeRef::Prim(Prim::I64)), // `Arc` is a reference to the interface object `T`, and // `Arc` a consumer-implemented callback interface; // the IR names the pointee in both cases. @@ -489,18 +679,21 @@ fn type_ref_at(ty: &syn::Type, aliases: &Aliases, depth: usize) -> syn::Result match aliases.get(other) { - Some(target) if matches!(seg.arguments, syn::PathArguments::None) => { - if depth > 16 { - return Err(syn::Error::new( - ty.span(), - format!("weaveffi: type alias `{other}` refers to itself"), - )); - } - type_ref_at(target, aliases, depth + 1) + other if matches!(seg.arguments, syn::PathArguments::None) => { + let target = match names.customs.get(other) { + Some(custom) => Some(&custom.repr), + None => names.aliases.get(other), + }; + match target { + Some(_) if depth > 16 => Err(syn::Error::new( + ty.span(), + format!("weaveffi: type alias `{other}` refers to itself"), + )), + Some(target) => type_ref_at(target, names, depth + 1), + None => Ok(TypeRef::Named(other.to_string())), } - _ => Ok(TypeRef::Named(other.to_string())), - }, + } + other => Ok(TypeRef::Named(other.to_string())), } } _ => Err(syn::Error::new(ty.span(), "unsupported type syntax")), @@ -525,14 +718,35 @@ pub fn peel_result(ty: &syn::Type) -> &syn::Type { ty } -/// Whether a function's return type is a `Result<_, _>`, which marks the -/// function as `throws` in the IDL. -pub fn output_is_result(output: &syn::ReturnType) -> bool { +/// The error type `E` of a `Result` return, if the return is one. +pub fn result_error(output: &syn::ReturnType) -> Option<&syn::Type> { let syn::ReturnType::Type(_, ty) = output else { - return false; + return None; }; - matches!(ty.as_ref(), syn::Type::Path(p) - if p.path.segments.last().is_some_and(|s| s.ident == "Result")) + let syn::Type::Path(p) = ty.as_ref() else { + return None; + }; + let seg = p.path.segments.last()?; + if seg.ident != "Result" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + match args.args.iter().nth(1) { + Some(syn::GenericArgument::Type(err)) => Some(err), + _ => None, + } +} + +/// What a callable with the error type `err` throws: the domain it names +/// when it's a `#[weaveffi::error]` enum of the tree (by its final path +/// segment), else `any`. +fn throws_of(err: &syn::Type, domains: &BTreeSet) -> Throws { + match type_path_ident(err) { + Some(name) if domains.contains(&name) => Throws::Domain(name), + _ => Throws::Any, + } } fn is_unit(ty: &syn::Type) -> bool { @@ -567,32 +781,6 @@ fn at(path: &[String], names: &[&str]) -> Vec { out } -/// Whether a return type is `Result`, the return type every -/// callback-interface method must have. -pub fn returns_foreign_result(output: &syn::ReturnType) -> bool { - let syn::ReturnType::Type(_, ty) = output else { - return false; - }; - let syn::Type::Path(p) = ty.as_ref() else { - return false; - }; - let Some(seg) = p.path.segments.last() else { - return false; - }; - if seg.ident != "Result" { - return false; - } - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return false; - }; - match args.args.iter().nth(1) { - Some(syn::GenericArgument::Type(err)) => { - type_path_ident(err).as_deref() == Some("ForeignError") - } - _ => false, - } -} - /// Whether a method receiver is one WeaveFFI can lift: `&self` (a borrow for /// the call) or `self: Arc` (a retained reference). /// @@ -624,7 +812,7 @@ fn returns_self(output: &syn::ReturnType, iface: &str) -> bool { impl Extractor<'_> { fn ty(&self, ty: &syn::Type) -> syn::Result { - type_ref_from_syn(ty, self.aliases) + type_ref_from_syn(ty, self.names) } /// Map a function's return type to its IDL return [`TypeRef`], peeling @@ -703,7 +891,7 @@ impl Extractor<'_> { params: self.params(path, &sig.inputs)?, returns, doc: extract_doc(attrs), - throws: output_is_result(&sig.output), + throws: result_error(&sig.output).map(|e| throws_of(e, &self.names.domains)), r#async: sig.asyncness.is_some(), cancellable: has_marker(attrs, "cancellable"), deprecated: parse_deprecated(attrs).1, @@ -750,8 +938,9 @@ impl Extractor<'_> { /// `async fn` returning the interface (constructors are synchronous, /// so that's an async factory). /// - /// Non-`pub` items are private helpers and stay unexported. The block's - /// `#[cfg]` is recorded on each member it declares. + /// Non-`pub` items are private helpers and stay unexported, and so does + /// a `pub fn` marked `#[weaveffi::skip]`. The block's `#[cfg]` is + /// recorded on each member it declares. fn members( &mut self, path: &[String], @@ -762,7 +951,7 @@ impl Extractor<'_> { let syn::ImplItem::Fn(f) = impl_item else { continue; }; - if !matches!(f.vis, syn::Visibility::Public(_)) { + if !matches!(f.vis, syn::Visibility::Public(_)) || has_marker(&f.attrs, "skip") { continue; } no_member_cfg( @@ -802,8 +991,9 @@ impl Extractor<'_> { /// [`CallbackInterfaceDef`]. /// /// Every trait method is a callback method the consumer implements. A - /// method takes `&self` and returns `Result`, - /// which maps to a `T` return; `#[weaveffi::throws]` marks it throwing. + /// method takes `&self` and returns `Result`, which maps to a `T` + /// return that throws `E` when `E` is a domain of the tree, and `any` + /// otherwise (`weaveffi::ForeignError`, say). fn callback_interface( &mut self, path: &[String], @@ -835,24 +1025,24 @@ impl Extractor<'_> { )); } } - if !returns_foreign_result(&f.sig.output) { + if result_error(&f.sig.output).is_none() { let span = match &f.sig.output { syn::ReturnType::Type(_, ty) => ty.span(), syn::ReturnType::Default => f.sig.ident.span(), }; return Err(syn::Error::new( span, - "weaveffi: a callback interface method must return \ - `Result` (or `Result<(), weaveffi::ForeignError>` \ - for no value), because the consumer's implementation can fail and the \ - `Err` is how its failure reaches you", + "weaveffi: a callback interface method must return `Result` (or \ + `Result<(), E>` for no value) with `E: From`, \ + because the consumer's implementation can fail and the `Err` is how its \ + failure reaches you; use `weaveffi::ForeignError` itself, or a \ + #[weaveffi::error] domain to receive the consumer's typed errors", )); } let returns = self.return_type(&f.sig.output)?; let method_path = at(path, &[&f.sig.ident.to_string()]); self.map.record(method_path.clone(), f.sig.ident.span()); - let mut method = self.function(&method_path, &f.sig, &f.attrs, returns)?; - method.throws = has_marker(&f.attrs, "throws"); + let method = self.function(&method_path, &f.sig, &f.attrs, returns)?; methods.push(method); } Ok(CallbackInterfaceDef { @@ -922,7 +1112,7 @@ impl Extractor<'_> { if !is_rich && !has_repr_i32(&item.attrs) { return Err(syn::Error::new( - item.span(), + item.ident.span(), format!( "enum `{}` must have #[repr(i32)] to be a #[weaveffi::enumeration]", item.ident @@ -983,8 +1173,8 @@ impl Extractor<'_> { }) } - /// Extract a `#[weaveffi::error]` enum into the module's - /// [`ErrorDomain`]. + /// Extract a `#[weaveffi::error]` enum into one of the module's + /// [`ErrorDomain`]s. /// /// Every variant needs an explicit integer discriminant: the code's /// stable ABI value. A variant may be a unit variant or carry named @@ -993,8 +1183,9 @@ impl Extractor<'_> { /// Rust requires a primitive representation such as `#[repr(i32)]` on an /// enum that mixes explicit discriminants with data-carrying variants. A /// variant's doc comment becomes the code's default message (falling - /// back to the variant name). The enum's name is the domain name, and - /// the macro generates matching `ErrorReport` and `ErrorDomain` + /// back to its message template, then the variant name; see + /// [`default_message`]). The enum's name is the domain name, and the + /// macro generates its `ErrorDomain`, `Display`, and `Error` /// implementations. fn error_domain(&mut self, path: &[String], item: &syn::ItemEnum) -> syn::Result { let mut codes = Vec::new(); @@ -1028,11 +1219,7 @@ impl Extractor<'_> { )); }; let doc = extract_doc(&v.attrs); - let message = doc - .as_deref() - .and_then(|d| d.lines().next()) - .unwrap_or(&code) - .to_string(); + let message = default_message(&v.attrs, &code)?; codes.push(ErrorCode { code: parse_discriminant(expr)?, name: code, @@ -1061,12 +1248,12 @@ impl Extractor<'_> { callback_interfaces: Vec::new(), structs: Vec::new(), enums: Vec::new(), - errors: None, + errors: Vec::new(), modules: Vec::new(), }; let Some((_, items)) = &item_mod.content else { return Err(syn::Error::new( - item_mod.span(), + item_mod.ident.span(), "weaveffi: a #[weaveffi::module] must have an inline body (`mod name { ... }`); \ the macro can't read a module from another file", )); @@ -1111,16 +1298,10 @@ impl Extractor<'_> { module.structs.push(record); } syn::Item::Enum(e) if has_marker(&e.attrs, "error") => { - if module.errors.is_some() { - return Err(syn::Error::new( - e.span(), - "weaveffi: a module may declare at most one #[weaveffi::error] \ - domain", - )); - } let decl = e.ident.to_string(); declare(&mut self.map, &decl, e.ident.span(), &e.attrs); - module.errors = Some(self.error_domain(&at(&path, &[&decl]), e)?); + let domain = self.error_domain(&at(&path, &[&decl]), e)?; + module.errors.push(domain); } syn::Item::Enum(e) if has_marker(&e.attrs, "enumeration") => { let decl = e.ident.to_string(); @@ -1170,36 +1351,50 @@ impl Extractor<'_> { } } +/// Everything [`extract_module`] reads from one module tree. +pub struct Extraction { + /// The tree's IR, rooted at the annotated module. + pub module: Module, + /// Where each declaration came from, and its `#[cfg]`. + pub source: SourceMap, + /// The tree's custom types. + pub customs: Customs, +} + /// Extract the module tree rooted at a `#[weaveffi::module]`-annotated -/// `mod`, together with its [`SourceMap`]. +/// `mod`, together with its [`SourceMap`] and custom types. /// /// Only items carrying a WeaveFFI marker are exported; everything else /// (private helpers, `use` items, free functions without /// `#[weaveffi::export]`) is ignored, so a module can freely mix exported -/// surface and implementation. Type aliases declared anywhere in the tree are -/// substituted. +/// surface and implementation. Type aliases and custom types declared +/// anywhere in the tree are substituted. /// /// # Errors /// /// Returns a spanned error when an annotated item cannot be mapped to the IR /// (an unsupported type, an enum without `#[repr(i32)]`, a callback interface -/// method that doesn't return `Result`, a -/// `#[cfg]` on a member, an out-of-line submodule, and so on). -pub fn extract_module(item_mod: &syn::ItemMod) -> syn::Result<(Module, SourceMap)> { - let mut aliases = Aliases::new(); - collect_aliases(item_mod, &mut aliases); +/// method that doesn't return a `Result`, a `#[cfg]` on a member, a +/// malformed `#[weaveffi::custom]`, an out-of-line submodule, and so on). +pub fn extract_module(item_mod: &syn::ItemMod) -> syn::Result { + let mut names = TreeNames::default(); + collect_names(item_mod, &[], &mut names)?; let mut extractor = Extractor { - aliases: &aliases, + names: &names, map: SourceMap::default(), }; let module = extractor.module(&[], item_mod)?; - Ok((module, extractor.map)) + Ok(Extraction { + module, + source: extractor.map, + customs: names.customs, + }) } #[cfg(test)] mod tests { use super::*; - use crate::ir::{Api, CURRENT_SCHEMA_VERSION}; + use weaveffi_model::ir::{Api, CURRENT_SCHEMA_VERSION}; /// Extract every top-level `#[weaveffi::module]` in `src`. fn api_from_src(src: &str) -> syn::Result { @@ -1208,7 +1403,7 @@ mod tests { for item in &file.items { if let syn::Item::Mod(item_mod) = item { if has_marker(&item_mod.attrs, "module") { - modules.push(extract_module(item_mod)?.0); + modules.push(extract_module(item_mod)?.module); } } } @@ -1456,7 +1651,7 @@ mod tests { ); let s = &m.interfaces[0]; assert_eq!(s.constructors.len(), 2); - assert!(s.constructors[0].throws); + assert_eq!(s.constructors[0].throws, Some(Throws::Any)); assert_eq!(s.constructors[0].returns, None); assert_eq!(s.methods.len(), 2); assert_eq!(s.methods[1].returns, Some(TypeRef::Named("Store".into()))); @@ -1528,7 +1723,7 @@ mod tests { f.name ); } - assert!(s.statics[0].throws); + assert!(s.statics[0].throws.is_some()); } #[test] @@ -1560,9 +1755,10 @@ mod tests { pub trait Listener: Send + Sync { fn on_message(&self, text: String, meta: Option) -> Result<(), ForeignError>; fn level(&self) -> Result; - #[weaveffi::throws] - fn label(&self) -> Result; + fn label(&self) -> Result; } + #[weaveffi::error] + pub enum ListenError { Deaf = 1 } #[weaveffi::export] pub fn subscribe(listener: Arc) {} #[weaveffi::export] @@ -1577,11 +1773,14 @@ mod tests { let methods = &cb.methods; assert_eq!(methods[0].params.len(), 2); assert_eq!(methods[0].returns, None); - assert!(!methods[0].throws); + assert_eq!(methods[0].throws, Some(Throws::Any)); assert_eq!(methods[1].returns, Some(TypeRef::Prim(Prim::I32))); - assert!(!methods[1].throws); + assert_eq!(methods[1].throws, Some(Throws::Any)); assert_eq!(methods[2].returns, Some(TypeRef::Prim(Prim::String))); - assert!(methods[2].throws); + assert_eq!( + methods[2].throws, + Some(Throws::Domain("ListenError".into())) + ); assert_eq!( m.functions[0].params[0].ty, TypeRef::Named("Listener".into()) @@ -1593,8 +1792,8 @@ mod tests { } #[test] - fn callback_methods_must_return_foreign_results() { - for ret in ["", "-> i32", "-> Result"] { + fn callback_methods_must_return_results() { + for ret in ["", "-> i32", "-> Option"] { let src = format!( "#[weaveffi::module] mod m {{ #[weaveffi::callback_interface] \ pub trait L {{ fn f(&self) {ret}; }} }}" @@ -1604,6 +1803,119 @@ mod tests { } } + #[test] + fn error_types_decide_what_a_function_throws() { + let m = one_module( + r#" + #[weaveffi::module] + mod m { + #[weaveffi::error] + pub enum AError { A = 1 } + #[weaveffi::error] + pub enum BError { + /// Bee. + #[weaveffi(message = "bee {n}")] + B { n: i32 } = 2, + #[weaveffi(message = "see")] + C = 3, + } + #[weaveffi::export] + pub fn a() -> Result<(), AError> { Ok(()) } + #[weaveffi::export] + pub fn b() -> Result { Ok(0) } + #[weaveffi::export] + pub fn io() -> Result<(), std::io::Error> { Ok(()) } + #[weaveffi::export] + pub fn s() -> Result<(), String> { Ok(()) } + #[weaveffi::export] + pub fn plain() {} + } + "#, + ); + let throws: Vec<_> = m.functions.iter().map(|f| f.throws.clone()).collect(); + assert_eq!( + throws, + [ + Some(Throws::Domain("AError".into())), + Some(Throws::Domain("BError".into())), + Some(Throws::Any), + Some(Throws::Any), + None, + ] + ); + assert_eq!(m.errors.len(), 2); + let b = &m.errors[1].codes; + assert_eq!( + (b[0].message.as_str(), b[1].message.as_str()), + ("Bee.", "see") + ); + assert_eq!(m.errors[0].codes[0].message, "A"); + } + + #[test] + fn sizes_chars_and_customs_map_to_their_idl_types() { + let m = one_module( + r#" + #[weaveffi::module] + mod m { + #[weaveffi::custom(repr = String, lift = parse_id, lower = Id::to_string)] + pub type Id = uuid::Uuid; + #[weaveffi::export] + pub fn f(n: usize, d: isize, c: char, ids: Vec) -> Option { None } + #[weaveffi::interface] + pub struct S; + impl S { + pub fn new() -> Self { S } + #[weaveffi::skip] + pub fn internal(&self) {} + } + } + "#, + ); + let f = &m.functions[0]; + let tys: Vec<_> = f.params.iter().map(|p| p.ty.clone()).collect(); + assert_eq!( + tys, + [ + TypeRef::Prim(Prim::U64), + TypeRef::Prim(Prim::I64), + TypeRef::Prim(Prim::String), + TypeRef::List(Box::new(TypeRef::Prim(Prim::String))), + ] + ); + assert_eq!( + f.returns, + Some(TypeRef::Optional(Box::new(TypeRef::Prim(Prim::String)))) + ); + assert!(m.interfaces[0].methods.is_empty()); + } + + #[test] + fn custom_types_need_every_argument() { + let err = api_from_src( + "#[weaveffi::module] mod m { #[weaveffi::custom(repr = String)] pub type Id = u8; }", + ) + .unwrap_err(); + assert!(err.to_string().contains("needs `lift`"), "{err}"); + } + + #[test] + fn only_namespaced_attributes_are_markers() { + let m = one_module( + r#" + #[weaveffi::module] + mod m { + #[export] + pub fn bare() {} + #[derive(Debug)] + pub enum NotADomain { #[error("x")] X } + } + "#, + ); + assert!(m.functions.is_empty()); + assert!(m.errors.is_empty()); + } + #[test] fn aliases_are_substituted() { let m = one_module( @@ -1685,7 +1997,7 @@ mod tests { let syn::Item::Mod(item) = &file.items[0] else { panic!("a module") }; - let (_, map) = extract_module(item).unwrap(); + let map = extract_module(item).unwrap().source; let path = |p: &[&str]| p.iter().map(ToString::to_string).collect::>(); assert_eq!(map.spans(&path(&["m", "f"])).len(), 1); assert_eq!(map.spans(&path(&["m", "f", "x", SourceMap::TYPE])).len(), 1); diff --git a/crates/weaveffi-macros/src/lib.rs b/crates/weaveffi-macros/src/lib.rs index 8a1ee6ba..2cf0e1f0 100644 --- a/crates/weaveffi-macros/src/lib.rs +++ b/crates/weaveffi-macros/src/lib.rs @@ -2,12 +2,11 @@ //! //! A producer annotates an ordinary Rust module with `#[weaveffi::module]`, //! tags the items it wants to export, and calls `weaveffi::export_runtime!()` -//! once. The module macro lowers the module tree to the WeaveFFI IR (through -//! [`weaveffi_model::rust`]), builds the canonical -//! [`Model`](weaveffi_model::model::Model), and emits the -//! `extern "C"` thunks every generated language binding calls. All of the -//! `unsafe` marshalling lives in the `weaveffi::abi` runtime, so the producer -//! writes only safe Rust. +//! once. The module macro lowers the module tree to the WeaveFFI IR, builds +//! the canonical [`Model`](weaveffi_model::model::Model) with the shared +//! validator, and emits the `extern "C"` thunks every generated language +//! binding calls. All of the `unsafe` marshalling lives in the +//! `weaveffi::abi` runtime, so the producer writes only safe Rust. //! //! ```ignore //! #[weaveffi::module] @@ -34,25 +33,28 @@ //! [`macro@enumeration`] a `#[repr(i32)]` C-style enum or a rich enum with //! data-carrying variants. //! * [`macro@interface`] declares an opaque, reference-counted object type -//! whose `impl` block's `pub fn`s become constructors, methods, and statics. -//! * [`macro@error`] declares the module's error domain from an enum with -//! explicit discriminants; a variant's named fields become the code's -//! structured payload. +//! whose `impl` blocks' `pub fn`s become constructors, methods, and +//! statics; [`macro@skip`] leaves one of them unexported. +//! * [`macro@error`] declares an error domain from an enum with explicit +//! discriminants; a variant's named fields become the code's structured +//! payload, and the macro generates `Display` and `std::error::Error`. //! * [`macro@callback_interface`] declares a trait the consumer implements, -//! whose methods return `Result`; -//! [`macro@throws`] lets one of them report the module's domain errors. -//! [`macro@cancellable`] marks an async function as cancellable. +//! whose methods return `Result` with `E: From`. +//! * [`macro@custom`] declares a type alias that crosses as another type. +//! * [`macro@cancellable`] marks an async function as cancellable. //! * [`export_runtime!`] emits the runtime symbols (memory, errors, cancel //! tokens, ABI version) once per library. //! -//! The item-level attributes are inert markers that [`macro@module`] reads; on -//! their own they expand to the item unchanged. +//! The item-level attributes are markers that [`macro@module`] reads and +//! removes; one that's left to expand on its own (because it isn't inside a +//! `#[weaveffi::module]`) is a compile error. #![deny(missing_docs)] use proc_macro::TokenStream; mod codegen; +mod extract; mod runtime; /// Mark an inline `mod` as an exported WeaveFFI namespace. @@ -87,63 +89,94 @@ pub fn export_runtime(input: TokenStream) -> TokenStream { .into() } -/// Generate `#[doc(hidden)]` no-op marker attributes that [`macro@module`] -/// reads. Each expands to the annotated item unchanged. +/// Generate the marker attributes that [`macro@module`] reads and strips. +/// Expanding on their own means they aren't inside a `#[weaveffi::module]`, +/// so each reports that and passes the item through unchanged (so the item's +/// other errors still surface). macro_rules! marker_attr { - ($(#[$meta:meta])* $name:ident) => { + ($(#[$meta:meta])* $name:ident, $placement:literal) => { $(#[$meta])* #[proc_macro_attribute] pub fn $name(_attr: TokenStream, item: TokenStream) -> TokenStream { - item + misplaced(stringify!($name), $placement, item) } }; } +/// A `compile_error!` for a marker that expanded on its own, pointing at the +/// attribute, followed by the item. +fn misplaced(name: &str, placement: &str, item: TokenStream) -> TokenStream { + let item = proc_macro2::TokenStream::from(item); + let span = proc_macro2::Span::call_site(); + let message = format!( + "`#[weaveffi::{name}]` only works {placement}; put the item in an inline module \ + annotated `#[weaveffi::module]` (`#[weaveffi::module] pub mod api {{ ... }}`)" + ); + let error = quote::quote_spanned!(span=> ::core::compile_error!(#message);); + quote::quote!(#error #item).into() +} + marker_attr! { /// Export a function across the FFI boundary. An `async fn` lowers to an - /// asynchronous symbol; a `fn -> Result` is fallible. - export + /// asynchronous symbol. A `fn -> Result` is fallible: it throws + /// `E` when `E` is a `#[weaveffi::error]` enum of the module tree, and + /// otherwise reports `E`'s `Display` output as an untyped error + /// (`throws any`). + export, "on an item inside a `#[weaveffi::module]`" } marker_attr! { /// Declare a by-value record (struct) serialized in the value-buffer /// format when it crosses the ABI. - record + record, "on an item inside a `#[weaveffi::module]`" } marker_attr! { /// Declare an interface: an opaque, reference-counted object type with - /// constructors, methods, and statics read from its `impl` block. Methods - /// take `&self` or `self: Arc`; the type must be `Send + Sync`. - interface + /// constructors, methods, and statics read from its `impl` blocks. + /// Methods take `&self` or `self: Arc`; the type must be + /// `Send + Sync`. + interface, "on an item inside a `#[weaveffi::module]`" } marker_attr! { - /// Declare the module's error domain from an enum with explicit - /// discriminants (the stable error codes). The enum must implement - /// `std::fmt::Display`, which supplies the runtime message; each - /// variant's doc comment is the documented default message. - error + /// Declare an error domain from an enum with explicit discriminants (the + /// stable error codes). The macro generates `Display` (each variant's + /// `#[weaveffi(message = "...")]` template, which may name the + /// variant's fields in braces, else the first line of its doc comment) + /// and `std::error::Error` (so the enum must derive `Debug`), unless + /// written `#[weaveffi::error(no_display)]`. A module may declare + /// several. + error, "on an item inside a `#[weaveffi::module]`" } marker_attr! { /// Declare an enum exported by value: a `#[repr(i32)]` C-style enum, or a /// rich enum whose variants carry named fields. - enumeration + enumeration, "on an item inside a `#[weaveffi::module]`" } marker_attr! { /// Declare a callback interface: a trait whose `&self` methods the /// consumer implements. Producers accept one as `Arc` (or - /// `Option>`). Every method returns - /// `Result`, which carries the consumer's - /// failure as a value. - callback_interface -} -marker_attr! { - /// Mark a callback-interface method as able to report the error domain - /// in scope: the consumer may fail with one of its codes, which the - /// producer decodes with `weaveffi::ForeignError::domain`. - throws + /// `Option>`). Every method returns `Result` with + /// `E: From`; when `E` is a `#[weaveffi::error]` + /// domain of the module tree, the method throws that domain and the + /// consumer's typed errors arrive as its variants. + callback_interface, "on an item inside a `#[weaveffi::module]`" } marker_attr! { /// Mark an `async fn` as cancellable: it takes a `weaveffi::CancelToken` /// as its final parameter, and cancelling the token completes the call /// with the cancelled code. - cancellable + cancellable, "on an exported `async fn` inside a `#[weaveffi::module]`" +} +marker_attr! { + /// Declare a custom type on a type alias, + /// `#[weaveffi::custom(repr = R, lift = f, lower = g)] pub type Name = T;`: + /// `Name` crosses the ABI as `R` (bindings see `R`), converted with + /// `f: fn(R) -> Result` on the way in (a failure is a + /// marshalling error carrying the message) and `g: fn(&T) -> R` on the + /// way out. + custom, "on a type alias inside a `#[weaveffi::module]`" +} +marker_attr! { + /// Leave a `pub fn` of an interface's `impl` block out of the exported + /// interface. + skip, "on a `pub fn` in the `impl` block of a `#[weaveffi::interface]` inside a `#[weaveffi::module]`" } diff --git a/crates/weaveffi-macros/src/runtime.rs b/crates/weaveffi-macros/src/runtime.rs index 2653565b..e489d363 100644 --- a/crates/weaveffi-macros/src/runtime.rs +++ b/crates/weaveffi-macros/src/runtime.rs @@ -5,6 +5,11 @@ //! emitted here, in the producer's crate, because `#[no_mangle]` symbols in //! a transitive `rlib` aren't guaranteed to be exported from a `cdylib`. //! +//! The expansion also defines the hidden module `crate::__weaveffi_runtime`, +//! which every `#[weaveffi::module]` references, so a crate that forgets to +//! call `export_runtime!()` (or calls it anywhere but the crate root) fails +//! to compile instead of producing a library with no runtime symbols. +//! //! `{prefix}_debug_live` is always exported. The macro can't see which cargo //! features the producer enabled on its `weaveffi` dependency, and a //! consumer shouldn't need to know either: without the `leak-check` feature @@ -38,6 +43,11 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { let debug_live = sym("debug_live"); let alloc = sym("alloc"); Ok(quote! { + /// Proof that this crate exports the WeaveFFI runtime, which every + /// `#[weaveffi::module]` checks for. + #[doc(hidden)] + pub mod __weaveffi_runtime {} + // Consumers compare this against the revision they were generated // for before touching any other symbol, so it must stay a plain // constant with no side effects. @@ -52,43 +62,51 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] pub unsafe extern "C" fn #error_set( - err: *mut ::weaveffi::abi::FfiError, - code: i32, - message: *const ::std::os::raw::c_char, + __wv_err: *mut ::weaveffi::abi::FfiError, + __wv_code: i32, + __wv_message_ptr: *const u8, + __wv_message_len: usize, ) { - unsafe { ::weaveffi::abi::error_set_c(err, code, message) } + unsafe { + ::weaveffi::abi::error_set_c( + __wv_err, + __wv_code, + __wv_message_ptr, + __wv_message_len, + ) + } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] pub unsafe extern "C" fn #error_set_payload( - err: *mut ::weaveffi::abi::FfiError, - ptr: *const u8, - len: usize, + __wv_err: *mut ::weaveffi::abi::FfiError, + __wv_ptr: *const u8, + __wv_len: usize, ) { - unsafe { ::weaveffi::abi::error_set_payload_c(err, ptr, len) } + unsafe { ::weaveffi::abi::error_set_payload_c(__wv_err, __wv_ptr, __wv_len) } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #error_clear(err: *mut ::weaveffi::abi::FfiError) { - unsafe { ::weaveffi::abi::error_clear(err) } + pub unsafe extern "C" fn #error_clear(__wv_err: *mut ::weaveffi::abi::FfiError) { + unsafe { ::weaveffi::abi::error_clear(__wv_err) } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #error_free(err: *mut ::weaveffi::abi::FfiError) { - unsafe { ::weaveffi::abi::error_free(err) } + pub unsafe extern "C" fn #error_free(__wv_err: *mut ::weaveffi::abi::FfiError) { + unsafe { ::weaveffi::abi::error_free(__wv_err) } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #free_bytes(ptr: *mut u8, len: usize) { - unsafe { ::weaveffi::abi::free_bytes(ptr, len) } + pub unsafe extern "C" fn #free_bytes(__wv_ptr: *mut u8, __wv_len: usize) { + unsafe { ::weaveffi::abi::free_bytes(__wv_ptr, __wv_len) } } #[doc(hidden)] @@ -101,29 +119,31 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #token_cancel(token: *mut ::weaveffi::abi::FfiCancelToken) { - unsafe { ::weaveffi::abi::cancel_token_cancel(token) } + pub unsafe extern "C" fn #token_cancel(__wv_token: *mut ::weaveffi::abi::FfiCancelToken) { + unsafe { ::weaveffi::abi::cancel_token_cancel(__wv_token) } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #token_is_cancelled(token: *const ::weaveffi::abi::FfiCancelToken) -> bool { - unsafe { ::weaveffi::abi::cancel_token_is_cancelled(token) } + pub unsafe extern "C" fn #token_is_cancelled( + __wv_token: *const ::weaveffi::abi::FfiCancelToken, + ) -> bool { + unsafe { ::weaveffi::abi::cancel_token_is_cancelled(__wv_token) } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] - pub unsafe extern "C" fn #token_destroy(token: *mut ::weaveffi::abi::FfiCancelToken) { - unsafe { ::weaveffi::abi::cancel_token_destroy(token) } + pub unsafe extern "C" fn #token_destroy(__wv_token: *mut ::weaveffi::abi::FfiCancelToken) { + unsafe { ::weaveffi::abi::cancel_token_destroy(__wv_token) } } #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code)] - pub extern "C" fn #debug_live(kind: i32) -> u64 { - ::weaveffi::abi::debug_live(kind) + pub extern "C" fn #debug_live(__wv_kind: i32) -> u64 { + ::weaveffi::abi::debug_live(__wv_kind) } // Consumers allocate the runs they hand to the producer (a @@ -132,8 +152,8 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { #[doc(hidden)] #[unsafe(no_mangle)] #[allow(unsafe_code)] - pub extern "C" fn #alloc(len: usize) -> *mut u8 { - ::weaveffi::abi::alloc(len) + pub extern "C" fn #alloc(__wv_len: usize) -> *mut u8 { + ::weaveffi::abi::alloc(__wv_len) } }) } diff --git a/crates/weaveffi-macros/tests/ui/fail_box_param.rs b/crates/weaveffi-macros/tests/ui/fail_box_param.rs index 2c72a59b..cb708116 100644 --- a/crates/weaveffi-macros/tests/ui/fail_box_param.rs +++ b/crates/weaveffi-macros/tests/ui/fail_box_param.rs @@ -9,4 +9,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_callback_error_without_from.rs b/crates/weaveffi-macros/tests/ui/fail_callback_error_without_from.rs new file mode 100644 index 00000000..10920acc --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_callback_error_without_from.rs @@ -0,0 +1,21 @@ +//! A callback method's error type must convert from `ForeignError`, which +//! carries every consumer failure that isn't one of its own codes. + +#[weaveffi::module] +mod bad { + #[weaveffi::error] + #[derive(Debug)] + pub enum LookupError { + /// missing + Missing = 1, + } + + #[weaveffi::callback_interface] + pub trait Source: Send + Sync { + fn lookup(&self, key: &str) -> Result; + } +} + +weaveffi::export_runtime!(); + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_callback_error_without_from.stderr b/crates/weaveffi-macros/tests/ui/fail_callback_error_without_from.stderr new file mode 100644 index 00000000..5d711015 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_callback_error_without_from.stderr @@ -0,0 +1,16 @@ +error[E0277]: the trait bound `LookupError: From` is not satisfied + --> tests/ui/fail_callback_error_without_from.rs:15:52 + | +15 | fn lookup(&self, key: &str) -> Result; + | ^^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `From` is not implemented for `LookupError` + --> tests/ui/fail_callback_error_without_from.rs:8:5 + | + 8 | pub enum LookupError { + | ^^^^^^^^^^^^^^^^^^^^ +note: required by a bound in `convert_foreign` + --> $WORKSPACE/crates/weaveffi/src/abi/callback.rs + | + | pub fn convert_foreign>(e: ForeignError) -> E { + | ^^^^^^^^^^^^^^^^^^ required by this bound in `convert_foreign` diff --git a/crates/weaveffi-macros/tests/ui/fail_callback_mut_self.rs b/crates/weaveffi-macros/tests/ui/fail_callback_mut_self.rs index 3c45b46f..99b02a90 100644 --- a/crates/weaveffi-macros/tests/ui/fail_callback_mut_self.rs +++ b/crates/weaveffi-macros/tests/ui/fail_callback_mut_self.rs @@ -6,4 +6,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.rs b/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.rs index 57d41afb..fd003c9d 100644 --- a/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.rs +++ b/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.rs @@ -10,4 +10,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.stderr b/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.stderr index 335a0ff9..5bab1f7d 100644 --- a/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_callback_plain_return.stderr @@ -1,4 +1,4 @@ -error: weaveffi: a callback interface method must return `Result` (or `Result<(), weaveffi::ForeignError>` for no value), because the consumer's implementation can fail and the `Err` is how its failure reaches you +error: weaveffi: a callback interface method must return `Result` (or `Result<(), E>` for no value) with `E: From`, because the consumer's implementation can fail and the `Err` is how its failure reaches you; use `weaveffi::ForeignError` itself, or a #[weaveffi::error] domain to receive the consumer's typed errors --> tests/ui/fail_callback_plain_return.rs:9:27 | 9 | fn name(&self) -> String; diff --git a/crates/weaveffi-macros/tests/ui/fail_custom_missing_lower.rs b/crates/weaveffi-macros/tests/ui/fail_custom_missing_lower.rs new file mode 100644 index 00000000..ba0c9abb --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_custom_missing_lower.rs @@ -0,0 +1,11 @@ +//! A custom type needs a repr and both conversions. + +#[weaveffi::module] +mod bad { + #[weaveffi::custom(repr = String, lift = str::parse)] + pub type Port = u16; +} + +weaveffi::export_runtime!(); + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_custom_missing_lower.stderr b/crates/weaveffi-macros/tests/ui/fail_custom_missing_lower.stderr new file mode 100644 index 00000000..804daf48 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_custom_missing_lower.stderr @@ -0,0 +1,5 @@ +error: weaveffi: #[weaveffi::custom] needs `lower`; write #[weaveffi::custom(repr = String, lift = parse_fn, lower = format_fn)] + --> tests/ui/fail_custom_missing_lower.rs:5:5 + | +5 | #[weaveffi::custom(repr = String, lift = str::parse)] + | ^ diff --git a/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.rs b/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.rs index 96ef65d7..f230df30 100644 --- a/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.rs +++ b/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.rs @@ -6,4 +6,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.stderr b/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.stderr index 99955f36..e9fd1215 100644 --- a/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_enum_without_repr.stderr @@ -1,5 +1,5 @@ error: enum `Mode` must have #[repr(i32)] to be a #[weaveffi::enumeration] - --> tests/ui/fail_enum_without_repr.rs:3:5 + --> tests/ui/fail_enum_without_repr.rs:4:14 | -3 | #[weaveffi::enumeration] - | ^ +4 | pub enum Mode { + | ^^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_error_without_display.stderr b/crates/weaveffi-macros/tests/ui/fail_error_without_display.stderr deleted file mode 100644 index e361672f..00000000 --- a/crates/weaveffi-macros/tests/ui/fail_error_without_display.stderr +++ /dev/null @@ -1,16 +0,0 @@ -error[E0277]: `Oops` doesn't implement `std::fmt::Display` - --> tests/ui/fail_error_without_display.rs:4:14 - | -4 | pub enum Oops { - | ^^^^ unsatisfied trait bound - | -help: the trait `std::fmt::Display` is not implemented for `Oops` - --> tests/ui/fail_error_without_display.rs:4:5 - | -4 | pub enum Oops { - | ^^^^^^^^^^^^^ -note: required by a bound in `__weaveffi_error_domains_must_implement_display` - --> tests/ui/fail_error_without_display.rs:4:14 - | -4 | pub enum Oops { - | ^^^^ required by this bound in `__weaveffi_error_domains_must_implement_display` diff --git a/crates/weaveffi-macros/tests/ui/fail_interface_not_sync.rs b/crates/weaveffi-macros/tests/ui/fail_interface_not_sync.rs index 44e74cae..b7b728d6 100644 --- a/crates/weaveffi-macros/tests/ui/fail_interface_not_sync.rs +++ b/crates/weaveffi-macros/tests/ui/fail_interface_not_sync.rs @@ -14,4 +14,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_interface_self_by_value.rs b/crates/weaveffi-macros/tests/ui/fail_interface_self_by_value.rs index adf88eda..236e82ad 100644 --- a/crates/weaveffi-macros/tests/ui/fail_interface_self_by_value.rs +++ b/crates/weaveffi-macros/tests/ui/fail_interface_self_by_value.rs @@ -8,4 +8,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_iter_param.rs b/crates/weaveffi-macros/tests/ui/fail_iter_param.rs index 325bc28b..94d95246 100644 --- a/crates/weaveffi-macros/tests/ui/fail_iter_param.rs +++ b/crates/weaveffi-macros/tests/ui/fail_iter_param.rs @@ -6,4 +6,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_marker_outside_module.rs b/crates/weaveffi-macros/tests/ui/fail_marker_outside_module.rs new file mode 100644 index 00000000..1cc9de4d --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_marker_outside_module.rs @@ -0,0 +1,10 @@ +//! A marker attribute only means something inside a `#[weaveffi::module]`. + +#[weaveffi::export] +pub fn orphan() -> i32 { + 1 +} + +weaveffi::export_runtime!(); + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_marker_outside_module.stderr b/crates/weaveffi-macros/tests/ui/fail_marker_outside_module.stderr new file mode 100644 index 00000000..4aca75cc --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_marker_outside_module.stderr @@ -0,0 +1,7 @@ +error: `#[weaveffi::export]` only works on an item inside a `#[weaveffi::module]`; put the item in an inline module annotated `#[weaveffi::module]` (`#[weaveffi::module] pub mod api { ... }`) + --> tests/ui/fail_marker_outside_module.rs:3:1 + | +3 | #[weaveffi::export] + | ^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `weaveffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/weaveffi-macros/tests/ui/fail_member_cfg.rs b/crates/weaveffi-macros/tests/ui/fail_member_cfg.rs index 1aa678dd..b6f672ed 100644 --- a/crates/weaveffi-macros/tests/ui/fail_member_cfg.rs +++ b/crates/weaveffi-macros/tests/ui/fail_member_cfg.rs @@ -11,4 +11,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_member_cfg.stderr b/crates/weaveffi-macros/tests/ui/fail_member_cfg.stderr index a4ebdb73..e583c077 100644 --- a/crates/weaveffi-macros/tests/ui/fail_member_cfg.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_member_cfg.stderr @@ -1,5 +1,5 @@ error: weaveffi: `#[cfg]` on a record field isn't supported, because the generated bindings can't follow it; put the `#[cfg]` on the whole type - --> tests/ui/fail_member_cfg.rs:9:9 + --> tests/ui/fail_member_cfg.rs:9:11 | 9 | #[cfg(unix)] - | ^ + | ^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_missing_export_runtime.rs b/crates/weaveffi-macros/tests/ui/fail_missing_export_runtime.rs new file mode 100644 index 00000000..4c78dd92 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_missing_export_runtime.rs @@ -0,0 +1,12 @@ +//! Every library needs the runtime symbols: a module tree in a crate that +//! never calls `weaveffi::export_runtime!()` fails to compile. + +#[weaveffi::module] +pub mod api { + #[weaveffi::export] + pub fn one() -> i32 { + 1 + } +} + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_missing_export_runtime.stderr b/crates/weaveffi-macros/tests/ui/fail_missing_export_runtime.stderr new file mode 100644 index 00000000..d22a89bf --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_missing_export_runtime.stderr @@ -0,0 +1,5 @@ +error[E0432]: unresolved import `crate` + --> tests/ui/fail_missing_export_runtime.rs:5:9 + | +5 | pub mod api { + | ^^^ no `__weaveffi_runtime` in the root diff --git a/crates/weaveffi-macros/tests/ui/fail_mut_ref_param.rs b/crates/weaveffi-macros/tests/ui/fail_mut_ref_param.rs index daad6db0..2d098b68 100644 --- a/crates/weaveffi-macros/tests/ui/fail_mut_ref_param.rs +++ b/crates/weaveffi-macros/tests/ui/fail_mut_ref_param.rs @@ -6,4 +6,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_nested_iter_return.rs b/crates/weaveffi-macros/tests/ui/fail_nested_iter_return.rs index ed320abe..18c81006 100644 --- a/crates/weaveffi-macros/tests/ui/fail_nested_iter_return.rs +++ b/crates/weaveffi-macros/tests/ui/fail_nested_iter_return.rs @@ -6,4 +6,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_error_without_display.rs b/crates/weaveffi-macros/tests/ui/fail_no_display_without_impl.rs similarity index 58% rename from crates/weaveffi-macros/tests/ui/fail_error_without_display.rs rename to crates/weaveffi-macros/tests/ui/fail_no_display_without_impl.rs index f541b34e..fed889b3 100644 --- a/crates/weaveffi-macros/tests/ui/fail_error_without_display.rs +++ b/crates/weaveffi-macros/tests/ui/fail_no_display_without_impl.rs @@ -1,6 +1,9 @@ +//! `#[weaveffi::error(no_display)]` leaves `Display` to the producer. + #[weaveffi::module] mod bad { - #[weaveffi::error] + #[weaveffi::error(no_display)] + #[derive(Debug)] pub enum Oops { /// Something broke. Broken = 1, @@ -12,4 +15,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_no_display_without_impl.stderr b/crates/weaveffi-macros/tests/ui/fail_no_display_without_impl.stderr new file mode 100644 index 00000000..62d70caf --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_no_display_without_impl.stderr @@ -0,0 +1,16 @@ +error[E0277]: `Oops` doesn't implement `std::fmt::Display` + --> tests/ui/fail_no_display_without_impl.rs:7:14 + | +7 | pub enum Oops { + | ^^^^ unsatisfied trait bound + | +help: the trait `std::fmt::Display` is not implemented for `Oops` + --> tests/ui/fail_no_display_without_impl.rs:7:5 + | +7 | pub enum Oops { + | ^^^^^^^^^^^^^ +note: required by a bound in `ErrorDomain` + --> $WORKSPACE/crates/weaveffi/src/abi/error.rs + | + | pub trait ErrorDomain: std::fmt::Display + Sized { + | ^^^^^^^^^^^^^^^^^ required by this bound in `ErrorDomain` diff --git a/crates/weaveffi-macros/tests/ui/fail_object_in_error_payload.rs b/crates/weaveffi-macros/tests/ui/fail_object_in_error_payload.rs new file mode 100644 index 00000000..a5df39d0 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_object_in_error_payload.rs @@ -0,0 +1,41 @@ +//! An error code's fields may not carry an interface object, directly or +//! through a record: a consumer that doesn't know the code discards its +//! payload undecoded, so the object would never be released. + +#[weaveffi::module] +mod bad { + use std::sync::Arc; + + #[weaveffi::interface] + pub struct Store; + + impl Store { + pub fn new() -> Store { + Store + } + } + + #[weaveffi::record] + pub struct Holder { + pub store: Arc, + } + + #[weaveffi::error] + #[repr(i32)] + #[derive(Debug)] + pub enum Oops { + /// The store is busy. + Busy { store: Arc } = 1, + /// The holder is busy. + Held { holder: Holder } = 2, + } + + #[weaveffi::export] + pub fn risky() -> Result { + Ok(1) + } +} + +weaveffi::export_runtime!(); + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_object_in_error_payload.stderr b/crates/weaveffi-macros/tests/ui/fail_object_in_error_payload.stderr new file mode 100644 index 00000000..cd300c03 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_object_in_error_payload.stderr @@ -0,0 +1,11 @@ +error: weaveffi: error payloads can't carry objects, but payload field 'store' of error code 'Oops::Busy' reaches 'Store' + --> tests/ui/fail_object_in_error_payload.rs:28:16 + | +28 | Busy { store: Arc } = 1, + | ^^^^^ + +error: weaveffi: error payloads can't carry objects, but payload field 'holder' of error code 'Oops::Held' reaches 'Holder' + --> tests/ui/fail_object_in_error_payload.rs:30:16 + | +30 | Held { holder: Holder } = 2, + | ^^^^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.rs b/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.rs index 8e017ac1..f5d1d9af 100644 --- a/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.rs +++ b/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.rs @@ -7,4 +7,6 @@ mod outer { mod inner; } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.stderr b/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.stderr index 0ede1844..8cac9f3d 100644 --- a/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_out_of_line_module.stderr @@ -1,5 +1,5 @@ error: weaveffi: a #[weaveffi::module] must have an inline body (`mod name { ... }`); the macro can't read a module from another file - --> tests/ui/fail_out_of_line_module.rs:6:5 + --> tests/ui/fail_out_of_line_module.rs:7:9 | -6 | #[weaveffi::module] - | ^ +7 | mod inner; + | ^^^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_out_of_tree_c_enum.rs b/crates/weaveffi-macros/tests/ui/fail_out_of_tree_c_enum.rs index a83b1586..e50ee205 100644 --- a/crates/weaveffi-macros/tests/ui/fail_out_of_tree_c_enum.rs +++ b/crates/weaveffi-macros/tests/ui/fail_out_of_tree_c_enum.rs @@ -22,4 +22,6 @@ pub mod paint { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_raw_pointer.rs b/crates/weaveffi-macros/tests/ui/fail_raw_pointer.rs index 866e4ebe..d9b64264 100644 --- a/crates/weaveffi-macros/tests/ui/fail_raw_pointer.rs +++ b/crates/weaveffi-macros/tests/ui/fail_raw_pointer.rs @@ -8,4 +8,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr b/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr deleted file mode 100644 index 767ae24e..00000000 --- a/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: weaveffi: function 'bad::risky' declares throws but no error domain is in scope; declare a #[weaveffi::error] enum in this module or a parent module - --> tests/ui/fail_result_without_error_domain.rs:8:12 - | -8 | pub fn risky() -> Result { - | ^^^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.rs b/crates/weaveffi-macros/tests/ui/fail_throws_any_without_display.rs similarity index 53% rename from crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.rs rename to crates/weaveffi-macros/tests/ui/fail_throws_any_without_display.rs index 1c434d73..ebe14c98 100644 --- a/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.rs +++ b/crates/weaveffi-macros/tests/ui/fail_throws_any_without_display.rs @@ -1,3 +1,6 @@ +//! An error type that isn't a domain of the tree reports its `Display` +//! output (`throws any`), so it must implement `Display`. + #[weaveffi::module] mod bad { pub enum Oops { @@ -10,4 +13,6 @@ mod bad { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_throws_any_without_display.stderr b/crates/weaveffi-macros/tests/ui/fail_throws_any_without_display.stderr new file mode 100644 index 00000000..f4b0496c --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_throws_any_without_display.stderr @@ -0,0 +1,16 @@ +error[E0277]: `Oops` doesn't implement `std::fmt::Display` + --> tests/ui/fail_throws_any_without_display.rs:11:35 + | +11 | pub fn risky() -> Result { + | ^^^^ unsatisfied trait bound + | +help: the trait `std::fmt::Display` is not implemented for `Oops` + --> tests/ui/fail_throws_any_without_display.rs:6:5 + | + 6 | pub enum Oops { + | ^^^^^^^^^^^^^ +note: required by a bound in `FfiError::untyped` + --> $WORKSPACE/crates/weaveffi/src/abi/error.rs + | + | pub fn untyped(e: &E) -> Self { + | ^^^^^^^^^^^^^^^^^ required by this bound in `FfiError::untyped` diff --git a/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.rs b/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.rs index 266075f8..f798c5ae 100644 --- a/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.rs +++ b/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.rs @@ -1,11 +1,13 @@ -//! `usize` varies with the platform, so it has no stable C ABI type. +//! `u128` has no IDL type (`usize` and `isize` cross as `u64` and `i64`). #[weaveffi::module] mod bad { #[weaveffi::export] - pub fn count(n: usize) -> i64 { + pub fn count(n: u128) -> i64 { n as i64 } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.stderr b/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.stderr index e682bedf..b51a020f 100644 --- a/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_unsupported_primitive.stderr @@ -1,5 +1,5 @@ -error: weaveffi: unsupported primitive type 'usize'; use `u64` or `i64` for sizes and counts (their width is the same on every platform), or `String` for a `char` +error: weaveffi: unsupported primitive type 'u128'; use `u64`, `i64`, `usize`, or `isize` (which cross as `u64` and `i64`), or a #[weaveffi::custom] type with a `String` repr for wider integers --> tests/ui/fail_unsupported_primitive.rs:6:21 | -6 | pub fn count(n: usize) -> i64 { - | ^^^^^ +6 | pub fn count(n: u128) -> i64 { + | ^^^^ diff --git a/crates/weaveffi-macros/tests/ui/pass_abi5_shapes.rs b/crates/weaveffi-macros/tests/ui/pass_abi5_shapes.rs new file mode 100644 index 00000000..da5c4952 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/pass_abi5_shapes.rs @@ -0,0 +1,227 @@ +//! The revision 5 shapes: optional scalars and numeric lists in every +//! position, `usize`/`isize`/`char`, custom types, several error domains in +//! one module (one opting out of the generated `Display`), `throws any` +//! functions, typed callback errors, and `#[weaveffi::skip]`. +#![deny(unsafe_code)] + +use std::sync::Arc; + +fn parse_celsius(text: String) -> Result { + text.parse().map(Celsius) +} + +fn format_celsius(c: &Celsius) -> String { + c.0.to_string() +} + +/// A temperature, a newtype declared outside the module. +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct Celsius(pub f64); + +#[weaveffi::module] +mod shapes { + use std::collections::BTreeMap; + use std::sync::Arc; + + use weaveffi::ForeignError; + + #[weaveffi::custom(repr = String, lift = super::parse_celsius, lower = super::format_celsius)] + pub type Temp = super::Celsius; + + #[weaveffi::error] + #[repr(i32)] + #[derive(Debug)] + pub enum SensorError { + /// sensor offline + Offline = 1, + #[weaveffi(message = "reading {value:?} out of range")] + Range { value: Temp } = 2, + #[weaveffi(message = "consumer failed: {message}")] + Consumer { message: String } = 3, + } + + impl From for SensorError { + fn from(e: ForeignError) -> Self { + Self::Consumer { message: e.message } + } + } + + /// A second domain in the same module, with a hand-written `Display`. + #[weaveffi::error(no_display)] + #[derive(Debug)] + pub enum CalibrationError { + /// drifted + Drifted = 10, + } + + impl std::fmt::Display for CalibrationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("drifted") + } + } + + impl std::error::Error for CalibrationError {} + + #[weaveffi::enumeration] + #[repr(i32)] + pub enum Unit { + C = 0, + F = 1, + } + + #[weaveffi::record] + pub struct Reading { + pub at: Temp, + pub history: Vec, + pub by_name: BTreeMap>, + pub count: usize, + pub grade: char, + } + + #[weaveffi::callback_interface] + pub trait Sensor: Send + Sync { + fn read(&self, unit: Option) -> Result, SensorError>; + fn burst(&self, sizes: &[u32], since: Option) -> Result, ForeignError>; + fn temp(&self, hint: Temp) -> Result; + fn grade(&self) -> Result; + fn reset(&self) -> Result<(), ForeignError>; + } + + /// An error type that converts from `ForeignError` but isn't a domain. + pub mod anyhow_like { + #[derive(Debug)] + pub struct Error(pub String); + impl From for Error { + fn from(e: weaveffi::ForeignError) -> Self { + Self(e.message) + } + } + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } + } + } + + #[weaveffi::interface] + pub struct Station { + size: usize, + } + + impl Station { + pub fn new(size: usize) -> Self { + Self { size } + } + pub fn size(&self) -> usize { + self.size + } + pub fn window(&self, xs: &[f64]) -> Option { + xs.iter().copied().reduce(f64::max) + } + /// Not exported. + #[weaveffi::skip] + pub fn internal(&self) -> Box { + Box::new(|| {}) + } + } + + // Constants named like the C slots and like error fields: every thunk + // parameter and binding is `__wv_*`, so none of them collide. + #[allow(non_upper_case_globals, dead_code)] + const out_err: i32 = 1; + #[allow(non_upper_case_globals, dead_code)] + const out_len: i32 = 2; + #[allow(non_upper_case_globals, dead_code)] + const callback: i32 = 3; + #[allow(non_upper_case_globals, dead_code)] + const context: i32 = 4; + #[allow(non_upper_case_globals, dead_code)] + const message: i32 = 5; + + #[weaveffi::export] + pub fn shadowing(base: i32, count: Option) -> String { + (base + count.unwrap_or(0)).to_string() + } + + #[weaveffi::export] + pub fn hottest(temps: Vec) -> Result { + temps + .into_iter() + .reduce(|a, b| if b > a { b } else { a }) + .ok_or(SensorError::Offline) + } + + #[weaveffi::export] + pub fn calibrate(by: Option) -> Result, CalibrationError> { + Ok(by) + } + + #[weaveffi::export] + pub fn parse(text: &str) -> Result { + text.parse() + } + + #[weaveffi::export] + pub fn describe(text: String) -> Result { + text.chars().next().ok_or_else(|| "empty".to_string()) + } + + #[weaveffi::export] + pub fn sizes(xs: &[u64], extra: Vec) -> Vec { + xs.iter().map(|x| *x as usize).chain(extra).collect() + } + + #[weaveffi::export] + pub async fn later(x: Option, ys: Vec) -> Option { + ys.into_iter().nth(usize::from(x.unwrap_or(0))) + } + + #[weaveffi::export] + pub async fn spread(n: u32) -> Vec { + (0..n).map(|i| i as f32).collect() + } + + #[weaveffi::export] + pub async fn temp_later(t: Temp) -> Temp { + t + } + + #[weaveffi::export] + pub fn ladders(n: u32) -> weaveffi::Iter> { + weaveffi::Iter::new((0..n).map(|i| (0..i).collect())) + } + + #[weaveffi::export] + pub fn maybes(n: u32) -> weaveffi::Iter> { + weaveffi::Iter::new((0..n).map(|i| (i % 2 == 0).then_some(Unit::C))) + } + + #[weaveffi::export] + pub fn temps(n: u32) -> weaveffi::Iter { + weaveffi::Iter::new((0..n).map(|i| super::Celsius(f64::from(i)))) + } + + #[weaveffi::export] + pub fn poll(sensor: Arc) -> Result { + let a = sensor.read(Some(Unit::F))?; + let b = sensor.burst(&[1, 2], None)?; + let t = sensor.temp(super::Celsius(1.0))?; + let g = sensor.grade().map_err(|e| SensorError::Consumer { message: e.0 })?; + sensor.reset()?; + Ok(format!("{a:?} {b:?} {t:?} {g}")) + } + + #[weaveffi::module] + pub mod nested { + #[weaveffi::export] + pub fn warmer(t: super::Temp, by: Option) -> super::Temp { + super::super::Celsius(t.0 + by.map_or(0.0, |b| b.0)) + } + } +} + +weaveffi::export_runtime!(); + +fn main() { + let _ = Arc::new(shapes::Station::new(1)).size(); +} diff --git a/crates/weaveffi-macros/tests/ui/pass_aliases_and_cfg.rs b/crates/weaveffi-macros/tests/ui/pass_aliases_and_cfg.rs index c671a7df..6e575c9e 100644 --- a/crates/weaveffi-macros/tests/ui/pass_aliases_and_cfg.rs +++ b/crates/weaveffi-macros/tests/ui/pass_aliases_and_cfg.rs @@ -48,6 +48,8 @@ mod tree { } } +weaveffi::export_runtime!(); + fn main() { let _ = tree::Store::new(); } diff --git a/crates/weaveffi-macros/tests/ui/pass_async_factory.rs b/crates/weaveffi-macros/tests/ui/pass_async_factory.rs index c0fa95ed..305439ed 100644 --- a/crates/weaveffi-macros/tests/ui/pass_async_factory.rs +++ b/crates/weaveffi-macros/tests/ui/pass_async_factory.rs @@ -8,17 +8,13 @@ mod store { #[weaveffi::error] #[repr(i32)] + #[derive(Debug)] pub enum StoreError { /// Empty path + #[weaveffi(message = "invalid path")] InvalidPath = 1, } - impl std::fmt::Display for StoreError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("invalid path") - } - } - #[weaveffi::interface] pub struct Store { path: String, @@ -53,6 +49,8 @@ mod store { } } +weaveffi::export_runtime!(); + fn main() { let _ = store::Store::new("x".into()).path(); } diff --git a/crates/weaveffi-macros/tests/ui/pass_full_surface.rs b/crates/weaveffi-macros/tests/ui/pass_full_surface.rs index 2977daaf..5382f2af 100644 --- a/crates/weaveffi-macros/tests/ui/pass_full_surface.rs +++ b/crates/weaveffi-macros/tests/ui/pass_full_surface.rs @@ -10,22 +10,15 @@ mod surface { #[weaveffi::error] #[repr(i32)] + #[derive(Debug)] pub enum Failure { /// Not found NotFound = 1, /// Payload variant + #[weaveffi(message = "{code}: {note}")] Detailed { code: i32, note: String } = 2, } - impl std::fmt::Display for Failure { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NotFound => f.write_str("not found"), - Self::Detailed { code, note } => write!(f, "{code}: {note}"), - } - } - } - #[weaveffi::enumeration] #[repr(i32)] pub enum Mode { @@ -148,6 +141,8 @@ mod surface { } } +weaveffi::export_runtime!(); + fn main() { let _ = Arc::new(surface::Widget::new(1)); } diff --git a/crates/weaveffi-macros/tests/ui/pass_rich_callbacks.rs b/crates/weaveffi-macros/tests/ui/pass_rich_callbacks.rs index cc43734c..af114235 100644 --- a/crates/weaveffi-macros/tests/ui/pass_rich_callbacks.rs +++ b/crates/weaveffi-macros/tests/ui/pass_rich_callbacks.rs @@ -1,5 +1,5 @@ -//! Callback methods return any family, may throw the domain in scope, and a -//! callback parameter may be optional. +//! Callback methods return any family, may throw a declared domain (whose +//! typed errors arrive typed), and a callback parameter may be optional. use std::sync::Arc; @@ -13,12 +13,15 @@ mod rich { #[repr(i32)] #[derive(Debug)] pub enum LookupError { + #[weaveffi(message = "missing {key}")] Missing { key: String } = 1, + #[weaveffi(message = "{message}")] + Other { message: String } = 2, } - impl std::fmt::Display for LookupError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("missing") + impl From for LookupError { + fn from(e: ForeignError) -> Self { + Self::Other { message: e.message } } } @@ -43,8 +46,7 @@ mod rich { fn card(&self) -> Result; fn token(&self) -> Result, ForeignError>; fn maybe(&self) -> Result>, ForeignError>; - #[weaveffi::throws] - fn lookup(&self, key: &str) -> Result; + fn lookup(&self, key: &str) -> Result; } #[weaveffi::export] @@ -57,12 +59,15 @@ mod rich { #[weaveffi::export] pub fn missing_key(source: Arc) -> Option { - match source.lookup("k").err()?.domain::()? { + match source.lookup("k").err()? { LookupError::Missing { key } => Some(key), + LookupError::Other { .. } => None, } } } +weaveffi::export_runtime!(); + fn main() { let _ = Arc::new(rich::Token::new()); } diff --git a/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs b/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs index 940eeda3..af41aff0 100644 --- a/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs +++ b/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs @@ -32,4 +32,6 @@ pub mod canvas { } } +weaveffi::export_runtime!(); + fn main() {} diff --git a/crates/weaveffi-model/Cargo.toml b/crates/weaveffi-model/Cargo.toml index 896be84d..88c45b29 100644 --- a/crates/weaveffi-model/Cargo.toml +++ b/crates/weaveffi-model/Cargo.toml @@ -6,31 +6,27 @@ rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true -description = "IR, parsers, Rust extractor, validation, and the validated model for WeaveFFI API definitions" +description = "IR, parsers, validation, and the validated model for WeaveFFI API definitions" keywords = ["ffi", "codegen", "bindings", "ir", "parser"] categories = ["development-tools::ffi"] [features] default = ["idl"] -# The IDL text formats (YAML, JSON, TOML), JSON Schema derivation, and fancy +# The IDL text formats (YAML and JSON), JSON Schema derivation, and fancy # miette diagnostics. The proc-macros build without it. -idl = ["dep:serde_yaml", "dep:toml", "dep:schemars", "dep:miette"] +idl = ["dep:serde-saphyr", "dep:schemars", "dep:miette"] [dependencies] serde = { workspace = true } thiserror = { workspace = true } heck = { workspace = true } -syn = { workspace = true } -proc-macro2 = "1.0" -serde_yaml = { workspace = true, optional = true } +serde-saphyr = { workspace = true, optional = true } serde_json = { workspace = true } -toml = { workspace = true, optional = true } schemars = { workspace = true, optional = true } miette = { workspace = true, optional = true } [dev-dependencies] -serde_yaml = { workspace = true } -toml = { workspace = true } +serde-saphyr = { workspace = true } [lints] workspace = true diff --git a/crates/weaveffi-model/src/abi/ctype.rs b/crates/weaveffi-model/src/abi/ctype.rs index f1273512..8f045ae2 100644 --- a/crates/weaveffi-model/src/abi/ctype.rs +++ b/crates/weaveffi-model/src/abi/ctype.rs @@ -7,8 +7,10 @@ //! structure is shared, every target agrees on the calling convention by //! construction. +use crate::ty::Prim; + /// Placement of a `const` qualifier on a pointer. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ConstPos { /// No `const` (mutable pointer): `T*`. None, @@ -18,7 +20,7 @@ pub enum ConstPos { /// A single C type in an ABI signature, independent of the configured symbol /// prefix (applied at render time by [`CType::render_c`]). -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CType { /// `int8_t` (the `i8` primitive). Int8, @@ -53,7 +55,9 @@ pub enum CType { CancelToken, /// `{prefix}_error`. Error, - /// An enum value type: `{prefix}_{module}_{name}`. + /// A C-style enum (or error-domain code) value type: + /// `{prefix}_{module}_{name}`, which the header declares as + /// `typedef int32_t`. Enum { /// Underscore-joined symbol path of the module that declares the enum. module: String, @@ -89,6 +93,26 @@ pub enum CType { } impl CType { + /// The by-value C type of a scalar primitive (`int32_t` for `i32`, + /// `double` for `f64`, `bool` for `bool`). For `string` and `bytes`, + /// which cross as byte runs, it's the run's element type, `uint8_t`. + #[must_use] + pub fn of_prim(p: Prim) -> CType { + match p { + Prim::Bool => CType::Bool, + Prim::I8 => CType::Int8, + Prim::I16 => CType::Int16, + Prim::I32 => CType::Int32, + Prim::I64 => CType::Int64, + Prim::U8 | Prim::String | Prim::Bytes => CType::Uint8, + Prim::U16 => CType::Uint16, + Prim::U32 => CType::Uint32, + Prim::U64 => CType::Uint64, + Prim::F32 => CType::Float, + Prim::F64 => CType::Double, + } + } + /// Pointer to `pointee` with no `const`. pub fn ptr(pointee: CType) -> CType { CType::Ptr { @@ -122,9 +146,8 @@ impl CType { /// imports `std::os::raw::c_char`), and [`Void`](Self::Void) as /// `std::ffi::c_void` (only meaningful as a pointee; a bare `void` /// *return* is the absence of a `-> T`, handled by the caller). - /// * A C-style [`Enum`](Self::Enum) crosses the ABI as its `int`-sized - /// discriminant, so it lowers to `i32` (matching the header's - /// `int`-backed `typedef enum`). + /// * A C-style [`Enum`](Self::Enum) crosses the ABI as its discriminant, + /// so it lowers to `i32` (matching the header's `typedef int32_t`). /// * A `const` pointer is `*const`; a non-`const` pointer is `*mut`. pub fn render_rust(&self, prefix: &str) -> String { match self { @@ -290,6 +313,14 @@ mod tests { assert_eq!(e.render_rust("weaveffi"), "i32"); } + #[test] + fn prims_map_to_their_scalar_types() { + assert_eq!(CType::of_prim(Prim::F64).render_c("p"), "double"); + assert_eq!(CType::of_prim(Prim::U16).render_rust("p"), "u16"); + assert_eq!(CType::of_prim(Prim::Bool), CType::Bool); + assert_eq!(CType::of_prim(Prim::String), CType::Uint8); + } + #[test] fn rust_void_is_only_a_pointee() { assert_eq!( diff --git a/crates/weaveffi-model/src/abi/lower.rs b/crates/weaveffi-model/src/abi/lower.rs index 3b024e18..66b65462 100644 --- a/crates/weaveffi-model/src/abi/lower.rs +++ b/crates/weaveffi-model/src/abi/lower.rs @@ -1,38 +1,35 @@ -//! The structural lowering: how each [`Ty`] maps onto C ABI parameter and -//! return slots. This is the single source of truth every generator shares. +//! The structural lowering: how each resolved type maps onto C ABI slots in +//! every position. This is the single source of truth the model stores on +//! every binding, so no generator re-derives it. //! -//! The lowering dispatches on [`Ty::family`]: +//! The lowering classifies a value [`Ty`] into its +//! [`Family`](crate::ty::Family) and, per +//! position, produces the passing contract from [`crate::plan`] that names +//! its slots: //! -//! * [`Family::Direct`] types occupy one C slot by value: scalars, bools, and -//! C-style enums. -//! * [`Family::String`] and [`Family::Bytes`] are a `const uint8_t*` + -//! `size_t` pair (UTF-8 for strings, never NUL-terminated). -//! * [`Family::Buffer`] types (records, rich enums, optionals, lists, and -//! maps) cross as one serialized value buffer: a `const uint8_t*` + -//! `size_t` pair encoded in the WeaveFFI buffer format. A buffered -//! parameter is borrowed for the call; a buffered return is -//! producer-allocated and released with `{prefix}_free_bytes` after -//! decoding. -//! * [`Family::Object`] is an interface pointer, borrowed as a parameter and -//! one strong reference as a return; a nullable one is `Interface?`. -//! * [`Family::Callback`] is a `void* ctx` plus `const {tag}_vtable*` pair, -//! only ever a parameter; a nullable one (`Cb?`) passes a null vtable for -//! none. +//! | Family | Parameter | Return | Async result | Iterator item | Callback return | +//! |---|---|---|---|---|---| +//! | Direct | `T n` | C return `T` | `T result` | `T* out_item` | C return `T` | +//! | OptDirect | `bool has_n, T n` | C return `bool`, `T* out_value` | `bool has_result, T result` | `bool* out_has_item, T* out_item` | C return `bool`, `T* out_value` | +//! | Slice | `const T* n_ptr, size_t n_len` | C return `T*`, `size_t* out_len` | `const T* result_ptr, size_t result_len` | `T** out_item, size_t* out_len` | `T** out_ptr, size_t* out_len` | +//! | String, Bytes, Buffer | `const uint8_t* n_ptr, size_t n_len` | C return `const uint8_t*`, `size_t* out_len` | `const uint8_t* result_ptr, size_t result_len` | `const uint8_t** out_item, size_t* out_len` | `uint8_t** out_ptr, size_t* out_len` | +//! | Object | `const {tag}* n` | C return `{tag}*` | `{tag}* result` | `{tag}** out_item` | C return `{tag}*` | //! -//! A callback-interface method's return lowers differently from a producer -//! return ([`lower_callback_return`]): the consumer produces the value, so a -//! string, bytes, or buffer return is a run the consumer allocates with -//! `{prefix}_alloc` and hands back through `uint8_t** out_ptr` and -//! `size_t* out_len`. +//! A callback method's parameter is lowered like a callable's, except that +//! an object parameter is a mutable `{tag}* n` (the consumer adopts one +//! strong reference). A callback interface parameter is +//! `void* n_ctx, const {vtable}* n_vtable`. -use crate::ty::{Family, Prim, Ty, TypeIndex}; +use crate::model::{c_tag, member_symbol}; +use crate::plan::{ArgPass, CallbackRetPass, ItemPass, ResultPass, RetPass}; +use crate::ty::{ParamTy, Prim, Ty, TypeIndex}; use super::ctype::{CType, ConstPos}; /// A named C parameter slot. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AbiParam { - /// The C parameter name (e.g. `out_err`, `data_ptr`, `contact_len`). + /// The C parameter name (for example `out_err`, `data_ptr`, `has_limit`). pub name: String, /// The C type of the slot. pub ty: CType, @@ -48,213 +45,400 @@ impl AbiParam { } } -/// A lowered return: the C return type plus any trailing out-parameters -/// (e.g. `size_t* out_len` for a bytes or buffered return). +/// A user type name the lowering needed a declaration for (an enum, +/// interface, or callback interface owner) but the index doesn't have. +/// +/// Validation rejects every such name before the model is built, so this is +/// only reachable when a rule is missing; the validator reports it as an +/// unknown type. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AbiReturn { - /// The C return type, or `void` when the value is delivered entirely - /// through [`out_params`](Self::out_params). - pub ret: CType, - /// Trailing out-parameter slots appended after the function's inputs. - pub out_params: Vec, +pub(crate) struct Undeclared(pub(crate) String); + +/// Where a parameter appears: a callable's input, or a callback method's +/// input (the consumer's trampoline receives it). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ParamSite { + /// A function, interface member, or launcher parameter. + Call, + /// A callback-interface method parameter. + CallbackMethod, } -/// The underscore-joined C path of the module declaring the user type -/// `name`. -/// -/// # Panics -/// -/// Panics when `name` isn't declared. Only records and rich enums may be -/// undeclared (the proc-macro assumes a name from a sibling module tree is -/// one of those), and they cross as value buffers, which never name a C -/// type. -fn owner_path(name: &str, types: &TypeIndex) -> String { - types - .module_path(name) - .unwrap_or_else(|| panic!("type '{name}' is not declared")) - .to_string() +/// A value type's lowering class: its family plus what the slots need. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Class<'a> { + Direct(CType), + OptDirect(CType), + Slice(Prim, CType), + String, + Bytes, + Buffer, + Object { interface: &'a str, nullable: bool }, } -/// The opaque C tag type of the interface `name`. -fn struct_tag(name: &str, types: &TypeIndex) -> CType { - CType::StructTag { - module: owner_path(name, types), - name: name.to_string(), - } +/// The lowering context: the type index (for C type owners) and the symbol +/// prefix (for the release symbols the contracts carry). +pub(crate) struct Lower<'a> { + pub(crate) types: &'a TypeIndex, + pub(crate) prefix: &'a str, } -/// The vtable struct type of the callback interface `name`. -fn vtable_tag(name: &str, types: &TypeIndex) -> CType { - CType::VtableTag { - module: owner_path(name, types), - name: name.to_string(), +impl<'a> Lower<'a> { + /// The underscore-joined C path of the module declaring `name`. + fn owner(&self, name: &str) -> Result { + self.types + .module_path(name) + .map(str::to_string) + .ok_or_else(|| Undeclared(name.to_string())) } -} -/// The by-value C type of a [`Family::Direct`] type. -fn direct_ctype(ty: &Ty, types: &TypeIndex) -> CType { - match ty { - Ty::Prim(p) => match p { - Prim::I8 => CType::Int8, - Prim::I16 => CType::Int16, - Prim::I32 => CType::Int32, - Prim::I64 => CType::Int64, - Prim::U8 => CType::Uint8, - Prim::U16 => CType::Uint16, - Prim::U32 => CType::Uint32, - Prim::U64 => CType::Uint64, - Prim::F32 => CType::Float, - Prim::F64 => CType::Double, - Prim::Bool => CType::Bool, - Prim::String | Prim::Bytes => unreachable!("{p} is not a direct type"), - }, - Ty::Enum(e) => CType::Enum { - module: owner_path(e, types), - name: e.clone(), - }, - other => unreachable!("{other} is not a direct type"), + fn enum_ctype(&self, name: &str) -> Result { + Ok(CType::Enum { + module: self.owner(name)?, + name: name.to_string(), + }) } -} -/// The two slots of a borrowed `(ptr, len)` parameter: `const uint8_t* -/// {name}_ptr` and `size_t {name}_len`. Shared by bytes and buffered values. -fn ptr_len_slots(name: &str) -> Vec { - vec![ - AbiParam::new(format!("{name}_ptr"), CType::const_ptr(CType::Uint8)), - AbiParam::new(format!("{name}_len"), CType::Size), - ] -} + /// The opaque C tag type of the interface `name`. + pub(crate) fn struct_tag(&self, name: &str) -> Result { + Ok(CType::StructTag { + module: self.owner(name)?, + name: name.to_string(), + }) + } -/// Expand one parameter into its ordered C ABI slots. -/// -/// # Panics -/// -/// Panics on an iterator type, which validation never admits as a parameter. -pub fn lower_param(name: &str, ty: &Ty, types: &TypeIndex) -> Vec { - match ty.family() { - Family::Direct => vec![AbiParam::new(name, direct_ctype(ty, types))], - Family::String | Family::Bytes | Family::Buffer => ptr_len_slots(name), - // An interface parameter borrows the object for the call: the callee - // reads through the const pointer and clones if it wants to retain - // the object. A nullable one is the same slot with null meaning none. - Family::Object { .. } => { - let iface = ty - .interface_name() - .expect("object family names an interface"); - vec![AbiParam::new( - name, - CType::Ptr { - konst: ConstPos::West, - pointee: Box::new(struct_tag(iface, types)), + /// The `{prefix}_{path}_{name}` C tag of the interface `name`. + fn tag_of(&self, name: &str) -> Result { + Ok(c_tag(self.prefix, &self.owner(name)?, name)) + } + + fn destroy_symbol(&self, interface: &str) -> Result { + Ok(member_symbol(&self.tag_of(interface)?, "destroy")) + } + + fn clone_symbol(&self, interface: &str) -> Result { + Ok(member_symbol(&self.tag_of(interface)?, "clone")) + } + + /// Classify a value type. Agrees with [`Ty::family`] by construction + /// (a unit test pins the agreement). + fn class<'t>(&self, ty: &'t Ty) -> Result, Undeclared> { + Ok(match ty { + Ty::Prim(Prim::String) => Class::String, + Ty::Prim(Prim::Bytes) => Class::Bytes, + Ty::Prim(p) => Class::Direct(CType::of_prim(*p)), + Ty::Enum(e) => Class::Direct(self.enum_ctype(e)?), + Ty::Interface(i) => Class::Object { + interface: i, + nullable: false, + }, + Ty::Optional(inner) => match inner.as_ref() { + Ty::Prim(p) if p.is_scalar() => Class::OptDirect(CType::of_prim(*p)), + Ty::Enum(e) => Class::OptDirect(self.enum_ctype(e)?), + Ty::Interface(i) => Class::Object { + interface: i, + nullable: true, }, - )] - } - // A callback interface is an opaque consumer context plus the - // consumer's static vtable for the interface. A nullable one passes a - // null vtable for none. - Family::Callback { .. } => { - let cb = ty - .callback_interface_name() - .expect("callback family names a callback interface"); - vec![ - AbiParam::new(format!("{name}_ctx"), CType::ptr(CType::Void)), - AbiParam::new( + _ => Class::Buffer, + }, + Ty::List(inner) => match inner.as_ref() { + Ty::Prim(p) if p.is_slice_elem() => Class::Slice(*p, CType::of_prim(*p)), + _ => Class::Buffer, + }, + Ty::Record(_) | Ty::RichEnum(_) | Ty::Map(_, _) => Class::Buffer, + }) + } + + /// Lower one parameter at `site`. + pub(crate) fn param( + &self, + name: &str, + ty: &ParamTy, + site: ParamSite, + ) -> Result { + match ty { + ParamTy::Value(ty) => self.value_param(name, ty, site), + // A callback interface is an opaque consumer context plus the + // consumer's static vtable for the interface. A nullable one + // passes a null vtable for none. + ParamTy::Callback { name: cb, nullable } => Ok(ArgPass::Callback { + ctx: AbiParam::new(format!("{name}_ctx"), CType::ptr(CType::Void)), + vtable: AbiParam::new( format!("{name}_vtable"), - CType::const_ptr(vtable_tag(cb, types)), + CType::const_ptr(CType::VtableTag { + module: self.owner(cb)?, + name: cb.clone(), + }), ), - ] + nullable: *nullable, + interface: cb.clone(), + }), } - Family::Iterator => unreachable!("iterator not valid as parameter"), } -} -/// Lower a return type to its C return type plus trailing out-parameters. -/// -/// # Panics -/// -/// Panics on an iterator type, whose launcher is lowered by the function -/// lowering in [`crate::model`] rather than as a plain value return, and on a -/// callback interface, which validation never admits as a return. -pub fn lower_return(ty: &Ty, types: &TypeIndex) -> AbiReturn { - let no_out = |ret| AbiReturn { - ret, - out_params: vec![], - }; - match ty.family() { - Family::Direct => no_out(direct_ctype(ty, types)), - // String, bytes, and buffered returns are producer-allocated byte - // runs: the caller copies or decodes them and then calls - // `{prefix}_free_bytes(ptr, len)`. - Family::String | Family::Bytes | Family::Buffer => AbiReturn { - ret: CType::const_ptr(CType::Uint8), - out_params: vec![AbiParam::new("out_len", CType::ptr(CType::Size))], - }, - // A returned interface transfers one strong reference; a nullable one - // may be null. - Family::Object { .. } => { - let iface = ty - .interface_name() - .expect("object family names an interface"); - no_out(CType::ptr(struct_tag(iface, types))) - } - Family::Callback { .. } => unreachable!("callback interfaces are never returned"), - Family::Iterator => { - unreachable!("iterator return handled specially by the function lowering") - } + /// Lower one value parameter at `site`. + pub(crate) fn value_param( + &self, + name: &str, + ty: &Ty, + site: ParamSite, + ) -> Result { + let ptr = |ty| AbiParam::new(format!("{name}_ptr"), ty); + let len = || AbiParam::new(format!("{name}_len"), CType::Size); + let bytes = || ptr(CType::const_ptr(CType::Uint8)); + Ok(match self.class(ty)? { + Class::Direct(c) => ArgPass::Direct { + slot: AbiParam::new(name, c), + }, + Class::OptDirect(c) => ArgPass::OptDirect { + has: AbiParam::new(format!("has_{name}"), CType::Bool), + value: AbiParam::new(name, c), + inner: match ty { + Ty::Optional(inner) => (**inner).clone(), + other => other.clone(), + }, + }, + Class::Slice(elem, c) => ArgPass::Slice { + ptr: ptr(CType::const_ptr(c)), + len: len(), + elem, + }, + Class::String => ArgPass::String { + ptr: bytes(), + len: len(), + }, + Class::Bytes => ArgPass::Bytes { + ptr: bytes(), + len: len(), + }, + Class::Buffer => ArgPass::Buffer { + ptr: bytes(), + len: len(), + }, + // A callable borrows the object for the call; a callback method + // receives one strong reference the consumer adopts, so its + // slot is mutable. + Class::Object { + interface, + nullable, + } => { + let konst = match site { + ParamSite::Call => ConstPos::West, + ParamSite::CallbackMethod => ConstPos::None, + }; + ArgPass::Object { + slot: AbiParam::new( + name, + CType::Ptr { + konst, + pointee: Box::new(self.struct_tag(interface)?), + }, + ), + nullable, + interface: interface.to_string(), + } + } + }) } -} -/// Lower the return type of a callback-interface method to its C return type -/// plus trailing out-parameters (placed before the method's `out_err`). -/// -/// A direct value is returned by value and an object as one strong -/// reference (`{tag}*`) the producer adopts. A string, bytes, or buffer -/// return is `void` with `uint8_t** out_ptr` and `size_t* out_len` slots: -/// the consumer allocates the run with `{prefix}_alloc`, writes it there, -/// and the producer adopts and frees it. -/// -/// # Panics -/// -/// Panics on an iterator or a callback interface, which validation never -/// admits as a callback method return. -pub fn lower_callback_return(ty: &Ty, types: &TypeIndex) -> AbiReturn { - match ty.family() { - Family::String | Family::Bytes | Family::Buffer => AbiReturn { - ret: CType::Void, - out_params: vec![ - AbiParam::new("out_ptr", CType::ptr(CType::ptr(CType::Uint8))), - AbiParam::new("out_len", CType::ptr(CType::Size)), - ], - }, - Family::Callback { .. } | Family::Iterator => { - unreachable!("{ty} is never a callback method return") - } - _ => lower_return(ty, types), + /// Lower a synchronous value return (or `void`) to its C return type and + /// passing contract. + pub(crate) fn ret(&self, ty: Option<&Ty>) -> Result<(CType, RetPass), Undeclared> { + let Some(ty) = ty else { + return Ok((CType::Void, RetPass::Void)); + }; + let out_len = || AbiParam::new("out_len", CType::ptr(CType::Size)); + let run = || CType::const_ptr(CType::Uint8); + Ok(match self.class(ty)? { + Class::Direct(c) => (c, RetPass::Direct), + Class::OptDirect(c) => ( + CType::Bool, + RetPass::OptDirect { + out_value: AbiParam::new("out_value", CType::ptr(c)), + }, + ), + Class::Slice(elem, c) => ( + CType::ptr(c), + RetPass::Slice { + out_len: out_len(), + elem, + }, + ), + Class::String => (run(), RetPass::String { out_len: out_len() }), + Class::Bytes => (run(), RetPass::Bytes { out_len: out_len() }), + Class::Buffer => (run(), RetPass::Buffer { out_len: out_len() }), + Class::Object { + interface, + nullable, + } => ( + CType::ptr(self.struct_tag(interface)?), + RetPass::Object { + nullable, + interface: interface.to_string(), + destroy_symbol: self.destroy_symbol(interface)?, + }, + ), + }) } -} -/// The trailing result fields appended to an async callback after the -/// `(context, err)` prefix. -/// -/// String, bytes, and buffered results are passed as an owned `ptr` + `len` -/// pair (released by the consumer with `{prefix}_free_bytes`); everything -/// else reuses its return slot type by value. -pub fn callback_result_params(ty: &Ty, types: &TypeIndex) -> Vec { - match ty.family() { - Family::String | Family::Bytes | Family::Buffer => vec![ - AbiParam::new("result_ptr", CType::const_ptr(CType::Uint8)), - AbiParam::new("result_len", CType::Size), - ], - _ => vec![AbiParam::new("result", lower_return(ty, types).ret)], + /// Lower an async result (or `void`) to its completion-callback slots. + pub(crate) fn result(&self, ty: Option<&Ty>) -> Result { + let Some(ty) = ty else { + return Ok(ResultPass::Void); + }; + let ptr = |c| AbiParam::new("result_ptr", c); + let len = || AbiParam::new("result_len", CType::Size); + let bytes = || ptr(CType::const_ptr(CType::Uint8)); + Ok(match self.class(ty)? { + Class::Direct(c) => ResultPass::Direct { + result: AbiParam::new("result", c), + }, + Class::OptDirect(c) => ResultPass::OptDirect { + has: AbiParam::new("has_result", CType::Bool), + value: AbiParam::new("result", c), + }, + Class::Slice(elem, c) => ResultPass::Slice { + ptr: ptr(CType::const_ptr(c)), + len: len(), + elem, + }, + Class::String => ResultPass::String { + ptr: bytes(), + len: len(), + }, + Class::Bytes => ResultPass::Bytes { + ptr: bytes(), + len: len(), + }, + Class::Buffer => ResultPass::Buffer { + ptr: bytes(), + len: len(), + }, + Class::Object { + interface, + nullable, + } => ResultPass::Object { + result: AbiParam::new("result", CType::ptr(self.struct_tag(interface)?)), + nullable, + interface: interface.to_string(), + destroy_symbol: self.destroy_symbol(interface)?, + }, + }) + } + + /// Lower an iterator element to `next`'s out slots. + pub(crate) fn item(&self, ty: &Ty) -> Result { + let out_item = |c| AbiParam::new("out_item", CType::ptr(c)); + let out_len = || AbiParam::new("out_len", CType::ptr(CType::Size)); + let run = || out_item(CType::const_ptr(CType::Uint8)); + Ok(match self.class(ty)? { + Class::Direct(c) => ItemPass::Direct { + out_item: out_item(c), + }, + Class::OptDirect(c) => ItemPass::OptDirect { + out_has: AbiParam::new("out_has_item", CType::ptr(CType::Bool)), + out_item: out_item(c), + }, + Class::Slice(elem, c) => ItemPass::Slice { + out_item: out_item(CType::ptr(c)), + out_len: out_len(), + elem, + }, + Class::String => ItemPass::String { + out_item: run(), + out_len: out_len(), + }, + Class::Bytes => ItemPass::Bytes { + out_item: run(), + out_len: out_len(), + }, + Class::Buffer => ItemPass::Buffer { + out_item: run(), + out_len: out_len(), + }, + Class::Object { + interface, + nullable, + } => ItemPass::Object { + out_item: out_item(CType::ptr(self.struct_tag(interface)?)), + nullable, + interface: interface.to_string(), + destroy_symbol: self.destroy_symbol(interface)?, + }, + }) + } + + /// Lower a callback method's return (or `void`) to its C return type and + /// passing contract. The consumer produces the value, so a run is one + /// it allocates with `{prefix}_alloc` and hands back through out slots. + pub(crate) fn callback_ret( + &self, + ty: Option<&Ty>, + ) -> Result<(CType, CallbackRetPass), Undeclared> { + let Some(ty) = ty else { + return Ok((CType::Void, CallbackRetPass::Void)); + }; + let out_ptr = |c| AbiParam::new("out_ptr", CType::ptr(CType::ptr(c))); + let out_len = || AbiParam::new("out_len", CType::ptr(CType::Size)); + let run = || out_ptr(CType::Uint8); + Ok(match self.class(ty)? { + Class::Direct(c) => (c, CallbackRetPass::Direct), + Class::OptDirect(c) => ( + CType::Bool, + CallbackRetPass::OptDirect { + out_value: AbiParam::new("out_value", CType::ptr(c)), + }, + ), + Class::Slice(elem, c) => ( + CType::Void, + CallbackRetPass::Slice { + out_ptr: out_ptr(c), + out_len: out_len(), + elem, + }, + ), + Class::String => ( + CType::Void, + CallbackRetPass::String { + out_ptr: run(), + out_len: out_len(), + }, + ), + Class::Bytes => ( + CType::Void, + CallbackRetPass::Bytes { + out_ptr: run(), + out_len: out_len(), + }, + ), + Class::Buffer => ( + CType::Void, + CallbackRetPass::Buffer { + out_ptr: run(), + out_len: out_len(), + }, + ), + Class::Object { + interface, + nullable, + } => ( + CType::ptr(self.struct_tag(interface)?), + CallbackRetPass::Object { + nullable, + interface: interface.to_string(), + clone_symbol: self.clone_symbol(interface)?, + }, + ), + }) } } #[cfg(test)] mod tests { use super::*; - use crate::ty::{TypeDecl, TypeKind}; + use crate::ty::{Family, TypeDecl, TypeKind}; - fn render(params: &[AbiParam]) -> Vec { + fn render(params: &[&AbiParam]) -> Vec { params .iter() .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) @@ -282,127 +466,414 @@ mod tests { types } - const I32: Ty = Ty::Prim(Prim::I32); + fn with(f: impl FnOnce(&Lower<'_>) -> T) -> T { + let types = types(); + f(&Lower { + types: &types, + prefix: "weaveffi", + }) + } + + fn ty(s: &str) -> Ty { + fn resolve(t: &crate::ir::TypeRef) -> Ty { + use crate::ir::TypeRef; + match t { + TypeRef::Prim(p) => Ty::Prim(*p), + TypeRef::Named(n) if n == "Store" => Ty::Interface(n.clone()), + TypeRef::Named(n) if n == "Status" => Ty::Enum(n.clone()), + TypeRef::Named(n) => Ty::Record(n.clone()), + TypeRef::Optional(i) => Ty::Optional(Box::new(resolve(i))), + TypeRef::List(i) => Ty::List(Box::new(resolve(i))), + TypeRef::Map(k, v) => Ty::Map(Box::new(resolve(k)), Box::new(resolve(v))), + TypeRef::Iterator(_) => unreachable!("not a value"), + } + } + resolve(&crate::ir::parse_type_ref(s).unwrap()) + } + + fn param(name: &str, t: &str, site: ParamSite) -> Vec { + with(|l| render(&l.value_param(name, &ty(t), site).unwrap().slots())) + } + + fn ret(t: &str) -> (String, Vec) { + with(|l| { + let (c, pass) = l.ret(Some(&ty(t))).unwrap(); + (c.render_c("weaveffi"), render(&pass.out_slots())) + }) + } + + fn result(t: &str) -> Vec { + with(|l| render(&l.result(Some(&ty(t))).unwrap().slots())) + } + + fn item(t: &str) -> Vec { + with(|l| render(&l.item(&ty(t)).unwrap().slots())) + } + + fn cb_ret(t: &str) -> (String, Vec) { + with(|l| { + let (c, pass) = l.callback_ret(Some(&ty(t))).unwrap(); + (c.render_c("weaveffi"), render(&pass.out_slots())) + }) + } + + #[test] + fn classes_agree_with_families() { + let cases = [ + "i32", + "bool", + "Status", + "string", + "bytes", + "Contact", + "Store", + "Store?", + "i32?", + "bool?", + "Status?", + "u8?", + "[i32]", + "[f64]", + "[u64]", + "[i8]", + "[bool]", + "[string]", + "[Status]", + "[[i32]]", + "i32??", + "string?", + "[i32]?", + "{string:i32}", + "[Store]", + ]; + with(|l| { + for s in cases { + let t = ty(s); + let class = l.class(&t).unwrap(); + let agrees = matches!( + (t.family(), &class), + (Family::Direct, Class::Direct(_)) + | (Family::OptDirect, Class::OptDirect(_)) + | (Family::String, Class::String) + | (Family::Bytes, Class::Bytes) + | (Family::Buffer, Class::Buffer) + ) || matches!( + (t.family(), &class), + (Family::Slice(a), Class::Slice(b, _)) if a == *b + ) || matches!( + (t.family(), &class), + (Family::Object { nullable: a }, Class::Object { nullable: b, .. }) if a == *b + ); + assert!(agrees, "{s}: {:?} vs {class:?}", t.family()); + } + }); + } #[test] fn params_lower_by_family() { - let t = types(); - assert_eq!(render(&lower_param("x", &I32, &t)), ["int32_t x"]); + let call = ParamSite::Call; + assert_eq!(param("x", "i32", call), ["int32_t x"]); assert_eq!( - render(&lower_param("s", &Ty::Prim(Prim::String), &t)), + param("s", "string", call), ["const uint8_t* s_ptr", "size_t s_len"] ); assert_eq!( - render(&lower_param("data", &Ty::Prim(Prim::Bytes), &t)), + param("data", "bytes", call), ["const uint8_t* data_ptr", "size_t data_len"] ); assert_eq!( - render(&lower_param("xs", &Ty::List(Box::new(I32)), &t)), + param("xs", "[string]", call), ["const uint8_t* xs_ptr", "size_t xs_len"] ); - // An undeclared record (a proc-macro's sibling-tree type) is fine: - // value buffers never name a C type. assert_eq!( - render(&lower_param("c", &Ty::Record("Contact".into()), &t)), + param("c", "Contact", call), ["const uint8_t* c_ptr", "size_t c_len"] ); + assert_eq!(param("s", "Store?", call), ["const weaveffi_kv_Store* s"]); + assert_eq!(param("s", "Status", call), ["weaveffi_shared_Status s"]); + // An optional that isn't OptDirect stays a buffer. assert_eq!( - render(&lower_param( - "s", - &Ty::Optional(Box::new(Ty::Interface("Store".into()))), - &t - )), - ["const weaveffi_kv_Store* s"] + param("o", "string?", call), + ["const uint8_t* o_ptr", "size_t o_len"] + ); + assert_eq!( + param("o", "[bool]", call), + ["const uint8_t* o_ptr", "size_t o_len"] ); + with(|l| { + let cb = ParamTy::Callback { + name: "Listener".into(), + nullable: true, + }; + let pass = l.param("l", &cb, call).unwrap(); + assert_eq!( + render(&pass.slots()), + [ + "void* l_ctx", + "const weaveffi_events_Listener_vtable* l_vtable" + ] + ); + assert!( + matches!(pass, ArgPass::Callback { nullable: true, ref interface, .. } if interface == "Listener") + ); + }); + } + + #[test] + fn opt_direct_params() { + for site in [ParamSite::Call, ParamSite::CallbackMethod] { + assert_eq!( + param("limit", "i32?", site), + ["bool has_limit", "int32_t limit"] + ); + assert_eq!(param("flag", "bool?", site), ["bool has_flag", "bool flag"]); + assert_eq!(param("r", "f64?", site), ["bool has_r", "double r"]); + assert_eq!( + param("s", "Status?", site), + ["bool has_s", "weaveffi_shared_Status s"] + ); + } + with(|l| { + let pass = l.value_param("s", &ty("Status?"), ParamSite::Call).unwrap(); + let ArgPass::OptDirect { has, value, inner } = pass else { + panic!("expected OptDirect, got {pass:?}"); + }; + assert_eq!(has.name, "has_s"); + assert_eq!(value.name, "s"); + assert_eq!(inner, Ty::Enum("Status".into())); + }); + } + + #[test] + fn slice_params() { + for site in [ParamSite::Call, ParamSite::CallbackMethod] { + assert_eq!( + param("xs", "[f64]", site), + ["const double* xs_ptr", "size_t xs_len"] + ); + assert_eq!( + param("xs", "[i32]", site), + ["const int32_t* xs_ptr", "size_t xs_len"] + ); + assert_eq!( + param("xs", "[u64]", site), + ["const uint64_t* xs_ptr", "size_t xs_len"] + ); + assert_eq!( + param("xs", "[i8]", site), + ["const int8_t* xs_ptr", "size_t xs_len"] + ); + assert_eq!( + param("xs", "[f32]", site), + ["const float* xs_ptr", "size_t xs_len"] + ); + } + with(|l| { + let pass = l.value_param("xs", &ty("[u16]"), ParamSite::Call).unwrap(); + assert!(matches!( + pass, + ArgPass::Slice { + elem: Prim::U16, + .. + } + )); + }); + } + + #[test] + fn callback_method_object_params_transfer_a_reference() { assert_eq!( - render(&lower_param("s", &Ty::Enum("Status".into()), &t)), - ["weaveffi_shared_Status s"] + param("s", "Store", ParamSite::Call), + ["const weaveffi_kv_Store* s"] ); assert_eq!( - render(&lower_param( - "listener", - &Ty::CallbackInterface("Listener".into()), - &t - )), - [ - "void* listener_ctx", - "const weaveffi_events_Listener_vtable* listener_vtable" - ] + param("s", "Store", ParamSite::CallbackMethod), + ["weaveffi_kv_Store* s"] ); } #[test] fn returns_lower_by_family() { - let t = types(); - let r = lower_return(&Ty::Prim(Prim::Bytes), &t); - assert_eq!(r.ret.render_c("weaveffi"), "const uint8_t*"); - assert_eq!(render(&r.out_params), ["size_t* out_len"]); - for ty in [ - Ty::Record("Contact".into()), - Ty::RichEnum("Shape".into()), - Ty::List(Box::new(Ty::Record("Contact".into()))), - Ty::List(Box::new(Ty::Interface("Store".into()))), - Ty::Map(Box::new(Ty::Prim(Prim::String)), Box::new(I32)), - Ty::Optional(Box::new(Ty::Prim(Prim::I64))), + assert_eq!(ret("i32"), ("int32_t".into(), vec![])); + assert_eq!(ret("Status"), ("weaveffi_shared_Status".into(), vec![])); + for t in [ + "string", + "bytes", + "Contact", + "[string]", + "[Store]", + "{string:i32}", + "string?", ] { - let r = lower_return(&ty, &t); - assert_eq!(r.ret.render_c("weaveffi"), "const uint8_t*", "{ty}"); - assert_eq!(render(&r.out_params), ["size_t* out_len"], "{ty}"); + assert_eq!( + ret(t), + ("const uint8_t*".into(), vec!["size_t* out_len".into()]), + "{t}" + ); } - let r = lower_return(&Ty::Optional(Box::new(Ty::Interface("Store".into()))), &t); - assert_eq!(r.ret.render_c("weaveffi"), "weaveffi_kv_Store*"); - assert!(r.out_params.is_empty()); - assert_eq!( - lower_return(&Ty::Enum("Status".into()), &t) - .ret - .render_c("weaveffi"), - "weaveffi_shared_Status" - ); + assert_eq!(ret("Store?"), ("weaveffi_kv_Store*".into(), vec![])); + with(|l| { + let (_, pass) = l.ret(Some(&ty("Store"))).unwrap(); + assert_eq!( + pass, + RetPass::Object { + nullable: false, + interface: "Store".into(), + destroy_symbol: "weaveffi_kv_Store_destroy".into(), + } + ); + assert_eq!(l.ret(None).unwrap(), (CType::Void, RetPass::Void)); + }); } #[test] - fn callback_method_returns_use_consumer_allocated_runs() { - let t = types(); - let r = lower_callback_return(&Ty::Prim(Prim::String), &t); - assert_eq!(r.ret, CType::Void); + fn opt_direct_and_slice_returns() { + assert_eq!( + ret("i64?"), + ("bool".into(), vec!["int64_t* out_value".into()]) + ); assert_eq!( - render(&r.out_params), - ["uint8_t** out_ptr", "size_t* out_len"] + ret("bool?"), + ("bool".into(), vec!["bool* out_value".into()]) ); - let r = lower_callback_return(&Ty::Interface("Store".into()), &t); - assert_eq!(r.ret.render_c("weaveffi"), "weaveffi_kv_Store*"); - assert!(r.out_params.is_empty()); - assert_eq!(lower_callback_return(&I32, &t).ret, CType::Int32); assert_eq!( - render(&lower_param( - "l", - &Ty::Optional(Box::new(Ty::CallbackInterface("Listener".into()))), - &t - )), - [ - "void* l_ctx", - "const weaveffi_events_Listener_vtable* l_vtable" - ] + ret("Status?"), + ( + "bool".into(), + vec!["weaveffi_shared_Status* out_value".into()] + ) ); + assert_eq!( + ret("[f64]"), + ("double*".into(), vec!["size_t* out_len".into()]) + ); + assert_eq!( + ret("[u64]"), + ("uint64_t*".into(), vec!["size_t* out_len".into()]) + ); + with(|l| { + let (_, pass) = l.ret(Some(&ty("[i32]"))).unwrap(); + assert!( + matches!(pass, RetPass::Slice { elem: Prim::I32, ref out_len } if out_len.name == "out_len") + ); + }); } #[test] - fn callback_results() { - let t = types(); + fn async_results() { + assert_eq!(result("i32"), ["int32_t result"]); + assert_eq!(result("i64?"), ["bool has_result", "int64_t result"]); + assert_eq!( + result("Status?"), + ["bool has_result", "weaveffi_shared_Status result"] + ); + assert_eq!( + result("[f32]"), + ["const float* result_ptr", "size_t result_len"] + ); assert_eq!( - render(&callback_result_params( - &Ty::List(Box::new(Ty::Prim(Prim::String))), - &t - )), + result("string"), ["const uint8_t* result_ptr", "size_t result_len"] ); assert_eq!( - render(&callback_result_params(&Ty::Prim(Prim::Bytes), &t)), + result("[string]"), ["const uint8_t* result_ptr", "size_t result_len"] ); + assert_eq!(result("Store?"), ["weaveffi_kv_Store* result"]); + with(|l| { + assert_eq!(l.result(None).unwrap(), ResultPass::Void); + let pass = l.result(Some(&ty("[f32]"))).unwrap(); + assert!(matches!( + pass, + ResultPass::Slice { + elem: Prim::F32, + .. + } + )); + let pass = l.result(Some(&ty("Store"))).unwrap(); + assert!( + matches!(pass, ResultPass::Object { ref destroy_symbol, .. } if destroy_symbol == "weaveffi_kv_Store_destroy") + ); + }); + } + + #[test] + fn iterator_items() { + assert_eq!(item("i32"), ["int32_t* out_item"]); + assert_eq!(item("i32?"), ["bool* out_has_item", "int32_t* out_item"]); + assert_eq!(item("[i32]"), ["int32_t** out_item", "size_t* out_len"]); + assert_eq!( + item("string"), + ["const uint8_t** out_item", "size_t* out_len"] + ); + assert_eq!( + item("Contact"), + ["const uint8_t** out_item", "size_t* out_len"] + ); + assert_eq!(item("Store"), ["weaveffi_kv_Store** out_item"]); + with(|l| { + assert!(matches!( + l.item(&ty("[u32]")).unwrap(), + ItemPass::Slice { + elem: Prim::U32, + .. + } + )); + }); + } + + #[test] + fn callback_method_returns() { + assert_eq!(cb_ret("i32"), ("int32_t".into(), vec![])); + assert_eq!( + cb_ret("f64?"), + ("bool".into(), vec!["double* out_value".into()]) + ); assert_eq!( - render(&callback_result_params(&I32, &t)), - ["int32_t result"] + cb_ret("[i16]"), + ( + "void".into(), + vec!["int16_t** out_ptr".into(), "size_t* out_len".into()] + ) ); + for t in ["string", "bytes", "Contact?", "[string]"] { + assert_eq!( + cb_ret(t), + ( + "void".into(), + vec!["uint8_t** out_ptr".into(), "size_t* out_len".into()] + ), + "{t}" + ); + } + assert_eq!(cb_ret("Store"), ("weaveffi_kv_Store*".into(), vec![])); + with(|l| { + let (_, pass) = l.callback_ret(Some(&ty("Store?"))).unwrap(); + assert!( + matches!(pass, CallbackRetPass::Object { nullable: true, ref clone_symbol, .. } if clone_symbol == "weaveffi_kv_Store_clone") + ); + assert_eq!( + l.callback_ret(None).unwrap(), + (CType::Void, CallbackRetPass::Void) + ); + }); + } + + #[test] + fn undeclared_owners_are_errors_not_panics() { + with(|l| { + assert_eq!( + l.value_param("x", &Ty::Enum("Nope".into()), ParamSite::Call), + Err(Undeclared("Nope".into())) + ); + assert_eq!( + l.ret(Some(&Ty::Interface("Nope".into()))), + Err(Undeclared("Nope".into())) + ); + // Records never name a C type, so a foreign one lowers fine. + assert!(l + .value_param("c", &Ty::Record("Elsewhere".into()), ParamSite::Call) + .is_ok()); + }); } } diff --git a/crates/weaveffi-model/src/abi/mod.rs b/crates/weaveffi-model/src/abi/mod.rs index 7e5aa658..6a36461c 100644 --- a/crates/weaveffi-model/src/abi/mod.rs +++ b/crates/weaveffi-model/src/abi/mod.rs @@ -1,11 +1,13 @@ //! The canonical WeaveFFI C ABI lowering. //! //! This module is the single source of truth for *how a resolved type lowers -//! onto the stable C ABI*: the ordered C slots of every parameter and return -//! and how every [`Ty`](crate::ty::Ty) crosses the boundary (by value, as a -//! pointer, as `ptr`+`len`, as a serialized value buffer, with a trailing -//! `out_err`, and so on). The [`Model`](crate::model::Model) assembles these -//! slots into each symbol's full signature once. +//! onto the stable C ABI*: the ordered C slots of every parameter, return, +//! async result, iterator item, and callback-method parameter and return +//! (by value, as a presence flag plus a value, as a typed array, as +//! `ptr`+`len`, as a serialized value buffer, with a trailing `out_err`, and +//! so on). The [`Model`](crate::model::Model) runs it once and stores the +//! result on every binding as the passing contracts in [`crate::plan`] and +//! the [`AbiFn`](crate::model::AbiFn) signatures. //! //! Every language generator and the producer macro read the resulting //! [`CType`]s from the model and map them onto their own FFI vocabulary. The @@ -15,27 +17,29 @@ pub mod ctype; pub mod lower; pub use ctype::{CType, ConstPos}; -pub use lower::{ - callback_result_params, lower_callback_return, lower_param, lower_return, AbiParam, AbiReturn, -}; +pub use lower::AbiParam; /// The trailing `out_err` parameter every fallible WeaveFFI symbol carries. +#[must_use] pub fn error_out_param() -> AbiParam { AbiParam::new("out_err", CType::ptr(CType::Error)) } /// The `void* context` token threaded through async completion callbacks. +#[must_use] pub fn context_param() -> AbiParam { AbiParam::new("context", CType::ptr(CType::Void)) } /// The leading `void* ctx` slot of every callback-interface method (the /// consumer's opaque implementation handle). +#[must_use] pub fn ctx_param() -> AbiParam { AbiParam::new("ctx", CType::ptr(CType::Void)) } /// The optional `{prefix}_cancel_token*` parameter of a cancellable async call. +#[must_use] pub fn cancel_token_param() -> AbiParam { AbiParam::new("cancel_token", CType::ptr(CType::CancelToken)) } diff --git a/crates/weaveffi-model/src/contract.rs b/crates/weaveffi-model/src/contract.rs index b27ec6b8..7b072a9a 100644 --- a/crates/weaveffi-model/src/contract.rs +++ b/crates/weaveffi-model/src/contract.rs @@ -15,28 +15,40 @@ //! it on the validated [`Model`], so the two sides agree by construction. //! //! * The **id** is [`fnv1a64`] of the declaration's dotted path -//! (`kv.Store.put`, `kv.KvError`, `kv.stats.Summary`). +//! (`kv.Store.put`, `kv.KvError`, `kv.KvError.NotFound`, +//! `kv.Listener.on_put`). //! * The **hash** is [`fnv1a64`] of a canonical signature string computed //! from the resolved model, never from source spelling: type aliases are //! already substituted, `[u8]` is already `bytes`, and every type is its -//! resolved IDL spelling (see [`Ty`]'s `Display`). Docs, deprecation text, +//! resolved IDL spelling (see [`Ty`]'s `Display`). Names that don't +//! affect the ABI (parameter and field names), docs, deprecation text, //! and error messages are excluded, and so is the order of sibling //! declarations, because each entry is independent. //! +//! Error domains and callback interfaces are **open**: each code and each +//! callback method is its own entry, so adding a code or a method never +//! breaks a deployed binding (a consumer maps a positive code it doesn't +//! know to the domain's base error type, and a vtable may grow at its end; +//! its `size` header is still checked). Inserting a method anywhere else, or +//! reordering methods, moves slots and breaks the binding. +//! //! The canonical strings are: //! -//! | Declaration | Signature | -//! |---|---| -//! | function or interface member | `{kind} {name}({param}: {type}, ...) -> {type or void}`, then ` throws`, ` async`, ` cancellable` when set; `kind` is `function`, `constructor`, `method`, or `static` | -//! | record | `record {name} {{field}: {type}, ...}` | -//! | enum | `enum {name} {{Variant} = {value}, ...}`, a rich variant followed by ` {{field}: {type}, ...}` | -//! | callback interface | `callback {name} {{method}({param}: {type}, ...) -> {type or void}; ...}`, a method followed by ` throws` when set | -//! | error domain | `errors {name} {{Code} = {value}, ...}`, a code with fields followed by ` {{field}: {type}, ...}` | -//! | interface | `interface {name}` (its members have their own entries) | +//! | Declaration | Path | Signature | +//! |---|---|---| +//! | function or interface member | `{module}.{name}`, `{module}.{Interface}.{name}` | `{kind} {name}({type}, ...) -> {type or void}`, then ` throws {Domain}` or ` throws any`, ` async`, ` cancellable` when set; `kind` is `function`, `constructor`, `method`, or `static` | +//! | interface | `{module}.{Interface}` | `interface {name}` (its members have their own entries) | +//! | record | `{module}.{Record}` | `record {name} {{type}, ...}` (field types only, in order) | +//! | enum | `{module}.{Enum}` | `enum {name} {{Variant} = {value}, ...}`, a variant with fields followed by ` {{type}, ...}` | +//! | callback interface | `{module}.{Callback}` | `callback {name}` | +//! | callback method | `{module}.{Callback}.{method}` | `callback_method #{slot} {name}({type}, ...) -> {type or void}` (`slot` is the method's zero-based vtable position), then ` throws {Domain}` or ` throws any` when set | +//! | error domain | `{module}.{Domain}` | `errors {name}` | +//! | error code | `{module}.{Domain}.{Code}` | `code {Code} = {value}`, a code with fields followed by ` {{type}, ...}` | use std::fmt::Write; -use crate::model::{FieldBinding, FnBinding, Model, ModuleBinding, ParamBinding}; +use crate::model::{FieldBinding, FnBinding, Model, ModuleBinding}; +use crate::plan::ErrorStrategy; use crate::ty::Ty; /// One declaration's entry in a module's contract table. @@ -47,8 +59,11 @@ pub struct ContractEntry { pub path: String, /// [`fnv1a64`] of [`path`](Self::path). pub id: u64, - /// [`fnv1a64`] of the declaration's canonical signature string. + /// [`fnv1a64`] of [`signature`](Self::signature). pub hash: u64, + /// The declaration's canonical signature string (see the table in the + /// [module docs](self)), kept for diagnostics and generated comments. + pub signature: String, } /// The 64-bit FNV-1a hash of `data`: tiny, dependency-free, and stable @@ -65,28 +80,27 @@ pub const fn fnv1a64(data: &[u8]) -> u64 { hash } -/// The contract table of the top-level module `root` (which must be one of -/// `model`'s [roots](Model::roots)): one entry per declaration in `root` and -/// its submodules, sorted ascending by id. +/// The contract table of the top-level module `root` (one of `model`'s +/// [roots](Model::roots)): one entry per declaration in `root` and its +/// submodules, sorted ascending by id. #[must_use] pub fn entries(model: &Model, root: &ModuleBinding) -> Vec { let mut out = Vec::new(); - for m in model - .modules - .iter() - .filter(|m| m.segments.first() == root.segments.first()) - { + let mut stack = vec![root]; + while let Some(m) = stack.pop() { module_entries(m, &mut out); + stack.extend(model.children(m)); } out.sort_by_key(|e| e.id); out } -fn entry(path: String, signature: &str) -> ContractEntry { +fn entry(path: String, signature: String) -> ContractEntry { ContractEntry { id: fnv1a64(path.as_bytes()), hash: fnv1a64(signature.as_bytes()), path, + signature, } } @@ -95,12 +109,12 @@ fn module_entries(m: &ModuleBinding, out: &mut Vec) { for f in &m.functions { out.push(entry( format!("{dot}.{}", f.name), - &callable_signature("function", f), + callable_signature("function", f), )); } for i in &m.interfaces { let owner = format!("{dot}.{}", i.name); - out.push(entry(owner.clone(), &format!("interface {}", i.name))); + out.push(entry(owner.clone(), format!("interface {}", i.name))); for (kind, members) in [ ("constructor", &i.constructors), ("method", &i.methods), @@ -109,7 +123,7 @@ fn module_entries(m: &ModuleBinding, out: &mut Vec) { for f in members { out.push(entry( format!("{owner}.{}", f.name), - &callable_signature(kind, f), + callable_signature(kind, f), )); } } @@ -117,7 +131,7 @@ fn module_entries(m: &ModuleBinding, out: &mut Vec) { for s in &m.structs { out.push(entry( format!("{dot}.{}", s.name), - &format!("record {} {{{}}}", s.name, fields(&s.fields)), + format!("record {} {{{}}}", s.name, field_types(&s.fields)), )); } for e in &m.enums { @@ -128,192 +142,226 @@ fn module_entries(m: &ModuleBinding, out: &mut Vec) { .collect(); out.push(entry( format!("{dot}.{}", e.name), - &format!("enum {} {{{}}}", e.name, variants.join(", ")), + format!("enum {} {{{}}}", e.name, variants.join(", ")), )); } for c in &m.callback_interfaces { - let methods: Vec = c - .methods - .iter() - .map(|cm| { - let mut sig = format!( - "{}({}) -> {}", - cm.name, - params(&cm.params), - ret(cm.ret.as_ref()) - ); - if cm.throws { - sig.push_str(" throws"); - } - sig - }) - .collect(); - out.push(entry( - format!("{dot}.{}", c.name), - &format!("callback {} {{{}}}", c.name, methods.join("; ")), - )); + let owner = format!("{dot}.{}", c.name); + out.push(entry(owner.clone(), format!("callback {}", c.name))); + for (slot, cm) in c.methods.iter().enumerate() { + let types: Vec = cm.params.iter().map(|p| p.ty.to_string()).collect(); + // The slot is the method's position in the vtable: reordering + // methods, or inserting one before others, must fail the check, + // while appending one leaves every existing slot unchanged. + let mut sig = format!( + "callback_method #{slot} {}({}) -> {}", + cm.name, + types.join(", "), + cm.ret + .as_ref() + .map_or_else(|| "void".to_string(), Ty::to_string) + ); + throws(&mut sig, &cm.error); + out.push(entry(format!("{owner}.{}", cm.name), sig)); + } } - if let Some(d) = &m.errors { - let codes: Vec = d - .codes - .iter() - .map(|c| case(&c.name, c.value, &c.fields)) - .collect(); - out.push(entry( - format!("{dot}.{}", d.name), - &format!("errors {} {{{}}}", d.name, codes.join(", ")), - )); + for d in &m.errors { + let owner = format!("{dot}.{}", d.name); + out.push(entry(owner.clone(), format!("errors {}", d.name))); + for c in &d.codes { + out.push(entry( + format!("{owner}.{}", c.name), + format!("code {}", case(&c.name, c.value, &c.fields)), + )); + } } } -fn params(params: &[ParamBinding]) -> String { - let all: Vec = params - .iter() - .map(|p| format!("{}: {}", p.name, p.ty)) - .collect(); +/// The field types, in order, comma-separated (names are ABI-neutral). +fn field_types(fields: &[FieldBinding]) -> String { + let all: Vec = fields.iter().map(|f| f.ty.to_string()).collect(); all.join(", ") } -fn fields(fields: &[FieldBinding]) -> String { - let all: Vec = fields - .iter() - .map(|f| format!("{}: {}", f.name, f.ty)) - .collect(); - all.join(", ") -} - -fn ret(ty: Option<&Ty>) -> String { - ty.map_or_else(|| "void".to_string(), ToString::to_string) -} - -/// `Name = value`, followed by ` {fields}` when the case carries any. +/// `Name = value`, followed by ` {types}` when the case carries fields. fn case(name: &str, value: i32, case_fields: &[FieldBinding]) -> String { let mut out = format!("{name} = {value}"); if !case_fields.is_empty() { - let _ = write!(out, " {{{}}}", fields(case_fields)); + let _ = write!(out, " {{{}}}", field_types(case_fields)); } out } +/// Append ` throws {Domain}` or ` throws any` for a throwing strategy. +fn throws(sig: &mut String, error: &ErrorStrategy) { + match error { + ErrorStrategy::Trap => {} + ErrorStrategy::Domain(domain) => { + let _ = write!(sig, " throws {domain}"); + } + ErrorStrategy::Untyped => sig.push_str(" throws any"), + } +} + fn callable_signature(kind: &str, f: &FnBinding) -> String { + let types: Vec = f.params.iter().map(|p| p.ty.to_string()).collect(); let mut sig = format!( "{kind} {}({}) -> {}", f.name, - params(&f.params), - ret(f.ret.as_ref()) + types.join(", "), + f.ret + .as_ref() + .map_or_else(|| "void".to_string(), ToString::to_string) ); - if f.throws { - sig.push_str(" throws"); - } + throws(&mut sig, &f.error); if f.is_async() { sig.push_str(" async"); } - if f.cancellable { + if f.cancellable() { sig.push_str(" cancellable"); } sig } -#[cfg(test)] +#[cfg(all(test, feature = "idl"))] mod tests { use super::*; use crate::ir::Api; use crate::pkg::Identity; fn model(yaml: &str) -> Model { - let api: Api = serde_yaml::from_str(yaml).unwrap(); + let api: Api = serde_saphyr::from_str(yaml).unwrap(); crate::validate::validate(&api, &Identity::named("kv"), None).unwrap() } const KV: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: kv doc: Key-value storage. errors: - name: KvError - codes: - - { name: NotFound, code: 1, message: missing, fields: [{ name: key, type: string }] } + - name: KvError + codes: + - { name: NotFound, code: 1, message: missing, fields: [{ name: key, type: string }] } + - { name: Full, code: 2, message: full } structs: - name: Entry fields: [{ name: key, type: string }, { name: value, type: bytes }] interfaces: - name: Store - constructors: [{ name: open, params: [{ name: path, type: string }], throws: true }] + constructors: [{ name: open, params: [{ name: path, type: string }], throws: KvError }] methods: - - { name: get, params: [{ name: key, type: string }], return: "bytes?", throws: true } + - { name: get, params: [{ name: key, type: string }], return: Entry, throws: KvError } + - { name: sizes, params: [{ name: limit, type: "u32?" }], return: "[u64]", throws: any } callback_interfaces: - name: Listener methods: - - { name: on_put, params: [{ name: entry, type: Entry }], return: string, throws: true } + - { name: on_put, params: [{ name: entry, type: Entry }], return: string, throws: KvError } + - { name: on_clear, params: [] } functions: - { name: version, params: [], return: string, doc: The version. } + - { name: fetch, params: [{ name: key, type: string }], return: "i64?", async: true, cancellable: true } + - { name: watch, params: [{ name: l, type: "Listener?" }], return: "iter<[i32]>" } modules: - name: stats enums: - name: Kind variants: [{ name: Hot, value: 0 }, { name: Cold, value: 1 }] + - name: Shape + variants: [{ name: Dot, value: 0 }, { name: Circle, value: 1, fields: [{ name: r, type: f64 }] }] "#; - fn paths(model: &Model) -> Vec { + fn table(model: &Model) -> Vec { let root = model.roots().next().unwrap(); - entries(model, root).into_iter().map(|e| e.path).collect() + entries(model, root) + } + + fn signature(model: &Model, path: &str) -> String { + let entry = table(model).into_iter().find(|e| e.path == path); + let entry = entry.unwrap_or_else(|| panic!("no entry {path}")); + assert_eq!(entry.id, fnv1a64(path.as_bytes())); + assert_eq!(entry.hash, fnv1a64(entry.signature.as_bytes())); + entry.signature } #[test] - fn one_entry_per_declaration_sorted_by_id() { + fn one_entry_per_declaration_code_and_callback_method_sorted_by_id() { let model = model(KV); - let root = model.roots().next().unwrap(); - let table = entries(&model, root); - let mut got = paths(&model); + let table = table(&model); + let mut got: Vec = table.iter().map(|e| e.path.clone()).collect(); got.sort(); assert_eq!( got, [ "kv.Entry", "kv.KvError", + "kv.KvError.Full", + "kv.KvError.NotFound", "kv.Listener", + "kv.Listener.on_clear", + "kv.Listener.on_put", "kv.Store", "kv.Store.get", "kv.Store.open", + "kv.Store.sizes", + "kv.fetch", "kv.stats.Kind", - "kv.version" + "kv.stats.Shape", + "kv.version", + "kv.watch", ] ); assert!(table.windows(2).all(|w| w[0].id < w[1].id)); - let get = table.iter().find(|e| e.path == "kv.Store.get").unwrap(); - assert_eq!(get.id, fnv1a64(b"kv.Store.get")); - assert_eq!( - get.hash, - fnv1a64(b"method get(key: string) -> bytes? throws") - ); - let listener = table.iter().find(|e| e.path == "kv.Listener").unwrap(); - assert_eq!( - listener.hash, - fnv1a64(b"callback Listener {on_put(entry: Entry) -> string throws}") - ); } - fn hash_of(model: &Model, path: &str) -> u64 { - let root = model.roots().next().unwrap(); - entries(model, root) - .into_iter() - .find(|e| e.path == path) - .unwrap() - .hash + #[test] + fn canonical_signatures() { + let model = model(KV); + let cases = [ + ("kv.Store.get", "method get(string) -> Entry throws KvError"), + ( + "kv.Store.open", + "constructor open(string) -> Store throws KvError", + ), + ("kv.Store.sizes", "method sizes(u32?) -> [u64] throws any"), + ("kv.Store", "interface Store"), + ("kv.version", "function version() -> string"), + ( + "kv.fetch", + "function fetch(string) -> i64? async cancellable", + ), + ("kv.watch", "function watch(Listener?) -> iter<[i32]>"), + ("kv.Entry", "record Entry {string, bytes}"), + ("kv.stats.Kind", "enum Kind {Hot = 0, Cold = 1}"), + ("kv.stats.Shape", "enum Shape {Dot = 0, Circle = 1 {f64}}"), + ("kv.Listener", "callback Listener"), + ( + "kv.Listener.on_put", + "callback_method #0 on_put(Entry) -> string throws KvError", + ), + ( + "kv.Listener.on_clear", + "callback_method #1 on_clear() -> void", + ), + ("kv.KvError", "errors KvError"), + ("kv.KvError.NotFound", "code NotFound = 1 {string}"), + ("kv.KvError.Full", "code Full = 2"), + ]; + for (path, expected) in cases { + assert_eq!(signature(&model, path), expected, "{path}"); + } } #[test] - fn prose_and_sibling_order_are_excluded() { + fn names_prose_and_sibling_order_are_excluded() { let a = model(KV); let edited = KV .replace("doc: The version.", "doc: Changed prose.") - .replace("message: missing", "message: gone"); + .replace("message: missing", "message: gone") + .replace("{ name: key, type: string }]", "{ name: k, type: string }]") + .replace("name: value, type: bytes", "name: data, type: bytes"); let b = model(&edited); - assert_eq!( - entries(&a, a.roots().next().unwrap()), - entries(&b, b.roots().next().unwrap()) - ); + assert_eq!(table(&a), table(&b)); // Swapping two variants changes the enum (its own order is part of // the signature) but not its neighbors. let swapped = KV.replace( @@ -321,20 +369,82 @@ modules: "[{ name: Cold, value: 1 }, { name: Hot, value: 0 }]", ); let c = model(&swapped); - assert_ne!(hash_of(&a, "kv.stats.Kind"), hash_of(&c, "kv.stats.Kind")); - assert_eq!(hash_of(&a, "kv.Entry"), hash_of(&c, "kv.Entry")); + assert_ne!( + signature(&a, "kv.stats.Kind"), + signature(&c, "kv.stats.Kind") + ); + assert_eq!(signature(&a, "kv.Entry"), signature(&c, "kv.Entry")); + } + + #[test] + fn domains_and_callbacks_are_open() { + let a = model(KV); + let grown = model( + &KV.replace( + "- { name: Full, code: 2, message: full }", + "- { name: Full, code: 2, message: full }\n - { name: Busy, code: 3, message: busy }", + ) + .replace( + "- { name: on_clear, params: [] }", + "- { name: on_clear, params: [] }\n - { name: on_close, params: [] }", + ), + ); + // Every entry the old binding has is unchanged in the grown table. + let grown_table = table(&grown); + for e in table(&a) { + assert!(grown_table.contains(&e), "{} changed", e.path); + } + assert_eq!(grown_table.len(), table(&a).len() + 2); + } + + #[test] + fn moving_a_callback_method_changes_its_slot() { + // A vtable is laid out in declaration order, so swapping two + // methods (even with identical signatures) or inserting one before + // another must fail the check for every method whose slot moved. + let a = model(KV); + let swapped = model(&KV.replace( + "- { name: on_put, params: [{ name: entry, type: Entry }], return: string, throws: KvError }\n - { name: on_clear, params: [] }", + "- { name: on_clear, params: [] }\n - { name: on_put, params: [{ name: entry, type: Entry }], return: string, throws: KvError }", + )); + for path in ["kv.Listener.on_put", "kv.Listener.on_clear"] { + assert_ne!(signature(&a, path), signature(&swapped, path), "{path}"); + } + let inserted = model(&KV.replace( + "- { name: on_clear, params: [] }", + "- { name: on_open, params: [] }\n - { name: on_clear, params: [] }", + )); + assert_eq!( + signature(&a, "kv.Listener.on_put"), + signature(&inserted, "kv.Listener.on_put") + ); + assert_ne!( + signature(&a, "kv.Listener.on_clear"), + signature(&inserted, "kv.Listener.on_clear") + ); } #[test] fn abi_changes_change_only_their_entry() { let a = model(KV); let b = model(&KV.replace( - "return: \"bytes?\", throws: true", - "return: bytes, throws: true", + "return: Entry, throws: KvError", + "return: \"Entry?\", throws: KvError", )); - assert_ne!(hash_of(&a, "kv.Store.get"), hash_of(&b, "kv.Store.get")); - assert_eq!(hash_of(&a, "kv.Store.open"), hash_of(&b, "kv.Store.open")); - assert_eq!(hash_of(&a, "kv.Store"), hash_of(&b, "kv.Store")); + assert_ne!(signature(&a, "kv.Store.get"), signature(&b, "kv.Store.get")); + assert_eq!( + signature(&a, "kv.Store.open"), + signature(&b, "kv.Store.open") + ); + assert_eq!(signature(&a, "kv.Store"), signature(&b, "kv.Store")); + let c = model(&KV.replace( + "return: Entry, throws: KvError", + "return: Entry, throws: any", + )); + assert_eq!( + signature(&c, "kv.Store.get"), + "method get(string) -> Entry throws any" + ); } #[test] diff --git a/crates/weaveffi-model/src/errors.rs b/crates/weaveffi-model/src/errors.rs index 022c16e1..bcc7a4ed 100644 --- a/crates/weaveffi-model/src/errors.rs +++ b/crates/weaveffi-model/src/errors.rs @@ -5,42 +5,58 @@ //! [`ErrorBinding`](crate::model::ErrorBinding). This module holds only the //! idiomatic naming rules every backend applies to those names, centralized so //! no target drifts into `KEY_NOT_FOUNDError` (raw SCREAMING_SNAKE with a -//! naive `Error` suffix) while another emits `keyNotFound`. +//! naive `Error` suffix) or `KitchenErrorsError` while another emits +//! `keyNotFound`. //! //! Backends pick the suffix that matches their ecosystem (`Error` or //! `Exception`) and case-convert each code's name through the helpers below. +//! Every generator names domain and code types through [`type_name`] (or +//! [`exception_type_name`]), never by hand. use heck::ToUpperCamelCase; +/// The suffixes [`type_name`] strips before appending its own, longest +/// first, so a raw name never ends up with two. +const ERROR_SUFFIXES: &[&str] = &["Exceptions", "Exception", "Errors", "Error"]; + /// PascalCase form of a raw error code name, with no suffix. /// `KEY_NOT_FOUND` -> `KeyNotFound`. Use for languages whose error variants -/// are nested types/cases (Kotlin sealed subclasses, etc.) rather than -/// standalone `*Error` classes. +/// are nested types or cases (Kotlin sealed subclasses, Swift enum cases) +/// rather than standalone `*Error` classes. +#[must_use] pub fn pascal(raw: &str) -> String { raw.to_upper_camel_case() } -/// PascalCase + exactly one `suffix`, avoiding doubled or SCREAMING suffixes. -/// `("KEY_NOT_FOUND", "Error")` -> `KeyNotFoundError`; -/// `("AlreadyError", "Error")` -> `AlreadyError`. +/// PascalCase plus exactly one `suffix`, never doubled. +/// +/// The raw name is converted to PascalCase, then a trailing `Error`, +/// `Errors`, `Exception`, or `Exceptions` is reduced to its stem before +/// `suffix` is appended: +/// +/// * `("KEY_NOT_FOUND", "Error")` -> `KeyNotFoundError`; +/// * `("KvError", "Error")` -> `KvError`; +/// * `("KitchenErrors", "Error")` -> `KitchenError`; +/// * `("Failure", "Error")` -> `FailureError`; +/// * `("KvError", "Exception")` -> `KvException`; +/// * a bare `Error` is just `suffix`. +#[must_use] pub fn type_name(raw: &str, suffix: &str) -> String { let pascal = raw.to_upper_camel_case(); - if pascal.ends_with(suffix) { - pascal - } else { - format!("{pascal}{suffix}") - } + let stem = ERROR_SUFFIXES + .iter() + .find_map(|s| pascal.strip_suffix(s)) + .unwrap_or(&pascal); + format!("{stem}{suffix}") } -/// Exception-branded type name for an error domain, for targets whose -/// idiomatic errors are exceptions rather than `*Error` types. -/// A trailing `Error` stem is replaced instead of stacked: -/// `KvError` -> `KvException`; `Failure` -> `FailureException`; a bare -/// `Error` -> `Exception`. +/// Exception-branded type name for an error domain or code, for targets +/// whose idiomatic errors are exceptions rather than `*Error` types: +/// [`type_name`] with the `Exception` suffix (`KvError` -> `KvException`, +/// `Failure` -> `FailureException`, a bare `Error` -> `Exception`). +#[must_use] pub fn exception_type_name(raw: &str) -> String { - let pascal = raw.to_upper_camel_case(); - let stem = pascal.strip_suffix("Error").unwrap_or(&pascal); - type_name(stem, "Exception") + type_name(raw, "Exception") } #[cfg(test)] @@ -56,6 +72,17 @@ mod tests { ); assert_eq!(type_name("AlreadyError", "Error"), "AlreadyError"); assert_eq!(type_name("invalid_input", "Error"), "InvalidInputError"); + assert_eq!(type_name("KvError", "Error"), "KvError"); + assert_eq!(type_name("Failure", "Error"), "FailureError"); + assert_eq!(type_name("Error", "Error"), "Error"); + } + + #[test] + fn type_name_reduces_plural_suffixes() { + assert_eq!(type_name("KitchenErrors", "Error"), "KitchenError"); + assert_eq!(type_name("kitchen_errors", "Error"), "KitchenError"); + assert_eq!(type_name("ContactErrors", "Exception"), "ContactException"); + assert_eq!(type_name("IoExceptions", "Error"), "IoError"); } #[test] @@ -65,6 +92,7 @@ mod tests { assert_eq!(exception_type_name("Failure"), "FailureException"); assert_eq!(exception_type_name("KvException"), "KvException"); assert_eq!(exception_type_name("Error"), "Exception"); + assert_eq!(exception_type_name("KitchenErrors"), "KitchenException"); } #[test] diff --git a/crates/weaveffi-model/src/ir.rs b/crates/weaveffi-model/src/ir.rs index f3fc9aaf..6cf7dd3e 100644 --- a/crates/weaveffi-model/src/ir.rs +++ b/crates/weaveffi-model/src/ir.rs @@ -3,8 +3,8 @@ //! //! This is the *document* model: [`Api`] is the root and owns a forest of //! [`Module`]s, each grouping [`Function`]s, [`InterfaceDef`]s, -//! [`CallbackInterfaceDef`]s, [`StructDef`]s, [`EnumDef`]s, and an optional -//! [`ErrorDomain`]. Types are referenced throughout by [`TypeRef`], which +//! [`CallbackInterfaceDef`]s, [`StructDef`]s, [`EnumDef`]s, and +//! [`ErrorDomain`]s. Types are referenced throughout by [`TypeRef`], which //! (de)serializes as a compact string (`i32`, `[string]`, `{string:i32}`, //! `Contact?`, and so on) rather than as a tagged object. //! @@ -39,7 +39,7 @@ use crate::ty::Prim; /// /// See [`docs/src/stability.md`](https://github.com/weavefoundry/weaveffi/blob/main/docs/src/stability.md) /// for the full schema policy and the surfaces covered by SemVer. -pub const CURRENT_SCHEMA_VERSION: &str = "0.11.0"; +pub const CURRENT_SCHEMA_VERSION: &str = "0.12.0"; /// Every IR schema version the current tools accept. /// @@ -50,7 +50,7 @@ pub const SUPPORTED_VERSIONS: &[&str] = &[CURRENT_SCHEMA_VERSION]; /// `skip_serializing_if` predicate for `bool` fields that default to `false`. /// Keeps the canonical IDL emitted by `weaveffi extract` minimal by omitting -/// flags the user never set (e.g. `async: false`, `throws: false`). +/// flags the user never set (for example `async: false`). #[allow(clippy::trivially_copy_pass_by_ref)] fn is_false(b: &bool) -> bool { !*b @@ -58,7 +58,7 @@ fn is_false(b: &bool) -> bool { /// Top-level WeaveFFI API definition: the root of a parsed IDL document. /// -/// This is the value an entire `.yml`, `.json`, or `.toml` IDL file +/// This is the value an entire `.yml` or `.json` IDL file /// deserializes into (see [`crate::parse`]) and the single input the validator /// consumes. It pairs the schema version with the module forest. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -69,7 +69,7 @@ fn is_false(b: &bool) -> bool { schemars(description = "Top-level WeaveFFI API definition.") )] pub struct Api { - /// IR schema version this document targets (for example `0.11.0`). + /// IR schema version this document targets (for example `0.12.0`). /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`]. pub version: String, /// Top-level modules that make up the API surface. Each is an independent @@ -124,10 +124,46 @@ impl Api { } modules(&self.modules, f); } + + /// Every type name the document references but doesn't declare: the + /// names a written type uses (`Named` references, at any depth) that no + /// record, enum, interface, callback interface, or error domain anywhere + /// in the document provides. + /// + /// The `#[weaveffi::module]` macro validates one module tree at a time, + /// so a record from a sibling tree is undeclared there; it passes these + /// names to [`Options::foreign`](crate::validate::Options::foreign) + /// explicitly. + #[must_use] + pub fn undeclared_type_names(&self) -> std::collections::BTreeSet { + fn declared<'a>(ms: &'a [Module], out: &mut std::collections::BTreeSet<&'a str>) { + for m in ms { + out.extend(m.structs.iter().map(|d| d.name.as_str())); + out.extend(m.enums.iter().map(|d| d.name.as_str())); + out.extend(m.interfaces.iter().map(|d| d.name.as_str())); + out.extend(m.callback_interfaces.iter().map(|d| d.name.as_str())); + out.extend(m.errors.iter().map(|d| d.name.as_str())); + declared(&m.modules, out); + } + } + let mut names = std::collections::BTreeSet::new(); + declared(&self.modules, &mut names); + let mut out = std::collections::BTreeSet::new(); + self.for_each_type_ref(&mut |ty| { + ty.walk(&mut |t| { + if let TypeRef::Named(n) = t { + if !names.contains(n.as_str()) { + out.insert(n.clone()); + } + } + }); + }); + out + } } /// A module: a named namespace grouping related functions, types, callback -/// interfaces, and an error domain. +/// interfaces, and error domains. /// /// Modules are the IDL's unit of organization and map onto each target /// language's natural grouping construct (a namespace, a submodule, a symbol @@ -168,10 +204,11 @@ pub struct Module { /// Enum types, C-style or algebraic, declared in this module. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub enums: Vec, - /// Optional error domain: the named codes this module's fallible functions - /// report. `None` when the module declares no errors. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub errors: Option, + /// The error domains this module declares: named sets of codes that + /// fallible callables anywhere in the API report by naming the domain in + /// their `throws`. Domain names share the global type namespace. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub errors: Vec, /// Nested submodules, forming a tree that mirrors a package hierarchy. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub modules: Vec, @@ -204,13 +241,14 @@ pub struct Function { /// comments. `None` when undocumented. #[serde(default, skip_serializing_if = "Option::is_none")] pub doc: Option, - /// Whether the function can fail with a domain error. A throwing function - /// surfaces as `throws`/`raises` in the idiomatic bindings, reporting the - /// owning module's error domain; a non-throwing function has a plain - /// signature and treats any error as a producer bug (a trap, not a typed - /// error). Defaults to `false`. - #[serde(default, skip_serializing_if = "is_false")] - pub throws: bool, + /// How the function can fail. Absent for a function that can't, which + /// treats any reported error as a producer bug (a trap, not a typed + /// error); an error domain's name (`throws: KvError`) for one that + /// reports that domain's codes; or `any` (`throws: any`) for one that + /// fails with an untyped error, a message under the generic runtime code + /// `-1`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub throws: Option, /// Whether the function is asynchronous, lowering to a completion-callback /// form rather than a blocking call. Serialized under the IDL key `async`. #[serde(default, rename = "async", skip_serializing_if = "is_false")] @@ -286,9 +324,9 @@ pub struct InterfaceDef { /// /// Methods are synchronous (never `async` or `cancellable`) and return /// nothing or any type except an iterator or a callback interface; a string, -/// bytes, or buffer return is a run the consumer allocates and the producer -/// adopts. A method may declare `throws` when an error domain is in scope, -/// letting the consumer report that domain's codes. Parameters may use any +/// bytes, slice, or buffer return is a run the consumer allocates and the +/// producer adopts. A method may declare `throws` (a domain, letting the +/// consumer report that domain's codes, or `any`). Parameters may use any /// type other than a callback interface or an iterator. A callback interface /// may itself be passed, bare or optional (`Listener?`), as a parameter of a /// function or interface member. @@ -332,6 +370,116 @@ pub struct Param { pub doc: Option, } +/// What a fallible callable reports: the IDL's `throws` value. +/// +/// Serialized as a string: the name of an error domain (`throws: KvError`) +/// or the keyword `any` (`throws: any`). Domain names are global, so the +/// named domain may be declared in any module. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Throws { + /// The callable reports the codes of the named error domain, each a + /// positive code with an optional payload, plus the runtime's negative + /// codes. + Domain(String), + /// The callable fails with an untyped error: the generic runtime code + /// `-1` and a message, never a positive code. + Any, +} + +/// The `throws` keyword for an untyped error. +const THROWS_ANY: &str = "any"; + +impl Throws { + /// The error domain's name for [`Throws::Domain`], or `None` for + /// [`Throws::Any`]. + #[must_use] + pub fn domain(&self) -> Option<&str> { + match self { + Throws::Domain(name) => Some(name), + Throws::Any => None, + } + } +} + +impl std::fmt::Display for Throws { + /// Renders the IDL spelling: the domain name or `any`. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Throws::Domain(name) => f.write_str(name), + Throws::Any => f.write_str(THROWS_ANY), + } + } +} + +impl Serialize for Throws { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for Throws { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl serde::de::Visitor<'_> for Visitor { + type Value = Throws; + + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("an error domain name or `any`") + } + + fn visit_str(self, s: &str) -> Result { + Ok(if s == THROWS_ANY { + Throws::Any + } else { + Throws::Domain(s.to_string()) + }) + } + + fn visit_bool(self, _: bool) -> Result { + Err(E::custom( + "`throws` names the error domain the callable reports (`throws: KvError`) \ + or is `any` for an untyped error; boolean `throws` was removed in schema 0.12", + )) + } + } + deserializer.deserialize_any(Visitor) + } +} + +/// Manual `JsonSchema` impl because [`Throws`] (de)serializes as a string: +/// an error domain's name or `any`. +#[cfg(feature = "idl")] +impl JsonSchema for Throws { + fn schema_name() -> String { + "Throws".to_string() + } + + fn schema_id() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(concat!(module_path!(), "::Throws")) + } + + fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let mut schema = schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + ..Default::default() + }; + let meta = schema.metadata(); + meta.title = Some("Throws".to_string()); + meta.description = Some( + "What a fallible callable reports: the name of an error domain declared in any \ + module, or `any` for an untyped error (code -1 and a message)." + .to_string(), + ); + schema.into() + } +} + /// A reference to a type in the IDL, exactly as written. /// /// Every user-defined type (record, enum, interface, or callback interface) is @@ -630,14 +778,20 @@ pub struct StructField { pub doc: Option, } -/// A module's error domain: the named set of error codes its fallible functions -/// can report. +/// An error domain: a named, open set of error codes that fallible callables +/// report by naming the domain in their `throws`. +/// +/// A module may declare several. The domain's name is a type name (unique +/// across the API); its code names are unique across the API too, and its +/// code values are positive and unique within the domain. Domains are open: +/// adding a code never breaks a deployed binding, which maps a positive code +/// it doesn't know to the domain's base error type. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct ErrorDomain { /// Error domain name, used to name the generated error type (for example - /// `ContactErrors`). + /// `ContactError`). Callables name it in their `throws`. pub name: String, /// The error codes that belong to this domain. pub codes: Vec, @@ -787,7 +941,7 @@ mod tests { #[test] fn document_round_trips_through_yaml_and_json() { let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: contacts doc: Address book @@ -806,7 +960,7 @@ modules: interfaces: - name: Book constructors: - - { name: open, params: [{ name: path, type: string }], throws: true } + - { name: open, params: [{ name: path, type: string }], throws: BookError } methods: - { name: find, params: [{ name: q, type: string }], return: "Contact?" } - { name: watch, params: [{ name: listener, type: ChangeListener }] } @@ -814,11 +968,14 @@ modules: - name: ChangeListener methods: - { name: on_change, params: [{ name: id, type: i64 }] } - - { name: should_continue, params: [], return: bool } + - { name: should_continue, params: [], return: bool, throws: any } errors: - name: BookError - codes: - - { name: NotFound, code: 1, message: missing, fields: [{ name: id, type: i64 }] } + - name: BookError + codes: + - { name: NotFound, code: 1, message: missing, fields: [{ name: id, type: i64 }] } + - name: IoError + codes: + - { name: Disk, code: 1, message: disk } functions: - name: count params: [] @@ -829,9 +986,9 @@ modules: modules: - name: inner functions: - - { name: ping, params: [], return: string } + - { name: ping, params: [], return: string, throws: any } "#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); + let api: Api = serde_saphyr::from_str(yaml).unwrap(); assert_eq!(api.version, CURRENT_SCHEMA_VERSION); let m = &api.modules[0]; assert_eq!(m.doc.as_deref(), Some("Address book")); @@ -843,19 +1000,58 @@ modules: "Contact".into() )))) ); + assert_eq!( + m.interfaces[0].constructors[0].throws, + Some(Throws::Domain("BookError".into())) + ); assert_eq!(m.callback_interfaces[0].methods.len(), 2); - assert_eq!(m.errors.as_ref().unwrap().codes[0].fields.len(), 1); + assert_eq!( + m.callback_interfaces[0].methods[1].throws, + Some(Throws::Any) + ); + assert_eq!(m.errors.len(), 2); + assert_eq!(m.errors[0].codes[0].fields.len(), 1); assert!(m.functions[0].r#async && m.functions[0].cancellable); + assert_eq!(m.functions[0].throws, None); assert_eq!(m.modules[0].functions[0].name, "ping"); let json = serde_json::to_string(&api).unwrap(); + assert!(json.contains(r#""throws":"BookError""#), "{json}"); + assert!(json.contains(r#""throws":"any""#), "{json}"); let back: Api = serde_json::from_str(&json).unwrap(); assert_eq!(back, api); - let yaml2 = serde_yaml::to_string(&api).unwrap(); - let back2: Api = serde_yaml::from_str(&yaml2).unwrap(); + let yaml2 = serde_saphyr::to_string(&api).unwrap(); + let back2: Api = serde_saphyr::from_str(&yaml2).unwrap(); assert_eq!(back2, api); } + #[test] + fn boolean_throws_is_rejected_with_a_pointer_to_the_new_syntax() { + let yaml = r#" +version: "0.12.0" +modules: + - name: m + functions: [{ name: f, throws: true }] +"#; + let err = serde_saphyr::from_str::(yaml).unwrap_err(); + assert!(err.to_string().contains("throws: KvError"), "{err}"); + } + + #[test] + fn undeclared_type_names_are_the_foreign_references() { + let yaml = r#" +version: "0.12.0" +modules: + - name: m + structs: [{ name: Local, fields: [{ name: other, type: "[Remote]" }] }] + functions: + - { name: f, params: [{ name: x, type: "{string:Elsewhere?}" }], return: Local } +"#; + let api: Api = serde_saphyr::from_str(yaml).unwrap(); + let names: Vec = api.undeclared_type_names().into_iter().collect(); + assert_eq!(names, ["Elsewhere", "Remote"]); + } + #[test] fn serialization_omits_defaulted_fields() { let api = Api { @@ -868,7 +1064,7 @@ modules: params: vec![], returns: None, doc: None, - throws: false, + throws: None, r#async: false, cancellable: false, deprecated: None, @@ -877,12 +1073,14 @@ modules: callback_interfaces: vec![], structs: vec![], enums: vec![], - errors: None, + errors: vec![], modules: vec![], }], }; - let yaml = serde_yaml::to_string(&api).unwrap(); - for noise in ["null", "[]", "false", "async", "throws", "doc", "params"] { + let yaml = serde_saphyr::to_string(&api).unwrap(); + for noise in [ + "null", "[]", "false", "async", "throws", "doc", "params", "errors", + ] { assert!( !yaml.contains(noise), "{noise} leaked into canonical form:\n{yaml}" @@ -900,5 +1098,6 @@ modules: assert!(json["properties"].get("package").is_none()); assert!(json["properties"].get("generators").is_none()); assert!(json["definitions"].get("CallbackInterfaceDef").is_some()); + assert_eq!(json["definitions"]["Throws"]["type"], "string"); } } diff --git a/crates/weaveffi-model/src/lib.rs b/crates/weaveffi-model/src/lib.rs index b753c952..a56e6fda 100644 --- a/crates/weaveffi-model/src/lib.rs +++ b/crates/weaveffi-model/src/lib.rs @@ -2,17 +2,15 @@ //! generation. //! //! * [`ir`] holds the in-memory IR types that an IDL document or annotated -//! Rust source lowers to, and [`parse`] reads the IDL text formats (YAML, -//! JSON, and TOML). -//! * [`rust`] extracts the IR from annotated Rust source for the -//! proc-macros. +//! Rust source lowers to, and [`parse`] reads the IDL text formats (YAML +//! and JSON). //! * [`validate`] checks a document against the library's //! [`Identity`](pkg::Identity) and builds the one [`Model`](model::Model) //! every generator consumes. //! * [`ty`], [`model`], [`abi`], [`plan`], [`errors`], and [`pkg`] hold the //! resolved types and type index, the model with every C symbol and ABI -//! signature, the C ABI lowering, the marshalling plan, the error naming -//! policy, and the package identity. +//! signature, the C ABI lowering, the passing contracts the model stores +//! on every binding, the error naming policy, and the package identity. //! * [`contract`] computes each top-level module's contract table, one //! fingerprint per declaration, so a stale binding refuses to load and //! names what changed. @@ -23,8 +21,9 @@ //! //! * `idl` (default): the IDL text formats, JSON Schema derivation for the IR //! types, and fancy miette diagnostics. Without it the crate still provides -//! the IR types, Rust extraction, validation, and the model, which is all -//! the proc-macros need. +//! the IR types, validation (including the scoped validation the +//! proc-macro runs on one module tree), and the model, which is all the +//! proc-macros need. #![deny(missing_docs)] #![warn(clippy::missing_errors_doc)] #![warn(clippy::missing_panics_doc)] @@ -43,6 +42,5 @@ pub mod model; pub mod parse; pub mod pkg; pub mod plan; -pub mod rust; pub mod ty; pub mod validate; diff --git a/crates/weaveffi-model/src/meta.rs b/crates/weaveffi-model/src/meta.rs index 3cf945b9..f1fc9a0f 100644 --- a/crates/weaveffi-model/src/meta.rs +++ b/crates/weaveffi-model/src/meta.rs @@ -17,8 +17,8 @@ //! self-delimiting, so a section holding several frames back to back //! [decodes](decode) into all of them. //! -//! One frame describes one declaration: a module (its name and doc), its -//! error domain, a free function, an interface (without its members), one +//! One frame describes one declaration: a module (its name and doc), one of +//! its error domains, a free function, an interface (without its members), one //! interface member, a record, an enum, or a callback interface. Each frame //! records the path of the module it belongs to and its position among its //! siblings of the same kind, so [`assemble`] restores declaration order. @@ -67,7 +67,7 @@ pub enum Item { /// A module's own name and doc; its declarations have frames of their /// own. Module(ModuleHeader), - /// A module's error domain. + /// One of a module's error domains. Error(ErrorDomain), /// A free function. Function(Function), @@ -314,8 +314,8 @@ pub fn module_frames(module: &Module, parent: &[String], index: u32, prefix: &st doc: module.doc.clone(), }), )]; - if let Some(errors) = &module.errors { - out.push(frame(&path, 0, Item::Error(errors.clone()))); + for (i, e) in module.errors.iter().enumerate() { + out.push(frame(&path, i, Item::Error(e.clone()))); } for (i, f) in module.functions.iter().enumerate() { out.push(frame(&path, i, Item::Function(f.clone()))); @@ -388,7 +388,7 @@ pub fn frames(api: &Api, prefix: &str) -> Vec { #[derive(Default)] struct Node { header: Option<(u32, ModuleHeader)>, - errors: Option, + errors: Vec<(u32, ErrorDomain)>, functions: Vec<(u32, Function)>, interfaces: Vec<(u32, InterfaceDef)>, members: Vec<(u32, Member)>, @@ -439,9 +439,11 @@ pub fn assemble(frames: &[Frame], prefix: &str) -> Result { full.push(h.name.clone()); nodes.entry(full).or_default().header = Some((i, h.clone())); } - Item::Error(e) => { - nodes.entry(frame.module.clone()).or_default().errors = Some(e.clone()); - } + Item::Error(e) => nodes + .entry(frame.module.clone()) + .or_default() + .errors + .push((i, e.clone())), Item::Function(f) => nodes .entry(frame.module.clone()) .or_default() @@ -515,7 +517,7 @@ fn orphan_of(path: &[String], node: &Node) -> (String, String) { .or_else(|| node.structs.first().map(|(_, d)| d.name.clone())) .or_else(|| node.enums.first().map(|(_, d)| d.name.clone())) .or_else(|| node.callbacks.first().map(|(_, d)| d.name.clone())) - .or_else(|| node.errors.as_ref().map(|e| e.name.clone())) + .or_else(|| node.errors.first().map(|(_, e)| e.name.clone())) .unwrap_or_default(); (format!("{module}.{name}"), module) } @@ -571,7 +573,7 @@ fn build( callback_interfaces: ordered(node.callbacks, |d| &d.name), structs: ordered(node.structs, |d| &d.name), enums: ordered(node.enums, |d| &d.name), - errors: node.errors, + errors: ordered(node.errors, |d| &d.name), modules: Vec::new(), }; module.modules = build(nodes, &path)?; @@ -586,22 +588,26 @@ mod tests { use crate::parse::parse_api_str; const API: &str = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: kv doc: The store. errors: - name: KvError - codes: - - { name: Missing, code: 1, message: missing, fields: [{ name: key, type: string }] } + - name: KvError + codes: + - { name: Missing, code: 1, message: missing, fields: [{ name: key, type: string }] } + - name: IoError + codes: + - { name: Disk, code: 1, message: disk } functions: - - { name: open_store, params: [{ name: path, type: string }], return: Store, throws: true } + - { name: open_store, params: [{ name: path, type: string }], return: Store, throws: KvError } + - { name: sync, params: [{ name: ids, type: "[u64]" }], return: "i32?", throws: any } - { name: version, return: u32 } interfaces: - name: Store constructors: - { name: new } - - { name: open, params: [{ name: path, type: string }], throws: true } + - { name: open, params: [{ name: path, type: string }], throws: IoError } methods: - { name: put, params: [{ name: key, type: string }] } - { name: count, return: u32 } diff --git a/crates/weaveffi-model/src/model/build.rs b/crates/weaveffi-model/src/model/build.rs index b7297a77..0c332dea 100644 --- a/crates/weaveffi-model/src/model/build.rs +++ b/crates/weaveffi-model/src/model/build.rs @@ -1,28 +1,37 @@ //! Building the [`Model`]: index every type declaration, resolve each written -//! [`TypeRef`] to a [`Ty`], and lower every declaration to its C symbols and -//! ABI signatures in one walk. +//! [`TypeRef`] to its positional type, and lower every declaration to its C +//! symbols, ABI signatures, and passing contracts in one walk. +//! +//! The build runs only on a document every validation rule accepted, so +//! each step that can fail (an undeclared name, a callback interface or an +//! iterator in a value position) is one a rule already reported. The steps +//! still return the matching [`ValidationError`] instead of panicking, so a +//! missing rule surfaces as a diagnostic, never as a crash. use heck::ToUpperCamelCase; use super::{ - AbiFn, AsyncBinding, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, EnumBinding, - EnumVariantBinding, ErrorBinding, ErrorCodeBinding, FieldBinding, FnBinding, InterfaceBinding, - IteratorBinding, Model, ModuleBinding, ParamBinding, StructBinding, + c_tag, member_symbol, AbiFn, AsyncBinding, CallShape, CallbackInterfaceBinding, + CallbackMethodBinding, CallbackParamBinding, EnumBinding, EnumVariantBinding, ErrorBinding, + ErrorCodeBinding, FieldBinding, FnBinding, InterfaceBinding, IteratorBinding, Model, + ModuleBinding, ParamBinding, StructBinding, }; +use crate::abi::lower::{Lower, ParamSite, Undeclared}; use crate::abi::{ - callback_result_params, cancel_token_param, context_param, ctx_param, error_out_param, - lower_callback_return, lower_param, lower_return, AbiParam, CType, ConstPos, + cancel_token_param, context_param, ctx_param, error_out_param, AbiParam, CType, ConstPos, }; use crate::ir::{ - Api, CallbackInterfaceDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, Param, - StructDef, StructField, TypeRef, + Api, CallbackInterfaceDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, StructField, + Throws, TypeRef, }; use crate::pkg::Identity; -use crate::ty::{Family, Ty, TypeDecl, TypeIndex, TypeKind}; +use crate::plan::{ErrorStrategy, RetPass}; +use crate::ty::{ParamTy, RetTy, Ty, TypeDecl, TypeIndex, TypeKind}; +use crate::validate::ValidationError; -/// Index every type declaration in `api`, numbering modules in the -/// depth-first pre-order [`Model::modules`] uses. The first declaration of a -/// name wins; validation reports any later one. +/// Index every type and error-domain declaration in `api`, numbering +/// modules in the depth-first pre-order [`Model::modules`] uses. The first +/// declaration of a name wins; validation reports any later one. pub(crate) fn index(api: &Api) -> TypeIndex { fn walk(modules: &[Module], parent: &[&str], types: &mut TypeIndex) { for m in modules { @@ -56,6 +65,9 @@ pub(crate) fn index(api: &Api) -> TypeIndex { for (i, c) in m.callback_interfaces.iter().enumerate() { declare(&c.name, TypeKind::CallbackInterface, i); } + for (i, d) in m.errors.iter().enumerate() { + declare(&d.name, TypeKind::ErrorDomain, i); + } walk(&m.modules, &segments, types); } } @@ -64,289 +76,341 @@ pub(crate) fn index(api: &Api) -> TypeIndex { types } -/// Map a written type reference to its resolved type. A name no declaration -/// provides resolves to a [`Ty::Record`] (see [`Model::assume_valid`]). -pub(crate) fn resolve(types: &TypeIndex, ty: &TypeRef) -> Ty { - match ty { - TypeRef::Prim(p) => Ty::Prim(*p), - TypeRef::Named(name) => { - let name = name.clone(); - match types.kind(&name) { - Some(TypeKind::Enum) => Ty::Enum(name), - Some(TypeKind::RichEnum) => Ty::RichEnum(name), - Some(TypeKind::Interface) => Ty::Interface(name), - Some(TypeKind::CallbackInterface) => Ty::CallbackInterface(name), - Some(TypeKind::Record) | None => Ty::Record(name), - } - } - TypeRef::Optional(inner) => Ty::Optional(Box::new(resolve(types, inner))), - TypeRef::List(inner) => Ty::List(Box::new(resolve(types, inner))), - TypeRef::Map(k, v) => Ty::Map(Box::new(resolve(types, k)), Box::new(resolve(types, v))), - TypeRef::Iterator(inner) => Ty::Iterator(Box::new(resolve(types, inner))), - } -} - -/// Build the model of `api` under `identity`, indexing its declarations -/// first. -pub(crate) fn build(api: &Api, identity: Identity) -> Model { - let types = index(api); - build_indexed(api, identity, types) -} - -/// Build the model of `api` from an index already built by [`index`]. -pub(crate) fn build_indexed(api: &Api, identity: Identity, types: TypeIndex) -> Model { +/// Build the model of `api` under `identity` from an index built by +/// [`index`]. +/// +/// # Errors +/// +/// Returns the violation a validation rule should already have reported: +/// an undeclared name, or a callback interface, error domain, or iterator +/// where a value type belongs. +pub(crate) fn build( + api: &Api, + identity: Identity, + types: TypeIndex, +) -> Result { let mut modules = Vec::new(); - let lowerer = Lowerer { - types: &types, - prefix: &identity.prefix, - }; - for m in &api.modules { - lowerer.module(m, &[], &mut modules); + { + let builder = Builder { + lower: Lower { + types: &types, + prefix: &identity.prefix, + }, + }; + for m in &api.modules { + builder.module(m, None, &[], &mut modules)?; + } } - Model { + Ok(Model { version: api.version.clone(), modules, identity, types, + }) +} + +impl From for ValidationError { + fn from(Undeclared(name): Undeclared) -> Self { + ValidationError::UnknownTypeRef { name } } } -/// The per-build lowering context: the type index and the symbol prefix. -struct Lowerer<'a> { - types: &'a TypeIndex, - prefix: &'a str, +/// The error strategy a written `throws` declares. +fn error_strategy(throws: Option<&Throws>) -> ErrorStrategy { + match throws { + None => ErrorStrategy::Trap, + Some(Throws::Domain(name)) => ErrorStrategy::Domain(name.clone()), + Some(Throws::Any) => ErrorStrategy::Untyped, + } } -/// A fully assembled C signature: the ordered parameter slots and the C -/// return type. -struct AbiSig { - params: Vec, - ret: CType, +/// The per-build context: the lowering (type index and symbol prefix). +struct Builder<'a> { + lower: Lower<'a>, } -impl Lowerer<'_> { - fn ty(&self, ty: &TypeRef) -> Ty { - resolve(self.types, ty) +impl Builder<'_> { + fn prefix(&self) -> &str { + self.lower.prefix + } + + /// Resolve a written type in a value position. + fn value(&self, ty: &TypeRef) -> Result { + Ok(match ty { + TypeRef::Prim(p) => Ty::Prim(*p), + TypeRef::Named(name) => { + let name = name.clone(); + match self.lower.types.kind(&name) { + Some(TypeKind::Record) => Ty::Record(name), + Some(TypeKind::Enum) => Ty::Enum(name), + Some(TypeKind::RichEnum) => Ty::RichEnum(name), + Some(TypeKind::Interface) => Ty::Interface(name), + Some(TypeKind::CallbackInterface) => { + return Err(ValidationError::CallbackInterfaceInInvalidPosition { + name, + location: "a value position".to_string(), + }) + } + Some(TypeKind::ErrorDomain) => { + return Err(ValidationError::ErrorDomainAsType { name }) + } + None if self.lower.types.is_foreign(&name) => Ty::Record(name), + None => return Err(ValidationError::UnknownTypeRef { name }), + } + } + TypeRef::Optional(inner) => Ty::Optional(Box::new(self.value(inner)?)), + TypeRef::List(inner) => Ty::List(Box::new(self.value(inner)?)), + TypeRef::Map(k, v) => Ty::Map(Box::new(self.value(k)?), Box::new(self.value(v)?)), + TypeRef::Iterator(_) => { + return Err(ValidationError::IteratorInInvalidPosition { + location: "a value position".to_string(), + }) + } + }) + } + + /// Resolve a written parameter type: a bare or optional callback + /// interface, or a value. + fn param_ty(&self, ty: &TypeRef) -> Result { + let (top, nullable) = match ty { + TypeRef::Optional(inner) => (inner.as_ref(), true), + other => (other, false), + }; + if let TypeRef::Named(name) = top { + if self.lower.types.kind(name) == Some(TypeKind::CallbackInterface) { + return Ok(ParamTy::Callback { + name: name.clone(), + nullable, + }); + } + } + Ok(ParamTy::Value(self.value(ty)?)) + } + + /// Resolve a written return type: an iterator, or a value. + fn ret_ty(&self, ty: &TypeRef) -> Result { + Ok(match ty { + TypeRef::Iterator(elem) => RetTy::Iterator(self.value(elem)?), + other => RetTy::Value(self.value(other)?), + }) } /// Recursively lower `module` and its descendants into the flat `out` /// list, pre-order (parent before children) so symbol declarations - /// precede uses and positions match the [`TypeIndex`]. - fn module(&self, module: &Module, parent: &[String], out: &mut Vec) { - let mut segments = parent.to_vec(); + /// precede uses and positions match the [`TypeIndex`]. Returns the + /// module's position. + fn module( + &self, + module: &Module, + parent: Option, + parent_segments: &[String], + out: &mut Vec, + ) -> Result { + let mut segments = parent_segments.to_vec(); segments.push(module.name.clone()); let path = segments.join("_"); - let prefix = self.prefix; + let dot_path = segments.join("."); - let functions = module - .functions - .iter() - .map(|f| self.callable(f, &format!("{prefix}_{path}_{}", f.name), None)) - .collect(); - out.push(ModuleBinding { + let binding = ModuleBinding { + index: out.len(), name: module.name.clone(), - dot_path: segments.join("."), doc: module.doc.clone(), - errors: module.errors.as_ref().map(|d| self.error_domain(d, &path)), + parent, + children: Vec::new(), + errors: module + .errors + .iter() + .map(|d| self.error_domain(d, &path, &dot_path)) + .collect::>()?, enums: module .enums .iter() .map(|e| self.enum_def(e, &path)) - .collect(), - structs: module.structs.iter().map(|s| self.struct_def(s)).collect(), + .collect::>()?, + structs: module + .structs + .iter() + .map(|s| { + Ok(StructBinding { + name: s.name.clone(), + doc: s.doc.clone(), + deprecated: s.deprecated.clone(), + c_tag: c_tag(self.prefix(), &path, &s.name), + fields: self.fields(&s.fields)?, + }) + }) + .collect::>()?, interfaces: module .interfaces .iter() .map(|i| self.interface(i, &path)) - .collect(), + .collect::>()?, callback_interfaces: module .callback_interfaces .iter() .map(|c| self.callback_interface(c, &path)) - .collect(), - functions, + .collect::>()?, + functions: module + .functions + .iter() + .map(|f| self.callable(f, &path, None, None)) + .collect::>()?, segments: segments.clone(), path, - }); - + dot_path, + }; + let index = binding.index; + out.push(binding); for child in &module.modules { - self.module(child, &segments, out); + let child = self.module(child, Some(index), &segments, out)?; + out[index].children.push(child); } + Ok(index) } - fn error_domain(&self, domain: &ErrorDomain, path: &str) -> ErrorBinding { - let c_tag = format!("{}_{path}_{}", self.prefix, domain.name); - ErrorBinding { - name: domain.name.clone(), - type_name: crate::errors::type_name(&domain.name, "Error"), - owner_path: path.to_string(), - codes: domain - .codes - .iter() - .map(|c| ErrorCodeBinding { + fn error_domain( + &self, + domain: &ErrorDomain, + path: &str, + dot_path: &str, + ) -> Result { + let tag = c_tag(self.prefix(), path, &domain.name); + let codes = domain + .codes + .iter() + .map(|c| { + Ok(ErrorCodeBinding { name: c.name.clone(), value: c.code, message: c.message.clone(), doc: c.doc.clone(), - c_const: format!("{c_tag}_{}", c.name), - fields: self.fields(&c.fields), + c_const: member_symbol(&tag, &c.name), + fields: self.fields(&c.fields)?, }) - .collect(), - c_tag, - } + }) + .collect::>()?; + Ok(ErrorBinding { + name: domain.name.clone(), + type_name: crate::errors::type_name(&domain.name, "Error"), + module: dot_path.to_string(), + owner_path: path.to_string(), + c_tag: tag, + codes, + }) } - fn fields(&self, fields: &[StructField]) -> Vec { + fn fields(&self, fields: &[StructField]) -> Result, ValidationError> { fields .iter() - .map(|f| FieldBinding { - name: f.name.clone(), - doc: f.doc.clone(), - ty: self.ty(&f.ty), + .map(|f| { + Ok(FieldBinding { + name: f.name.clone(), + doc: f.doc.clone(), + ty: self.value(&f.ty)?, + }) }) .collect() } - fn struct_def(&self, s: &StructDef) -> StructBinding { - StructBinding { - name: s.name.clone(), - doc: s.doc.clone(), - deprecated: s.deprecated.clone(), - fields: self.fields(&s.fields), - } - } - - fn enum_def(&self, e: &EnumDef, path: &str) -> EnumBinding { - let c_tag = format!("{}_{path}_{}", self.prefix, e.name); + fn enum_def(&self, e: &EnumDef, path: &str) -> Result { + let tag = c_tag(self.prefix(), path, &e.name); let variants = e .variants .iter() - .map(|v| EnumVariantBinding { - name: v.name.clone(), - value: v.value, - doc: v.doc.clone(), - c_const: format!("{c_tag}_{}", v.name), - fields: self.fields(&v.fields), + .map(|v| { + Ok(EnumVariantBinding { + name: v.name.clone(), + value: v.value, + doc: v.doc.clone(), + c_const: member_symbol(&tag, &v.name), + fields: self.fields(&v.fields)?, + }) }) - .collect(); - EnumBinding { + .collect::>()?; + Ok(EnumBinding { name: e.name.clone(), doc: e.doc.clone(), deprecated: e.deprecated.clone(), - c_tag, + c_tag: tag, variants, rich: e.is_rich(), - } - } - - fn params(&self, params: &[Param]) -> Vec { - params - .iter() - .map(|p| { - let ty = self.ty(&p.ty); - ParamBinding { - name: p.name.clone(), - abi: lower_param(&p.name, &ty, self.types), - ty, - doc: p.doc.clone(), - } - }) - .collect() - } - - /// Lower a return type to its C return plus out-parameters, or `void`. - fn ret(&self, ret: Option<&Ty>) -> (CType, Vec) { - match ret { - Some(ty) => { - let r = lower_return(ty, self.types); - (r.ret, r.out_params) - } - None => (CType::Void, vec![]), - } - } - - /// The signature of one callback-interface method as it appears in the - /// vtable: `ctx`, then every parameter's slots, then the return's out - /// slots, then `out_err` (see - /// [`lower_callback_return`](crate::abi::lower_callback_return)). - /// - /// Object parameters differ from a plain call: the producer transfers one - /// strong reference the consumer adopts, so the slot is a mutable `{tag}*` - /// rather than the borrowed `const {tag}*` of a top-level parameter. - fn callback_method_signature(&self, params: &[ParamBinding], ret: Option<&Ty>) -> AbiSig { - let mut out = vec![ctx_param()]; - for p in params { - if matches!(p.ty.family(), Family::Object { .. }) { - let [slot] = p.abi.as_slice() else { - unreachable!("object parameter '{}' has one slot", p.name); - }; - let CType::Ptr { pointee, .. } = &slot.ty else { - unreachable!("object slot '{}' is a pointer", p.name); - }; - out.push(AbiParam::new(&slot.name, CType::ptr((**pointee).clone()))); - } else { - out.extend(p.abi.iter().cloned()); - } - } - let ret = match ret { - Some(ty) => { - let r = lower_callback_return(ty, self.types); - out.extend(r.out_params); - r.ret - } - None => CType::Void, - }; - out.push(error_out_param()); - AbiSig { params: out, ret } + }) } - /// The full C signature of a *synchronous* call: every input parameter's - /// slots, then the return type's out-parameters, then `out_err`. - fn sync_signature(&self, params: &[ParamBinding], ret: Option<&Ty>) -> AbiSig { - let mut out: Vec = params.iter().flat_map(|p| p.abi.iter().cloned()).collect(); - let (ret, out_params) = self.ret(ret); - out.extend(out_params); - out.push(error_out_param()); - AbiSig { params: out, ret } - } - - fn callback_interface(&self, c: &CallbackInterfaceDef, path: &str) -> CallbackInterfaceBinding { - let c_tag = format!("{}_{path}_{}", self.prefix, c.name); + fn callback_interface( + &self, + c: &CallbackInterfaceDef, + path: &str, + ) -> Result { + let tag = c_tag(self.prefix(), path, &c.name); let methods = c .methods .iter() - .map(|m| { - let params = self.params(&m.params); - let ret = m.returns.as_ref().map(|r| self.ty(r)); - let sig = self.callback_method_signature(¶ms, ret.as_ref()); - CallbackMethodBinding { - name: m.name.clone(), - doc: m.doc.clone(), - deprecated: m.deprecated.clone(), - throws: m.throws, - params, - ret, - abi_params: sig.params, - abi_ret: sig.ret, - } - }) - .collect(); - CallbackInterfaceBinding { + .map(|m| self.callback_method(m)) + .collect::>()?; + Ok(CallbackInterfaceBinding { name: c.name.clone(), doc: c.doc.clone(), deprecated: c.deprecated.clone(), - vtable_tag: format!("{c_tag}_vtable"), - c_tag, + vtable_tag: member_symbol(&tag, "vtable"), + c_tag: tag, methods, + }) + } + + /// Lower one callback-interface method to its vtable entry: `ctx`, then + /// every parameter's slots, then the return's out slots, then `out_err`. + fn callback_method(&self, m: &Function) -> Result { + let params = m + .params + .iter() + .map(|p| { + let ty = self.value(&p.ty)?; + Ok(CallbackParamBinding { + pass: self + .lower + .value_param(&p.name, &ty, ParamSite::CallbackMethod)?, + name: p.name.clone(), + ty, + doc: p.doc.clone(), + }) + }) + .collect::, ValidationError>>()?; + let ret = m.returns.as_ref().map(|r| self.value(r)).transpose()?; + let (c_ret, ret_pass) = self.lower.callback_ret(ret.as_ref())?; + let mut slots = vec![ctx_param()]; + for p in ¶ms { + slots.extend(p.pass.slots().into_iter().cloned()); } + slots.extend(ret_pass.out_slots().into_iter().cloned()); + slots.push(error_out_param()); + Ok(CallbackMethodBinding { + name: m.name.clone(), + doc: m.doc.clone(), + deprecated: m.deprecated.clone(), + abi: AbiFn { + symbol: m.name.clone(), + params: slots, + ret: c_ret, + }, + params, + ret, + ret_pass, + error: error_strategy(m.throws.as_ref()), + }) } /// Lower an interface: constructors become statics returning the /// interface, methods gain the implicit `self` slot, and all member /// symbols hang off the interface's `c_tag`. - fn interface(&self, iface: &InterfaceDef, path: &str) -> InterfaceBinding { - let c_tag = format!("{}_{path}_{}", self.prefix, iface.name); - let self_slot = AbiParam::new( + fn interface( + &self, + iface: &InterfaceDef, + path: &str, + ) -> Result { + let tag = c_tag(self.prefix(), path, &iface.name); + let owner = format!("{path}_{}", iface.name); + let receiver = AbiParam::new( "self", CType::Ptr { konst: ConstPos::West, @@ -356,151 +420,162 @@ impl Lowerer<'_> { }), }, ); - let member = |name: &str| format!("{c_tag}_{name}"); + let constructed = RetTy::Value(Ty::Interface(iface.name.clone())); let constructors = iface .constructors .iter() - .map(|c| { - // A constructor yields a new strong reference to the - // interface, exactly like a static returning it. - let mut f = c.clone(); - f.returns = Some(TypeRef::Named(iface.name.clone())); - self.callable(&f, &member(&c.name), None) - }) - .collect(); + .map(|c| self.callable(c, &owner, None, Some(constructed.clone()))) + .collect::>()?; let methods = iface .methods .iter() - .map(|m| self.callable(m, &member(&m.name), Some(self_slot.clone()))) - .collect(); + .map(|m| self.callable(m, &owner, Some(receiver.clone()), None)) + .collect::>()?; let statics = iface .statics .iter() - .map(|s| self.callable(s, &member(&s.name), None)) - .collect(); - InterfaceBinding { + .map(|s| self.callable(s, &owner, None, None)) + .collect::>()?; + Ok(InterfaceBinding { name: iface.name.clone(), doc: iface.doc.clone(), deprecated: iface.deprecated.clone(), - clone_symbol: format!("{c_tag}_clone"), - destroy_symbol: format!("{c_tag}_destroy"), - c_tag, + clone_symbol: member_symbol(&tag, "clone"), + destroy_symbol: member_symbol(&tag, "destroy"), + c_tag: tag, constructors, methods, statics, - } + }) } - /// Lower one callable (free function or interface member) whose full base - /// C symbol is `c_base`. When `self_slot` is given (an instance method), - /// it is prepended to every ABI signature but never appears in the - /// retained [`ParamBinding`] list. - fn callable(&self, f: &Function, c_base: &str, self_slot: Option) -> FnBinding { - let prefix = self.prefix; - let params = self.params(&f.params); - let ret = f.returns.as_ref().map(|r| self.ty(r)); - // The prefix-stripped spelling used for `CType::Named` cores (which - // render as `{prefix}_{core}`), e.g. `kv_Store_scan` from - // `weaveffi_kv_Store_scan`. - let core_base = c_base - .strip_prefix(&format!("{prefix}_")) - .expect("c_base always starts with the symbol prefix") - .to_string(); - let with_self = |mut params: Vec| { - if let Some(s) = &self_slot { - params.insert(0, s.clone()); - } - params + /// Lower one callable (free function or interface member). + /// + /// `owner` is the prefix-free C path the callable's symbols hang off: + /// the module path for a free function, `{module path}_{Interface}` for + /// a member. `receiver` is an instance method's `self` slot; `ret` + /// overrides the written return (a constructor returns its interface). + fn callable( + &self, + f: &Function, + owner: &str, + receiver: Option, + ret: Option, + ) -> Result { + let prefix = self.prefix(); + // `CType::Named` cores render as `{prefix}_{core}`. + let core = format!("{owner}_{}", f.name); + let symbol = format!("{prefix}_{core}"); + let params = f + .params + .iter() + .map(|p| { + let ty = self.param_ty(&p.ty)?; + Ok(ParamBinding { + pass: self.lower.param(&p.name, &ty, ParamSite::Call)?, + name: p.name.clone(), + ty, + doc: p.doc.clone(), + }) + }) + .collect::, ValidationError>>()?; + let ret = match ret { + Some(ret) => Some(ret), + None => f.returns.as_ref().map(|r| self.ret_ty(r)).transpose()?, }; - let inputs = - || -> Vec { params.iter().flat_map(|p| p.abi.iter().cloned()).collect() }; - - let shape = if let Some(elem) = ret.as_ref().and_then(Ty::iterator_elem) { - let pascal = f.name.to_upper_camel_case(); - // `{owner}_{Pascal}Iterator`, where owner is the module path for a - // free function or `{module path}_{Interface}` for a method. - let owner = &core_base[..core_base.len() - f.name.len() - 1]; - let iter_core = format!("{owner}_{pascal}Iterator"); - let iter_tag = format!("{prefix}_{iter_core}"); - - let mut launch_params = inputs(); - launch_params.push(error_out_param()); - let launch = AbiFn { - symbol: c_base.to_string(), - params: with_self(launch_params), - ret: CType::ptr(CType::Named(iter_core.clone())), - }; + let mut slots: Vec = receiver.iter().cloned().collect(); + for p in ¶ms { + slots.extend(p.pass.slots().into_iter().cloned()); + } - let item = lower_return(elem, self.types); - let mut next_params = vec![ - AbiParam::new("iter", CType::ptr(CType::Named(iter_core.clone()))), - AbiParam::new("out_item", CType::ptr(item.ret)), - ]; - next_params.extend(item.out_params); - next_params.push(error_out_param()); - let next = AbiFn { - symbol: format!("{iter_tag}_next"), - params: next_params, - ret: CType::Int32, + let (abi, ret_pass, shape) = if f.r#async { + let value = match &ret { + None => None, + Some(RetTy::Value(ty)) => Some(ty), + Some(RetTy::Iterator(_)) => { + return Err(ValidationError::AsyncIteratorReturn { + module: owner.to_string(), + function: f.name.clone(), + }) + } }; - - CallShape::Iterator(IteratorBinding { - elem: elem.clone(), - iter_tag: iter_tag.clone(), - launch, - next, - destroy_symbol: format!("{iter_tag}_destroy"), - }) - } else if f.r#async { + let result = self.lower.result(value)?; // Launcher: the input slots, the cancel token when cancellable, // then the completion callback and its context. - let mut launch_params = inputs(); - if f.cancellable { - launch_params.push(cancel_token_param()); - } - launch_params.push(AbiParam::new( + let cancel_token = f.cancellable.then(cancel_token_param); + slots.extend(cancel_token.iter().cloned()); + slots.push(AbiParam::new( "callback", - CType::Named(format!("{core_base}_callback")), + CType::Named(format!("{core}_callback")), )); - launch_params.push(context_param()); - // Completion callback: `(void* context, {prefix}_error* err, - // )`. + slots.push(context_param()); let mut callback_params = vec![ context_param(), AbiParam::new("err", CType::ptr(CType::Error)), ]; - if let Some(ret) = &ret { - callback_params.extend(callback_result_params(ret, self.types)); - } - CallShape::Async(AsyncBinding { - launch: AbiFn { - symbol: c_base.to_string(), - params: with_self(launch_params), - ret: CType::Void, - }, - callback_type: format!("{c_base}_callback"), + callback_params.extend(result.slots().into_iter().cloned()); + let launch = AbiFn { + symbol: symbol.clone(), + params: slots, + ret: CType::Void, + }; + let shape = CallShape::Async(AsyncBinding { + callback_type: format!("{symbol}_callback"), callback_params, - }) + result, + cancel_token, + }); + (launch, RetPass::Void, shape) + } else if let Some(RetTy::Iterator(elem)) = &ret { + // `{owner}_{Pascal}Iterator`. + let iter_core = format!("{owner}_{}Iterator", f.name.to_upper_camel_case()); + let iter_tag = format!("{prefix}_{iter_core}"); + let handle = CType::ptr(CType::Named(iter_core)); + slots.push(error_out_param()); + let launch = AbiFn { + symbol, + params: slots, + ret: handle.clone(), + }; + let item = self.lower.item(elem)?; + let mut next_params = vec![AbiParam::new("iter", handle)]; + next_params.extend(item.slots().into_iter().cloned()); + next_params.push(error_out_param()); + let iterator = IteratorBinding { + elem: elem.clone(), + next: AbiFn { + symbol: member_symbol(&iter_tag, "next"), + params: next_params, + ret: CType::Int32, + }, + item, + destroy_symbol: member_symbol(&iter_tag, "destroy"), + iter_tag, + }; + (launch, RetPass::Iterator(iterator), CallShape::Sync) } else { - let sig = self.sync_signature(¶ms, ret.as_ref()); - CallShape::Sync(AbiFn { - symbol: c_base.to_string(), - params: with_self(sig.params), - ret: sig.ret, - }) + let (c_ret, ret_pass) = self.lower.ret(ret.as_ref().and_then(RetTy::value))?; + slots.extend(ret_pass.out_slots().into_iter().cloned()); + slots.push(error_out_param()); + let call = AbiFn { + symbol, + params: slots, + ret: c_ret, + }; + (call, ret_pass, CallShape::Sync) }; - FnBinding { + Ok(FnBinding { name: f.name.clone(), doc: f.doc.clone(), deprecated: f.deprecated.clone(), - cancellable: f.cancellable, - throws: f.throws, - has_self: self_slot.is_some(), + receiver, params, ret, - c_base: c_base.to_string(), + ret_pass, + error: error_strategy(f.throws.as_ref()), + abi, shape, - } + }) } } diff --git a/crates/weaveffi-model/src/model/mod.rs b/crates/weaveffi-model/src/model/mod.rs index e1a33e7b..947f4b6b 100644 --- a/crates/weaveffi-model/src/model/mod.rs +++ b/crates/weaveffi-model/src/model/mod.rs @@ -1,38 +1,54 @@ //! The **model**: the one validated, fully lowered view of an API that every //! language backend consumes. //! -//! [`validate`](crate::validate::validate) checks an [`Api`] and builds the -//! [`Model`] exactly once per run. The model owns everything a generator -//! reads: +//! [`validate`](crate::validate::validate) checks an [`Api`](crate::ir::Api) +//! and builds the [`Model`] exactly once per run. The model owns everything +//! a generator reads: //! //! * the library's [`Identity`] (C symbol prefix, library name, package //! metadata) and the schema version; -//! * a flat list of [`ModuleBinding`]s in which every **type** is a resolved -//! [`Ty`] (no unresolved names), so a backend dispatches on [`Ty::family`] -//! and [`Ty::wire`] instead of re-deriving what a name means; +//! * a flat list of [`ModuleBinding`]s, linked by [`parent`] and +//! [`children`] indices, in which every **type** is resolved +//! ([`Ty`], [`ParamTy`], [`RetTy`]; no unresolved names); //! * every emitted **C symbol name**, precomputed once, so all backends agree -//! by construction; -//! * every function, interface member, and callback-interface method paired -//! with its lowered [`AbiFn`] signature (built from [`crate::abi`]), so no -//! backend re-derives parameter arity, ordering, or `out_*`/`out_err` -//! placement; and +//! by construction, plus the typed [symbol table](Model::c_symbols); +//! * every function, interface member, and callback-interface method with +//! its lowered [`AbiFn`] signature and its **passing contracts** +//! ([`ArgPass`] on each parameter, [`RetPass`], [`ResultPass`], +//! [`ItemPass`], or [`CallbackRetPass`] for what comes back, and the +//! [`ErrorStrategy`]), with every obligation resolved (release symbols, +//! element types, error domain names), so no backend re-derives arity, +//! slot order, `out_*`/`out_err` placement, or ownership; and //! * the [`TypeIndex`], which maps each (global) type name to its //! declaration and owning module, behind typed lookups such as -//! [`Model::interface`] and [`Model::owner`]. +//! [`Model::interface`], [`Model::error_domain`], and [`Model::owner`]. //! -//! A backend reads the *idiomatic* shape from the retained [`Ty`]s -//! (`param.ty`, `field.ty`, ...) and the *native* shape from the [`AbiFn`]s, -//! then writes only the marshalling that bridges the two (see -//! [`crate::plan`]) in its own idioms. Generators never see the [`Api`]. +//! A backend reads the *idiomatic* shape from the resolved types and the +//! *native* shape from the passing contracts and [`AbiFn`]s, then writes +//! only the marshalling that bridges the two in its own idioms. Generators +//! never see the [`Api`](crate::ir::Api). +//! +//! [`parent`]: ModuleBinding::parent +//! [`children`]: ModuleBinding::children +//! [`ArgPass`]: crate::plan::ArgPass +//! [`RetPass`]: crate::plan::RetPass +//! [`ResultPass`]: crate::plan::ResultPass +//! [`ItemPass`]: crate::plan::ItemPass +//! [`CallbackRetPass`]: crate::plan::CallbackRetPass +//! [`ErrorStrategy`]: crate::plan::ErrorStrategy mod build; +mod symbols; +#[cfg(all(test, feature = "idl"))] +mod tests; -pub(crate) use build::{build_indexed, index}; +pub(crate) use build::{build, index}; +pub use symbols::{Symbol, SymbolOwner}; use crate::abi::{AbiParam, CType}; -use crate::ir::{Api, TypeRef}; use crate::pkg::Identity; -use crate::ty::{Ty, TypeDecl, TypeIndex, TypeKind}; +use crate::plan::{ArgPass, CallbackRetPass, ErrorStrategy, ItemPass, ResultPass, RetPass}; +use crate::ty::{ParamTy, RetTy, Ty, TypeDecl, TypeIndex, TypeKind}; /// The runtime symbols every producer exports (and every C header declares), /// without the `{prefix}_` that begins each one. See the C ABI contract for @@ -74,7 +90,7 @@ pub const VALUE_CODECS: &[&str] = &["write", "read", "decode", "free"]; /// The C ABI revision this model lowers to. Producers export it from /// `{prefix}_abi_version()` and every generated consumer checks it at load. -pub const ABI_VERSION: u32 = 4; +pub const ABI_VERSION: u32 = 5; /// The symbol of a top-level module's contract table function, /// `{prefix}_{module}_contract` (see [`crate::contract`]). @@ -90,143 +106,190 @@ pub fn contract_check_symbol(prefix: &str, module: &str) -> String { format!("{prefix}_{module}_contract_check") } -/// A single lowered C symbol: its name, ordered ABI parameter slots, and C -/// return type. This is what a backend declares to its FFI layer and calls. +/// `{prefix}_{path}_{name}`: the C tag (type name stem) of a declaration +/// named `name` in the module whose underscore-joined path is `path`. +pub(crate) fn c_tag(prefix: &str, path: &str, name: &str) -> String { + format!("{prefix}_{path}_{name}") +} + +/// `{tag}_{member}`: a symbol hanging off a C tag (`_clone`, `_destroy`, +/// an interface member, an iterator's `_next`). +pub(crate) fn member_symbol(tag: &str, member: &str) -> String { + format!("{tag}_{member}") +} + +/// A single lowered C signature: its name, ordered ABI parameter slots, and +/// C return type. This is what a backend declares to its FFI layer and +/// calls (or, for a callback method, implements). #[derive(Debug, Clone, PartialEq, Eq)] pub struct AbiFn { - /// The fully-qualified, prefixed C symbol (e.g. `weaveffi_math_add`). + /// The fully qualified, prefixed C symbol (for example + /// `weaveffi_math_add`); for a callback method, the vtable field name. pub symbol: String, - /// Ordered parameter slots, including any trailing `out_*` and `out_err`. + /// Ordered parameter slots, including any leading `self` or `ctx` and + /// any trailing `out_*` and `out_err`. pub params: Vec, /// The C return type. pub ret: CType, } -/// How a function crosses the boundary. Exactly one shape applies to any given -/// function: synchronous, asynchronous (callback-completed), or iterator-returning. +/// How a callable crosses the boundary: a blocking call or an async launch. +/// +/// An iterator-returning function is a [`Sync`](Self::Sync) call whose +/// [`FnBinding::ret_pass`] is [`RetPass::Iterator`]. +// A model holds a few of these per callable and never moves them in bulk, +// so boxing the large variant would only cost consumers a deref. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallShape { - /// A plain blocking call: [`AbiFn`] is the symbol to invoke. - Sync(AbiFn), - /// An async launcher plus its completion-callback typedef. + /// A blocking call: invoke [`FnBinding::abi`], then receive its return + /// as [`FnBinding::ret_pass`] says. + Sync, + /// An async launch: invoke [`FnBinding::abi`] (the launcher, which + /// returns `void`) and receive the result in the completion callback. Async(AsyncBinding), - /// An iterator-returning function: an opaque handle plus `next`/`destroy`. - Iterator(IteratorBinding), } -/// The lowered surface of an `async` function. +/// The lowered completion side of an `async` callable. +/// +/// The launcher is [`FnBinding::abi`]: the receiver (if any), the input +/// slots, the `cancel_token` slot when cancellable, then +/// `{callback_type} callback` and `void* context`. It returns `void` and has +/// no `out_err`; every failure arrives through the completion. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AsyncBinding { - /// The launcher: input slots, optional `cancel_token`, then `callback` and - /// `context`. Returns `void`. - pub launch: AbiFn, - /// The completion-callback function-pointer typedef name + /// The completion-callback function-pointer typedef /// (`{symbol}_callback`). pub callback_type: String, - /// The callback's parameter slots: `(void* context, {prefix}_error* err, - /// )`. + /// The completion callback's parameter slots, in order: + /// `void* context`, `{prefix}_error* err`, then the result slots + /// ([`result`](Self::result)). pub callback_params: Vec, + /// How the result arrives in the callback, with the release it owes. + pub result: ResultPass, + /// The launcher's `{prefix}_cancel_token* cancel_token` slot for a + /// `cancellable` callable, else `None`. + pub cancel_token: Option, +} + +impl AsyncBinding { + /// `true` when the launcher takes a cancel token. + #[must_use] + pub fn cancellable(&self) -> bool { + self.cancel_token.is_some() + } } -/// The lowered surface of an `iter`-returning function. +/// The lowered surface of an `iter`-returning callable. +/// +/// The launcher is [`FnBinding::abi`], which returns `{iter_tag}*`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct IteratorBinding { /// The element type `T` of `iter`. pub elem: Ty, - /// The opaque iterator tag (`{prefix}_{path}_{Pascal}Iterator`). + /// The opaque iterator tag (`{prefix}_{owner}_{Pascal}Iterator`). pub iter_tag: String, - /// The launcher returning `{iter_tag}*`. - pub launch: AbiFn, - /// `int32_t {iter_tag}_next({iter_tag}* iter, T* out_item, ..., error* out_err)`. + /// `int32_t {iter_tag}_next({iter_tag}* iter, , + /// {prefix}_error* out_err)`: returns `1` with an element, `0` when + /// done. pub next: AbiFn, + /// How each element arrives through `next`'s out slots. + pub item: ItemPass, /// `void {iter_tag}_destroy({iter_tag}* iter)`. pub destroy_symbol: String, } -impl IteratorBinding { - /// The C type of one element: the pointee of `next`'s `T* out_item` - /// slot. - /// - /// # Panics - /// - /// Panics if the model built `next` without its `out_item` pointer slot, - /// which would be a bug in the model construction. - pub fn item_ctype(&self) -> &CType { - match &self.next.params[1].ty { - CType::Ptr { pointee, .. } => pointee, - other => panic!("iterator `out_item` slot is {other:?}, not a pointer"), - } - } -} - -/// One IR parameter, retained with its lowered ABI slots. +/// One parameter of a callable, with its lowered passing contract. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParamBinding { /// The parameter name as written in the IDL. pub name: String, /// The resolved type a backend renders the parameter as. - pub ty: Ty, + pub ty: ParamTy, /// Optional doc comment carried from the IDL. pub doc: Option, - /// The ordered C ABI slots this single parameter expands into. - pub abi: Vec, + /// The C slots the parameter occupies and how they're filled. + pub pass: ArgPass, } -/// A function, fully lowered. +/// A callable (free function or interface member), fully lowered. /// -/// Free functions and interface members share this shape. For an instance -/// method, [`has_self`](Self::has_self) is `true` and every [`AbiFn`] in -/// [`shape`](Self::shape) carries an implicit leading `const {c_tag}* self` -/// slot that does **not** appear in [`params`](Self::params); a wrapper -/// passes its own native handle there. +/// For an instance method, [`receiver`](Self::receiver) is the implicit +/// leading `const {c_tag}* self` slot of [`abi`](Self::abi), which does +/// **not** appear in [`params`](Self::params); a wrapper passes its own +/// native handle there. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FnBinding { - /// The function name as written in the IDL. + /// The callable's name as written in the IDL. pub name: String, /// Optional doc comment carried from the IDL. pub doc: Option, - /// Deprecation message when the function is marked deprecated, else `None`. + /// Deprecation message when the callable is marked deprecated, else + /// `None`. pub deprecated: Option, - /// Whether an async function accepts a trailing `cancel_token` slot. - pub cancellable: bool, - /// Whether the function reports typed domain errors. A throwing function - /// surfaces as `throws`/`raises` in idiomatic wrappers using the error - /// domain in scope ([`Model::error_domain`]); a non-throwing function - /// has a plain signature, and a - /// reported error (only ever a producer panic) surfaces as the target's - /// unrecoverable-error idiom instead. - pub throws: bool, - /// `true` for an instance method: the ABI signatures carry an implicit - /// leading `self` slot not present in [`params`](Self::params). - pub has_self: bool, - /// Input parameters with their lowered slots. + /// The `const {c_tag}* self` slot of an instance method, else `None`. + pub receiver: Option, + /// Input parameters with their passing contracts. pub params: Vec, - /// The resolved return type (`None` = void). For an iterator function this - /// is the `iter` type itself; the element `T` also lives in - /// [`IteratorBinding`]. For an interface constructor this is the - /// constructed interface type. - pub ret: Option, - /// Base C symbol (`{prefix}_{module_path}_{name}` for a free function, - /// `{c_tag}_{name}` for an interface member) before any `_async`/iterator - /// suffixing. - pub c_base: String, - /// The call shape (sync / async / iterator). + /// The resolved return type (`None` = void). For an interface + /// constructor this is the constructed interface type. + pub ret: Option, + /// How [`abi`](Self::abi)'s C return and trailing out slots carry the + /// return back: the value for a sync call, [`RetPass::Iterator`] for an + /// iterator launcher, and [`RetPass::Void`] for an async launcher. + pub ret_pass: RetPass, + /// How a reported error is interpreted. + pub error: ErrorStrategy, + /// The symbol to call: the sync entry point, the async launcher, or the + /// iterator launcher, with its full ordered slot list. + pub abi: AbiFn, + /// Whether the call blocks or completes asynchronously. pub shape: CallShape, } impl FnBinding { - /// `true` when the function is `async` (lowered as a callback-completed - /// launcher). + /// `true` for an instance method (one with a [`receiver`](Self::receiver)). + #[must_use] + pub fn has_self(&self) -> bool { + self.receiver.is_some() + } + + /// `true` when the callable is `async`. + #[must_use] pub fn is_async(&self) -> bool { matches!(self.shape, CallShape::Async(_)) } + + /// The completion side of an async callable, else `None`. + #[must_use] + pub fn async_binding(&self) -> Option<&AsyncBinding> { + match &self.shape { + CallShape::Async(a) => Some(a), + CallShape::Sync => None, + } + } + + /// The iterator surface of an iterator-returning callable, else `None`. + #[must_use] + pub fn iterator(&self) -> Option<&IteratorBinding> { + match &self.ret_pass { + RetPass::Iterator(it) => Some(it), + _ => None, + } + } + + /// `true` when the callable is async and takes a cancel token. + #[must_use] + pub fn cancellable(&self) -> bool { + self.async_binding().is_some_and(AsyncBinding::cancellable) + } } /// A field of a record, a rich-enum variant, or an error code's payload. /// -/// Records and rich enums are value types: they declare no C symbols of their -/// own and cross the ABI serialized inside a value buffer, so a field is just -/// its name and type. Field declaration order **is** the wire order. +/// Records and rich enums are value types: they declare no C functions of +/// their own and cross the ABI serialized inside a value buffer, so a field +/// is just its name and type. Field declaration order **is** the wire order. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldBinding { /// The field name as written in the IDL. @@ -238,8 +301,8 @@ pub struct FieldBinding { } /// A struct (record), fully lowered: a plain value type generators emit as a -/// native data class plus buffer read/write functions. No C symbols exist for -/// a record; instances cross the ABI serialized in value buffers. +/// native data class plus buffer read/write functions. Instances cross the +/// ABI serialized in value buffers. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StructBinding { /// The struct name as written in the IDL. @@ -248,6 +311,9 @@ pub struct StructBinding { pub doc: Option, /// Deprecation message when the struct is marked deprecated, else `None`. pub deprecated: Option, + /// `{prefix}_{module_path}_{name}`: the struct the C value-buffer + /// helper header declares, and the stem of its codecs. + pub c_tag: String, /// The fields in declaration (and wire) order. pub fields: Vec, } @@ -255,12 +321,13 @@ pub struct StructBinding { /// An enum, fully lowered. /// /// A *C-style* enum (every variant a bare discriminant) crosses the ABI by -/// value as an integer. An *algebraic* (rich) enum, at least one variant with -/// associated data, is a value type exactly like a struct: it crosses the ABI -/// serialized in a value buffer as an `i32` tag followed by the active -/// variant's fields in declaration order. Either way, the C header still -/// emits the discriminant constants ([`EnumVariantBinding::c_const`]) so C -/// consumers can switch on the value or tag. +/// value as an `int32_t`. An *algebraic* (rich) enum, at least one variant +/// with associated data, is a value type exactly like a struct: it crosses +/// the ABI serialized in a value buffer as an `i32` tag followed by the +/// active variant's fields in declaration order. Either way, the C header +/// still emits the discriminant constants +/// ([`EnumVariantBinding::c_const`]) so C consumers can switch on the value +/// or tag. #[derive(Debug, Clone, PartialEq, Eq)] pub struct EnumBinding { /// The enum name as written in the IDL. @@ -280,6 +347,7 @@ pub struct EnumBinding { impl EnumBinding { /// `true` when this is a rich (algebraic) sum-type enum. + #[must_use] pub fn is_rich(&self) -> bool { self.rich } @@ -307,16 +375,17 @@ pub struct EnumVariantBinding { /// /// Constructors, methods, and statics are all [`FnBinding`]s sharing the /// member symbol scheme `{c_tag}_{name}`. Methods additionally carry an -/// implicit leading `const {c_tag}* self` ABI slot ([`FnBinding::has_self`]). -/// A constructor's [`FnBinding::ret`] is synthesized as the interface type -/// itself, so wrappers can reuse their ordinary return-marshalling path. +/// implicit leading `const {c_tag}* self` slot ([`FnBinding::receiver`]). +/// A constructor's [`FnBinding::ret`] is the interface type itself, so +/// wrappers can reuse their ordinary return-marshalling path. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceBinding { /// The interface name as written in the IDL. pub name: String, /// Optional doc comment carried from the IDL. pub doc: Option, - /// Deprecation message when the interface is marked deprecated, else `None`. + /// Deprecation message when the interface is marked deprecated, else + /// `None`. pub deprecated: Option, /// `{prefix}_{module_path}_{name}`, the opaque tag. pub c_tag: String, @@ -333,13 +402,35 @@ pub struct InterfaceBinding { pub destroy_symbol: String, } +impl InterfaceBinding { + /// Every member: constructors, then methods, then statics. + pub fn members(&self) -> impl Iterator { + self.constructors + .iter() + .chain(&self.methods) + .chain(&self.statics) + } +} + +/// One parameter of a callback-interface method: always a value type, with +/// the slots the producer fills and the consumer's trampoline receives. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CallbackParamBinding { + /// The parameter name as written in the IDL. + pub name: String, + /// The resolved type. + pub ty: Ty, + /// Optional doc comment carried from the IDL. + pub doc: Option, + /// The C slots the parameter occupies. Never [`ArgPass::Callback`]; an + /// [`ArgPass::Object`] here transfers one strong reference to the + /// consumer. + pub pass: ArgPass, +} + /// One method of a callback interface, lowered to its vtable entry. /// /// The consumer implements this; the producer calls it through the vtable. -/// [`abi_params`](Self::abi_params) is the full C slot list of the vtable -/// entry (`void* ctx`, the parameter slots, the return's out slots, then -/// `{prefix}_error* out_err`) and [`abi_ret`](Self::abi_ret) its C return -/// type. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CallbackMethodBinding { /// The method name as written in the IDL; also the vtable field name. @@ -348,31 +439,29 @@ pub struct CallbackMethodBinding { pub doc: Option, /// Deprecation message when the method is marked deprecated, else `None`. pub deprecated: Option, - /// Whether the method may report a positive code of the error domain in - /// scope ([`Model::error_domain`]) through its `out_err`. - pub throws: bool, - /// Input parameters with their lowered slots. - pub params: Vec, - /// The resolved return type (`None` = void). Any family but iterators - /// and callback interfaces; see - /// [`lower_callback_return`](crate::abi::lower_callback_return). + /// Input parameters with their passing contracts. + pub params: Vec, + /// The resolved return type (`None` = void). pub ret: Option, - /// The vtable entry's C parameter slots: `ctx`, then every parameter's - /// slots, then the return's out slots (`out_ptr` and `out_len` for a - /// string, bytes, or buffer return), then `out_err`. - pub abi_params: Vec, - /// The vtable entry's C return type. - pub abi_ret: CType, + /// How the return crosses back to the producer. + pub ret_pass: CallbackRetPass, + /// Which failures the method may report. + pub error: ErrorStrategy, + /// The vtable entry's signature: [`symbol`](AbiFn::symbol) is the + /// method's field name, and the slots are `void* ctx` first, then every + /// parameter's slots, then the return's out slots, then + /// `{prefix}_error* out_err` last. + pub abi: AbiFn, } /// A callback interface, fully lowered. /// /// The C ABI sees a vtable struct ([`vtable_tag`](Self::vtable_tag)) that /// starts with a fixed header, `uint32_t size` (the vtable's size as the -/// consumer compiled it), `uint32_t flags` (reserved, `0`), and -/// `void (*free)(void* ctx)`, followed by one function-pointer field per -/// method in declaration order. A parameter of this type lowers to two -/// slots, `void* {name}_ctx` and `const {vtable_tag}* {name}_vtable`. +/// consumer compiled it), `uint32_t flags`, and `void (*free)(void* ctx)`, +/// followed by one function-pointer field per method in declaration order. A +/// parameter of this type lowers to two slots, `void* {name}_ctx` and +/// `const {vtable_tag}* {name}_vtable`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CallbackInterfaceBinding { /// The callback interface name as written in the IDL. @@ -390,10 +479,11 @@ pub struct CallbackInterfaceBinding { pub methods: Vec, } -/// One error code of a module's error domain, with its C constant name. +/// One error code of an error domain, with its C constant name. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ErrorCodeBinding { - /// The code name exactly as written in the IDL (e.g. `KEY_NOT_FOUND`). + /// The code name exactly as written in the IDL (for example + /// `KEY_NOT_FOUND`). pub name: String, /// The numeric ABI code carried in `{prefix}_error.code`. pub value: i32, @@ -413,6 +503,7 @@ pub struct ErrorCodeBinding { impl ErrorCodeBinding { /// `{c_const}_payload`, the struct the C value-buffer header declares /// for the code's [`fields`](Self::fields) (only when there are any). + #[must_use] pub fn payload_tag(&self) -> String { format!("{}_payload", self.c_const) } @@ -420,44 +511,56 @@ impl ErrorCodeBinding { /// An error domain, lowered on the module that declares it. /// -/// Every throwing function reports codes from the domain in scope for its -/// module: the module's own, or the nearest ancestor's -/// ([`Model::error_domain`]). Backends emit one error type per declaring -/// module and reference it from inheriting submodules. +/// A callable reports a domain's codes when its +/// [`ErrorStrategy`] is [`Domain`](ErrorStrategy::Domain) naming it; +/// [`Model::error_domain`] resolves the name. Domains are open: a consumer +/// maps a positive code it doesn't know to the domain's base error type. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ErrorBinding { - /// The domain name as written in the IDL (e.g. `KvError`). + /// The domain name as written in the IDL (for example `KvError`). pub name: String, - /// PascalCase type name with exactly one `Error` suffix (e.g. `KvError`); - /// backends that brand exceptions swap the suffix via - /// [`crate::errors::type_name`]. + /// The domain's error type name with exactly one `Error` suffix (for + /// example `KvError`, and `KitchenError` for `KitchenErrors`), from + /// [`crate::errors::type_name`]. Backends that brand exceptions use + /// [`crate::errors::exception_type_name`] on [`name`](Self::name). pub type_name: String, - /// Underscore-joined path of the module that declares the domain. + /// Dot-joined path of the declaring module (for example `kv.stats`). + pub module: String, + /// Underscore-joined path of the declaring module. pub owner_path: String, - /// `{prefix}_{owner_path}_{name}`, the C tag naming the domain's code - /// constants. + /// `{prefix}_{owner_path}_{name}`, the C type (`typedef int32_t`) naming + /// the domain's code constants. pub c_tag: String, /// The domain's codes in declaration order. pub codes: Vec, } -/// One module, flattened with its underscore-joined symbol path. +/// One module, flattened with its underscore-joined symbol path and linked +/// to its parent and children by index into [`Model::modules`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModuleBinding { + /// The module's position in [`Model::modules`]. + pub index: usize, /// The module name (its final path segment). pub name: String, - /// Path segments from the root (e.g. `["outer", "inner"]`). + /// Path segments from the root (for example `["outer", "inner"]`). pub segments: Vec, - /// Underscore-joined path used as the C symbol segment (e.g. `outer_inner`). + /// Underscore-joined path used as the C symbol segment (for example + /// `outer_inner`). pub path: String, - /// Dot-joined path (e.g. `outer.inner`), the declaration path diagnostics - /// and generated comments quote. + /// Dot-joined path (for example `outer.inner`), the declaration path + /// diagnostics and generated comments quote. pub dot_path: String, + /// The parent module's position in [`Model::modules`], or `None` for a + /// top-level module. + pub parent: Option, + /// The direct submodules' positions in [`Model::modules`], in + /// declaration order. + pub children: Vec, /// The module's doc comment, if the IDL records one. pub doc: Option, - /// The error domain this module declares, if any. A module without one - /// inherits the nearest ancestor's ([`Model::error_domain`]). - pub errors: Option, + /// The error domains this module declares, in declaration order. + pub errors: Vec, /// Enums declared in this module, fully lowered. pub enums: Vec, /// Structs declared in this module, fully lowered. @@ -476,23 +579,19 @@ impl ModuleBinding { pub fn callables(&self) -> impl Iterator { self.functions .iter() - .chain(self.interfaces.iter().flat_map(|i| { - i.constructors - .iter() - .chain(i.methods.iter()) - .chain(i.statics.iter()) - })) + .chain(self.interfaces.iter().flat_map(InterfaceBinding::members)) } /// `true` when any callable in this module is `async`. + #[must_use] pub fn has_async(&self) -> bool { self.callables().any(FnBinding::is_async) } /// `true` when any callable in this module returns an iterator. + #[must_use] pub fn has_iterators(&self) -> bool { - self.callables() - .any(|f| matches!(f.shape, CallShape::Iterator(_))) + self.callables().any(|f| f.iterator().is_some()) } } @@ -507,56 +606,40 @@ pub struct Model { pub identity: Identity, /// The IR schema version of the source document. pub version: String, - /// Modules in depth-first pre-order, each carrying its joined symbol path. + /// Modules in depth-first pre-order (a parent before its children). pub modules: Vec, /// Every user type name, mapped to its declaration. pub types: TypeIndex, } impl Model { - /// Build the model without validating the document. - /// - /// Type names that resolve to no declaration become [`Ty::Record`] - /// references named exactly as written. Tests use it for hand-built - /// trees; everything else goes through - /// [`validate`](crate::validate::validate). - #[doc(hidden)] - #[must_use] - pub fn assume_valid(api: &Api, identity: Identity) -> Self { - build::build(api, identity) - } - - /// Resolve a written type reference against this model's declarations. - /// A name no declaration provides resolves to a [`Ty::Record`] (see - /// [`assume_valid`](Self::assume_valid)). - pub fn resolve(&self, ty: &TypeRef) -> Ty { - build::resolve(&self.types, ty) - } - /// The C symbol prefix every emitted name starts with. + #[must_use] pub fn prefix(&self) -> &str { &self.identity.prefix } - /// The top-level modules (those with a single path segment), in order. + /// The top-level modules, in declaration order. pub fn roots(&self) -> impl Iterator { - self.modules.iter().filter(|m| m.segments.len() == 1) + self.modules.iter().filter(|m| m.parent.is_none()) } - /// The direct submodules of `parent`, in declaration order. Backends that - /// render nested namespaces recurse with this instead of re-walking the - /// IR tree. + /// The direct submodules of `parent`, in declaration order. pub fn children<'a>( &'a self, parent: &'a ModuleBinding, ) -> impl Iterator + 'a { - self.modules.iter().filter(move |m| { - m.segments.len() == parent.segments.len() + 1 - && m.segments[..parent.segments.len()] == parent.segments[..] - }) + parent.children.iter().filter_map(|&i| self.modules.get(i)) } - /// Iterate every function across all modules, paired with its module. + /// The parent of `module`, or `None` for a top-level module. + #[must_use] + pub fn parent(&self, module: &ModuleBinding) -> Option<&ModuleBinding> { + module.parent.and_then(|i| self.modules.get(i)) + } + + /// Iterate every free function across all modules, paired with its + /// module. pub fn functions(&self) -> impl Iterator { self.modules .iter() @@ -581,12 +664,21 @@ impl Model { .flat_map(|m| m.callback_interfaces.iter().map(move |c| (m, c))) } - /// The declaration of the user type `name`. + /// Iterate every error domain across all modules, paired with its + /// module. + pub fn error_domains(&self) -> impl Iterator { + self.modules + .iter() + .flat_map(|m| m.errors.iter().map(move |e| (m, e))) + } + + /// The declaration of the user type `name`, which must be one of + /// `kinds`. /// /// # Panics /// - /// Panics when `name` isn't declared, which validation rules out for - /// every name a [`Ty`] carries. + /// Panics when `name` isn't declared as one of `kinds`, which + /// validation rules out for every name the model carries. fn decl(&self, name: &str, kinds: &[TypeKind]) -> &TypeDecl { match self.types.get(name) { Some(d) if kinds.contains(&d.kind) => d, @@ -595,12 +687,13 @@ impl Model { } } - /// The module declaring the user type `name`. + /// The module declaring the user type or error domain `name`. /// /// # Panics /// - /// Panics when `name` isn't declared, which validation rules out for - /// every name a [`Ty`] carries. + /// Panics when `name` isn't declared in this model (a name the model + /// itself carries always is; a foreign record has no owner). + #[must_use] pub fn owner(&self, name: &str) -> &ModuleBinding { let decl = self.types.get(name); let decl = decl.unwrap_or_else(|| panic!("type '{name}' is not declared")); @@ -612,17 +705,19 @@ impl Model { /// # Panics /// /// Panics when no interface has that name. + #[must_use] pub fn interface(&self, name: &str) -> &InterfaceBinding { let d = self.decl(name, &[TypeKind::Interface]); &self.modules[d.module].interfaces[d.index] } /// The callback interface named `name`, as carried by - /// [`Ty::CallbackInterface`]. + /// [`ParamTy::Callback`]. /// /// # Panics /// /// Panics when no callback interface has that name. + #[must_use] pub fn callback_interface(&self, name: &str) -> &CallbackInterfaceBinding { let d = self.decl(name, &[TypeKind::CallbackInterface]); &self.modules[d.module].callback_interfaces[d.index] @@ -634,162 +729,72 @@ impl Model { /// # Panics /// /// Panics when no enum has that name. + #[must_use] pub fn enumeration(&self, name: &str) -> &EnumBinding { let d = self.decl(name, &[TypeKind::Enum, TypeKind::RichEnum]); &self.modules[d.module].enums[d.index] } - /// The error domain in scope for `module`'s throwing functions: its own, - /// else the nearest ancestor's, else `None` (in which case validation - /// has rejected any `throws` there). - pub fn error_domain(&self, module: &ModuleBinding) -> Option<&ErrorBinding> { - (1..=module.segments.len()).rev().find_map(|n| { - let scope = &module.segments[..n]; - self.modules - .iter() - .find(|m| m.segments == scope) - .and_then(|m| m.errors.as_ref()) - }) + /// The record named `name`, as carried by [`Ty::Record`]. + /// + /// # Panics + /// + /// Panics when no record in this model has that name (including a + /// foreign record, which [`TypeIndex::is_foreign`] identifies). + #[must_use] + pub fn record(&self, name: &str) -> &StructBinding { + let d = self.decl(name, &[TypeKind::Record]); + &self.modules[d.module].structs[d.index] + } + + /// The error domain named `name`, as carried by + /// [`ErrorStrategy::Domain`]. Domain names are global, so the domain may + /// be declared in any module. + /// + /// # Panics + /// + /// Panics when no error domain has that name; validation guarantees one + /// for every name an [`ErrorStrategy`] in the model carries. + #[must_use] + pub fn error_domain(&self, name: &str) -> &ErrorBinding { + let d = self.decl(name, &[TypeKind::ErrorDomain]); + &self.modules[d.module].errors[d.index] } - /// Every C identifier the library's ABI declares, paired with a - /// human-readable description of the declaration that owns it, in - /// declaration order. Validation rejects an API in which two entries share - /// an identifier. + /// Every C identifier the library's ABI declares, with the declaration + /// that owns it, in declaration order. Validation rejects an API in + /// which two entries share an identifier. /// /// Covers the runtime surface ([`RUNTIME_SYMBOLS`]), each top-level /// module's contract table function and the C header's checker for it, - /// every callable's symbols (sync entry point, async launcher and - /// completion type, iterator launcher, type, `_next`, and `_destroy`), - /// interface types with their `_clone` and `_destroy`, enum types and - /// constants (and a rich enum's tag type), error-domain types and code - /// constants, callback-interface vtable types, and what the C - /// value-buffer helper header declares per user type: the struct of - /// every record, rich enum, and error code with fields - /// ([`ErrorCodeBinding::payload_tag`]), each with its - /// [`VALUE_CODECS`]. (The header's list, map, and optional shapes fall - /// in [`RESERVED_SYMBOL_FAMILIES`].) - pub fn c_symbols(&self) -> Vec<(String, String)> { - let p = self.prefix(); - let mut out: Vec<(String, String)> = RUNTIME_SYMBOLS - .iter() - .map(|s| (format!("{p}_{s}"), format!("the runtime symbol '{p}_{s}'"))) - .collect(); - for m in self.roots() { - out.push(( - contract_symbol(p, &m.name), - format!("the contract table of module '{}'", m.name), - )); - out.push(( - contract_check_symbol(p, &m.name), - format!("the contract check of module '{}'", m.name), - )); - } - let callable = |out: &mut Vec<(String, String)>, f: &FnBinding, owner: &str| { - let what = format!("'{owner}.{}'", f.name); - match &f.shape { - CallShape::Sync(abi) => out.push((abi.symbol.clone(), format!("function {what}"))), - CallShape::Async(a) => { - out.push((a.launch.symbol.clone(), format!("async function {what}"))); - out.push(( - a.callback_type.clone(), - format!("the completion type of {what}"), - )); - } - CallShape::Iterator(it) => { - out.push((it.launch.symbol.clone(), format!("function {what}"))); - out.push((it.iter_tag.clone(), format!("the iterator type of {what}"))); - out.push(( - it.next.symbol.clone(), - format!("the iterator step of {what}"), - )); - out.push(( - it.destroy_symbol.clone(), - format!("the iterator destructor of {what}"), - )); - } - } - }; - let codecs = |out: &mut Vec<(String, String)>, tag: &str, what: &str| { - for codec in VALUE_CODECS { - out.push(( - format!("{tag}_{codec}"), - format!("the value-buffer codec '{tag}_{codec}' of {what}"), - )); - } - }; - for m in &self.modules { - let dot = &m.dot_path; - if let Some(e) = &m.errors { - out.push((e.c_tag.clone(), format!("error domain '{dot}.{}'", e.name))); - for c in &e.codes { - let what = format!("error code '{dot}.{}.{}'", e.name, c.name); - out.push((c.c_const.clone(), what.clone())); - if !c.fields.is_empty() { - let payload = c.payload_tag(); - codecs(&mut out, &payload, &format!("the payload of {what}")); - out.push((payload, format!("the payload struct of {what}"))); - } - } - } - for e in &m.enums { - out.push((e.c_tag.clone(), format!("enum '{dot}.{}'", e.name))); - if e.rich { - out.push(( - format!("{}_Tag", e.c_tag), - format!("the tag type of enum '{dot}.{}'", e.name), - )); - codecs(&mut out, &e.c_tag, &format!("enum '{dot}.{}'", e.name)); - } - for v in &e.variants { - out.push(( - v.c_const.clone(), - format!("enum variant '{dot}.{}.{}'", e.name, v.name), - )); - } - } - for s in &m.structs { - let tag = format!("{p}_{}_{}", m.path, s.name); - let what = format!("record '{dot}.{}'", s.name); - codecs(&mut out, &tag, &what); - out.push((tag, what)); - } - for c in &m.callback_interfaces { - out.push(( - c.vtable_tag.clone(), - format!("the vtable of callback interface '{dot}.{}'", c.name), - )); - } - for i in &m.interfaces { - let owner = format!("{dot}.{}", i.name); - out.push((i.c_tag.clone(), format!("interface '{owner}'"))); - out.push((i.clone_symbol.clone(), format!("the clone of '{owner}'"))); - out.push(( - i.destroy_symbol.clone(), - format!("the destructor of '{owner}'"), - )); - for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { - callable(&mut out, f, &owner); - } - } - for f in &m.functions { - callable(&mut out, f, dot); - } - } - out + /// every callable's symbols (entry point or launcher, async completion + /// type, iterator type, `_next`, and `_destroy`), interface types with + /// their `_clone` and `_destroy`, enum types and constants (and a rich + /// enum's tag type), error-domain types and code constants, + /// callback-interface vtable types, and what the C value-buffer helper + /// header declares per user type: the struct of every record, rich enum, + /// and error code with fields ([`ErrorCodeBinding::payload_tag`]), each + /// with its [`VALUE_CODECS`]. (The header's list, map, and optional + /// shapes fall in [`RESERVED_SYMBOL_FAMILIES`].) + #[must_use] + pub fn c_symbols(&self) -> Vec { + symbols::collect(self) } /// `true` when any callable anywhere in the API is `async`. + #[must_use] pub fn has_async(&self) -> bool { self.modules.iter().any(ModuleBinding::has_async) } /// `true` when any callable anywhere in the API returns an iterator. + #[must_use] pub fn has_iterators(&self) -> bool { self.modules.iter().any(ModuleBinding::has_iterators) } /// `true` when the API declares any callback interface. + #[must_use] pub fn has_callback_interfaces(&self) -> bool { self.modules .iter() @@ -797,418 +802,34 @@ impl Model { } /// `true` when the API declares any interface. + #[must_use] pub fn has_interfaces(&self) -> bool { self.modules.iter().any(|m| !m.interfaces.is_empty()) } - /// `true` when any type anywhere in the API crosses the ABI as a value - /// buffer (records, rich enums, optionals, lists, maps, error payloads). + /// `true` when any value anywhere in the API crosses the ABI as a value + /// buffer (records, rich enums, error payloads, and optionals, lists, + /// and maps outside the OptDirect and Slice families). + #[must_use] pub fn has_buffers(&self) -> bool { let buffered = |ty: &Ty| ty.any(&Ty::is_buffered); - let signature = |params: &[ParamBinding], ret: &Option| { - params.iter().any(|p| buffered(&p.ty)) || ret.as_ref().is_some_and(buffered) + let callable = |f: &FnBinding| { + f.params.iter().any(|p| p.ty.value().is_some_and(buffered)) + || f.ret.as_ref().is_some_and(|r| buffered(r.elem())) + }; + let method = |m: &CallbackMethodBinding| { + m.params.iter().any(|p| buffered(&p.ty)) || m.ret.as_ref().is_some_and(buffered) }; self.modules.iter().any(|m| { !m.structs.is_empty() || m.enums.iter().any(|e| e.rich) || m.errors - .as_ref() - .is_some_and(|e| e.codes.iter().any(|c| !c.fields.is_empty())) - || m.callables().any(|f| signature(&f.params, &f.ret)) + .iter() + .any(|e| e.codes.iter().any(|c| !c.fields.is_empty())) + || m.callables().any(callable) || m.callback_interfaces .iter() - .any(|c| c.methods.iter().any(|f| signature(&f.params, &f.ret))) + .any(|c| c.methods.iter().any(method)) }) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::ir::{ - CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, - Module, Param, StructDef, StructField, TypeRef, - }; - use crate::ty::Prim; - - fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } - } - - fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } - } - - fn module(name: &str) -> Module { - Module { - name: name.into(), - doc: None, - functions: vec![], - interfaces: vec![], - callback_interfaces: vec![], - structs: vec![], - enums: vec![], - errors: None, - modules: vec![], - } - } - - fn build(modules: Vec, prefix: &str) -> Model { - let api = Api { - version: crate::ir::CURRENT_SCHEMA_VERSION.into(), - modules, - }; - Model::assume_valid(&api, Identity::named(prefix)) - } - - fn rendered(abi: &AbiFn) -> Vec { - abi.params - .iter() - .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) - .collect() - } - - #[test] - fn sync_function_symbol_and_sig() { - let m = Module { - functions: vec![func( - "add", - vec![ - param("a", TypeRef::Prim(Prim::I32)), - param("b", TypeRef::Prim(Prim::I32)), - ], - Some(TypeRef::Prim(Prim::I32)), - )], - ..module("math") - }; - let model = build(vec![m], "weaveffi"); - let f = &model.modules[0].functions[0]; - assert_eq!(f.c_base, "weaveffi_math_add"); - let CallShape::Sync(abi) = &f.shape else { - panic!("expected sync") - }; - assert_eq!(abi.symbol, "weaveffi_math_add"); - assert_eq!(abi.ret, CType::Int32); - assert_eq!( - rendered(abi), - ["int32_t a", "int32_t b", "weaveffi_error* out_err"] - ); - - let model = build(vec![module("net")], "acme"); - assert_eq!(model.prefix(), "acme"); - } - - #[test] - fn async_function_has_launch_and_callback() { - let m = Module { - functions: vec![Function { - cancellable: true, - r#async: true, - ..func( - "fetch", - vec![param("id", TypeRef::Prim(Prim::I64))], - Some(TypeRef::Prim(Prim::String)), - ) - }], - ..module("net") - }; - let model = build(vec![m], "weaveffi"); - let CallShape::Async(a) = &model.modules[0].functions[0].shape else { - panic!("expected async") - }; - assert_eq!(a.launch.symbol, "weaveffi_net_fetch"); - assert_eq!(a.callback_type, "weaveffi_net_fetch_callback"); - let names: Vec<&str> = a.launch.params.iter().map(|p| p.name.as_str()).collect(); - assert_eq!(names, ["id", "cancel_token", "callback", "context"]); - assert_eq!(a.callback_params[0].name, "context"); - assert_eq!(a.callback_params[1].name, "err"); - assert_eq!(a.callback_params[2].name, "result_ptr"); - assert!(model.has_async()); - } - - #[test] - fn iterator_function_has_next_and_destroy() { - let m = Module { - functions: vec![func( - "get_messages", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Prim(Prim::String)))), - )], - ..module("events") - }; - let model = build(vec![m], "weaveffi"); - let CallShape::Iterator(it) = &model.modules[0].functions[0].shape else { - panic!("expected iterator") - }; - assert_eq!(it.iter_tag, "weaveffi_events_GetMessagesIterator"); - assert_eq!(it.launch.symbol, "weaveffi_events_get_messages"); - assert_eq!(it.next.symbol, "weaveffi_events_GetMessagesIterator_next"); - assert_eq!( - it.destroy_symbol, - "weaveffi_events_GetMessagesIterator_destroy" - ); - assert_eq!(it.elem, Ty::Prim(Prim::String)); - assert_eq!(it.next.ret, CType::Int32); - assert_eq!(it.next.params[1].ty.render_c("weaveffi"), "const uint8_t**"); - assert_eq!(it.item_ctype().render_c("weaveffi"), "const uint8_t*"); - assert!(model.has_iterators()); - } - - #[test] - fn user_types_resolve_to_kinds_and_buffers() { - let shared = Module { - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - deprecated: Some("use Person".into()), - fields: vec![ - StructField { - name: "name".into(), - ty: TypeRef::Prim(Prim::String), - doc: None, - }, - StructField { - name: "status".into(), - ty: TypeRef::Named("Status".into()), - doc: None, - }, - StructField { - name: "store".into(), - ty: TypeRef::Optional(Box::new(TypeRef::Named("Store".into()))), - doc: None, - }, - ], - }], - enums: vec![EnumDef { - name: "Status".into(), - doc: None, - deprecated: None, - variants: vec![EnumVariant { - name: "Ok".into(), - value: 0, - doc: None, - fields: vec![], - }], - }], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: None, - deprecated: None, - constructors: vec![func("open", vec![], None)], - methods: vec![func( - "save", - vec![param("contact", TypeRef::Named("Contact".into()))], - Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))), - )], - statics: vec![], - }], - ..module("contacts") - }; - let other = Module { - functions: vec![func( - "status_of", - vec![param("store", TypeRef::Named("Store".into()))], - Some(TypeRef::Named("Status".into())), - )], - ..module("ops") - }; - let model = build(vec![shared, other], "weaveffi"); - let contacts = &model.modules[0]; - let s = &contacts.structs[0]; - assert_eq!(s.deprecated.as_deref(), Some("use Person")); - assert_eq!(s.fields[1].ty, Ty::Enum("Status".into())); - assert_eq!( - s.fields[2].ty, - Ty::Optional(Box::new(Ty::Interface("Store".into()))) - ); - assert_eq!(contacts.enums[0].c_tag, "weaveffi_contacts_Status"); - assert_eq!( - contacts.enums[0].variants[0].c_const, - "weaveffi_contacts_Status_Ok" - ); - - let iface = &contacts.interfaces[0]; - assert_eq!(iface.c_tag, "weaveffi_contacts_Store"); - assert_eq!(iface.clone_symbol, "weaveffi_contacts_Store_clone"); - assert_eq!(iface.destroy_symbol, "weaveffi_contacts_Store_destroy"); - assert_eq!( - iface.constructors[0].ret, - Some(Ty::Interface("Store".into())) - ); - let save = &iface.methods[0]; - assert!(save.has_self); - assert_eq!(save.params[0].ty, Ty::Record("Contact".into())); - let CallShape::Sync(abi) = &save.shape else { - panic!("expected sync") - }; - assert_eq!( - rendered(abi), - [ - "const weaveffi_contacts_Store* self", - "const uint8_t* contact_ptr", - "size_t contact_len", - "size_t* out_len", - "weaveffi_error* out_err" - ] - ); - assert_eq!(abi.ret.render_c("weaveffi"), "const uint8_t*"); - - let ops = &model.modules[1]; - let f = &ops.functions[0]; - assert_eq!(f.params[0].ty, Ty::Interface("Store".into())); - assert_eq!(f.ret, Some(Ty::Enum("Status".into()))); - let CallShape::Sync(abi) = &f.shape else { - panic!("expected sync") - }; - assert_eq!( - rendered(abi), - [ - "const weaveffi_contacts_Store* store", - "weaveffi_error* out_err" - ] - ); - assert_eq!(abi.ret.render_c("weaveffi"), "weaveffi_contacts_Status"); - assert!(model.has_buffers()); - assert!(model.has_interfaces()); - assert_eq!(model.interface("Store").c_tag, "weaveffi_contacts_Store"); - assert_eq!(model.owner("Status").path, "contacts"); - assert_eq!( - model.enumeration("Status").c_tag, - "weaveffi_contacts_Status" - ); - } - - #[test] - fn callback_interfaces_lower_to_vtables() { - let m = Module { - callback_interfaces: vec![CallbackInterfaceDef { - name: "Listener".into(), - doc: Some("Receives messages.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![param("text", TypeRef::Prim(Prim::String))], - None, - ), - func("should_stop", vec![], Some(TypeRef::Prim(Prim::Bool))), - ], - }], - functions: vec![func( - "subscribe", - vec![param("listener", TypeRef::Named("Listener".into()))], - None, - )], - ..module("events") - }; - let model = build(vec![m], "weaveffi"); - let mb = &model.modules[0]; - let cb = &mb.callback_interfaces[0]; - assert_eq!(cb.c_tag, "weaveffi_events_Listener"); - assert_eq!(cb.vtable_tag, "weaveffi_events_Listener_vtable"); - let on_message = &cb.methods[0]; - let slots: Vec = on_message - .abi_params - .iter() - .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) - .collect(); - assert_eq!( - slots, - [ - "void* ctx", - "const uint8_t* text_ptr", - "size_t text_len", - "weaveffi_error* out_err" - ] - ); - assert_eq!(on_message.abi_ret, CType::Void); - assert_eq!(cb.methods[1].abi_ret, CType::Bool); - assert_eq!(model.callback_interface("Listener").c_tag, cb.c_tag); - assert!(model.has_callback_interfaces()); - - let f = &mb.functions[0]; - assert_eq!(f.params[0].ty, Ty::CallbackInterface("Listener".into())); - let CallShape::Sync(abi) = &f.shape else { - panic!("expected sync") - }; - assert_eq!( - rendered(abi), - [ - "void* listener_ctx", - "const weaveffi_events_Listener_vtable* listener_vtable", - "weaveffi_error* out_err" - ] - ); - assert!(!model.has_buffers()); - } - - #[test] - fn nested_modules_flatten_pre_order_with_paths_and_docs() { - let inner = Module { - functions: vec![Function { - doc: Some("Leaf.".into()), - ..func("leaf_fn", vec![], None) - }], - ..module("inner") - }; - let outer = Module { - doc: Some("Outer module.".into()), - functions: vec![func("outer_fn", vec![], None)], - modules: vec![inner], - ..module("outer") - }; - let model = build(vec![outer], "weaveffi"); - let paths: Vec<&str> = model.modules.iter().map(|m| m.path.as_str()).collect(); - assert_eq!(paths, ["outer", "outer_inner"]); - assert_eq!(model.modules[1].dot_path, "outer.inner"); - assert_eq!( - model.modules[1].functions[0].c_base, - "weaveffi_outer_inner_leaf_fn" - ); - assert_eq!(model.modules[0].doc.as_deref(), Some("Outer module.")); - assert_eq!(model.modules[1].doc, None); - } - - #[test] - fn error_domains_are_declared_once_and_inherited_by_lookup() { - let domain = ErrorDomain { - name: "OuterError".into(), - codes: vec![ErrorCode { - name: "Bad".into(), - code: 1, - message: "bad".into(), - doc: None, - fields: vec![], - }], - }; - let outer = Module { - errors: Some(domain), - modules: vec![module("inner")], - ..module("outer") - }; - let model = build(vec![outer, module("other")], "weaveffi"); - let [outer, inner, other] = &model.modules[..] else { - panic!("three modules"); - }; - assert!(outer.errors.is_some() && inner.errors.is_none()); - assert_eq!( - model.error_domain(inner).map(|e| e.c_tag.as_str()), - Some("weaveffi_outer_OuterError") - ); - assert_eq!(model.error_domain(outer), outer.errors.as_ref()); - assert!(model.error_domain(other).is_none()); - } -} diff --git a/crates/weaveffi-model/src/model/symbols.rs b/crates/weaveffi-model/src/model/symbols.rs new file mode 100644 index 00000000..7396cef5 --- /dev/null +++ b/crates/weaveffi-model/src/model/symbols.rs @@ -0,0 +1,323 @@ +//! The typed C symbol table: every identifier the ABI declares, paired with +//! the declaration that owns it. + +use std::fmt; + +use super::{ + contract_check_symbol, contract_symbol, FnBinding, Model, RUNTIME_SYMBOLS, VALUE_CODECS, +}; +use crate::plan::RetPass; + +/// One C identifier the library's ABI declares. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Symbol { + /// The full, prefixed C identifier. + pub name: String, + /// The declaration that owns it. + pub owner: SymbolOwner, +} + +/// The declaration that owns a C [`Symbol`]. +/// +/// Every `path` is a dotted declaration path: the module path, then the +/// declaration name, then a member, variant, or code name (`kv.open`, +/// `kv.Store.get`, `kv.KvError.NotFound`). `Display` renders the +/// human-readable description diagnostics quote. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SymbolOwner { + /// The runtime surface every producer exports. + Runtime, + /// A top-level module's contract table function (`module` is its name). + ContractTable { + /// The top-level module's name. + module: String, + }, + /// The C header's checker for a top-level module's contract. + ContractCheck { + /// The top-level module's name. + module: String, + }, + /// A callable's entry point: the sync symbol, async launcher, or + /// iterator launcher. + Callable { + /// The callable's path. + path: String, + }, + /// An async callable's completion-callback typedef. + AsyncCallback { + /// The callable's path. + path: String, + }, + /// The opaque iterator type an iterator-returning callable launches. + IteratorType { + /// The callable's path. + path: String, + }, + /// That iterator's `_next`. + IteratorNext { + /// The callable's path. + path: String, + }, + /// That iterator's `_destroy`. + IteratorDestroy { + /// The callable's path. + path: String, + }, + /// An interface's opaque type. + Interface { + /// The interface's path. + path: String, + }, + /// An interface's `_clone`. + InterfaceClone { + /// The interface's path. + path: String, + }, + /// An interface's `_destroy`. + InterfaceDestroy { + /// The interface's path. + path: String, + }, + /// An enum's type (a C-style enum's `int32_t` type, or a rich enum's + /// buffer-header struct). + Enum { + /// The enum's path. + path: String, + }, + /// A rich enum's `_Tag` type. + EnumTag { + /// The enum's path. + path: String, + }, + /// An enum variant's constant. + EnumVariant { + /// The variant's path. + path: String, + }, + /// An error domain's code type. + ErrorDomain { + /// The domain's path. + path: String, + }, + /// An error code's constant. + ErrorCode { + /// The code's path. + path: String, + }, + /// The buffer-header struct of an error code's payload. + ErrorPayload { + /// The code's path. + path: String, + }, + /// A record's buffer-header struct. + Record { + /// The record's path. + path: String, + }, + /// A callback interface's vtable type. + Vtable { + /// The callback interface's path. + path: String, + }, + /// One of the [`VALUE_CODECS`] the buffer header declares for a struct. + Codec { + /// The codec (`write`, `read`, `decode`, or `free`). + codec: &'static str, + /// The struct's owner: a [`Record`](Self::Record), + /// [`Enum`](Self::Enum), or [`ErrorPayload`](Self::ErrorPayload). + of: Box, + }, +} + +impl fmt::Display for SymbolOwner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Runtime => f.write_str("the runtime surface"), + Self::ContractTable { module } => { + write!(f, "the contract table of module '{module}'") + } + Self::ContractCheck { module } => { + write!(f, "the contract check of module '{module}'") + } + Self::Callable { path } => write!(f, "function '{path}'"), + Self::AsyncCallback { path } => write!(f, "the completion type of '{path}'"), + Self::IteratorType { path } => write!(f, "the iterator type of '{path}'"), + Self::IteratorNext { path } => write!(f, "the iterator step of '{path}'"), + Self::IteratorDestroy { path } => write!(f, "the iterator destructor of '{path}'"), + Self::Interface { path } => write!(f, "interface '{path}'"), + Self::InterfaceClone { path } => write!(f, "the clone of '{path}'"), + Self::InterfaceDestroy { path } => write!(f, "the destructor of '{path}'"), + Self::Enum { path } => write!(f, "enum '{path}'"), + Self::EnumTag { path } => write!(f, "the tag type of enum '{path}'"), + Self::EnumVariant { path } => write!(f, "enum variant '{path}'"), + Self::ErrorDomain { path } => write!(f, "error domain '{path}'"), + Self::ErrorCode { path } => write!(f, "error code '{path}'"), + Self::ErrorPayload { path } => { + write!(f, "the payload struct of error code '{path}'") + } + Self::Record { path } => write!(f, "record '{path}'"), + Self::Vtable { path } => write!(f, "the vtable of callback interface '{path}'"), + Self::Codec { codec, of } => write!(f, "the value-buffer codec '{codec}' of {of}"), + } + } +} + +/// Build the symbol table of `model` (see [`Model::c_symbols`]). +pub(super) fn collect(model: &Model) -> Vec { + let p = model.prefix(); + let mut out: Vec = RUNTIME_SYMBOLS + .iter() + .map(|s| Symbol { + name: format!("{p}_{s}"), + owner: SymbolOwner::Runtime, + }) + .collect(); + let push = |out: &mut Vec, name: &str, owner: SymbolOwner| { + out.push(Symbol { + name: name.to_string(), + owner, + }); + }; + for m in model.roots() { + push( + &mut out, + &contract_symbol(p, &m.name), + SymbolOwner::ContractTable { + module: m.name.clone(), + }, + ); + push( + &mut out, + &contract_check_symbol(p, &m.name), + SymbolOwner::ContractCheck { + module: m.name.clone(), + }, + ); + } + let callable = |out: &mut Vec, f: &FnBinding, owner: &str| { + let path = format!("{owner}.{}", f.name); + out.push(Symbol { + name: f.abi.symbol.clone(), + owner: SymbolOwner::Callable { path: path.clone() }, + }); + if let Some(a) = f.async_binding() { + out.push(Symbol { + name: a.callback_type.clone(), + owner: SymbolOwner::AsyncCallback { path: path.clone() }, + }); + } + if let RetPass::Iterator(it) = &f.ret_pass { + out.push(Symbol { + name: it.iter_tag.clone(), + owner: SymbolOwner::IteratorType { path: path.clone() }, + }); + out.push(Symbol { + name: it.next.symbol.clone(), + owner: SymbolOwner::IteratorNext { path: path.clone() }, + }); + out.push(Symbol { + name: it.destroy_symbol.clone(), + owner: SymbolOwner::IteratorDestroy { path }, + }); + } + }; + let codecs = |out: &mut Vec, tag: &str, of: &SymbolOwner| { + for codec in VALUE_CODECS { + out.push(Symbol { + name: format!("{tag}_{codec}"), + owner: SymbolOwner::Codec { + codec, + of: Box::new(of.clone()), + }, + }); + } + }; + for m in &model.modules { + let dot = &m.dot_path; + for e in &m.errors { + let path = format!("{dot}.{}", e.name); + push( + &mut out, + &e.c_tag, + SymbolOwner::ErrorDomain { path: path.clone() }, + ); + for c in &e.codes { + let code = format!("{path}.{}", c.name); + push( + &mut out, + &c.c_const, + SymbolOwner::ErrorCode { path: code.clone() }, + ); + if !c.fields.is_empty() { + let owner = SymbolOwner::ErrorPayload { path: code }; + let tag = c.payload_tag(); + codecs(&mut out, &tag, &owner); + push(&mut out, &tag, owner); + } + } + } + for e in &m.enums { + let path = format!("{dot}.{}", e.name); + let owner = SymbolOwner::Enum { path: path.clone() }; + push(&mut out, &e.c_tag, owner.clone()); + if e.rich { + push( + &mut out, + &format!("{}_Tag", e.c_tag), + SymbolOwner::EnumTag { path: path.clone() }, + ); + codecs(&mut out, &e.c_tag, &owner); + } + for v in &e.variants { + push( + &mut out, + &v.c_const, + SymbolOwner::EnumVariant { + path: format!("{path}.{}", v.name), + }, + ); + } + } + for s in &m.structs { + let owner = SymbolOwner::Record { + path: format!("{dot}.{}", s.name), + }; + codecs(&mut out, &s.c_tag, &owner); + push(&mut out, &s.c_tag, owner); + } + for c in &m.callback_interfaces { + push( + &mut out, + &c.vtable_tag, + SymbolOwner::Vtable { + path: format!("{dot}.{}", c.name), + }, + ); + } + for i in &m.interfaces { + let path = format!("{dot}.{}", i.name); + push( + &mut out, + &i.c_tag, + SymbolOwner::Interface { path: path.clone() }, + ); + push( + &mut out, + &i.clone_symbol, + SymbolOwner::InterfaceClone { path: path.clone() }, + ); + push( + &mut out, + &i.destroy_symbol, + SymbolOwner::InterfaceDestroy { path: path.clone() }, + ); + for f in i.members() { + callable(&mut out, f, &path); + } + } + for f in &m.functions { + callable(&mut out, f, dot); + } + } + out +} diff --git a/crates/weaveffi-model/src/model/tests.rs b/crates/weaveffi-model/src/model/tests.rs new file mode 100644 index 00000000..4e4533f3 --- /dev/null +++ b/crates/weaveffi-model/src/model/tests.rs @@ -0,0 +1,687 @@ +//! Model construction tests: symbols, signatures, and the passing contracts +//! stored on every binding, built from short YAML documents through the +//! validator (the only way to obtain a model). + +use super::*; +use crate::ir::{Api, CURRENT_SCHEMA_VERSION}; +use crate::parse::parse_api_str; +use crate::ty::Prim; +use crate::validate::{validate, validate_scoped, Options}; + +fn model(body: &str) -> Model { + model_with_prefix(body, "weaveffi") +} + +fn model_with_prefix(body: &str, prefix: &str) -> Model { + let doc = format!("version: \"{CURRENT_SCHEMA_VERSION}\"\n{body}"); + let api = parse_api_str(&doc, "yaml").expect("fixture parses"); + match validate(&api, &Identity::named(prefix), None) { + Ok(model) => model, + Err(e) => panic!("fixture is invalid: {e}"), + } +} + +fn rendered(params: &[AbiParam]) -> Vec { + params + .iter() + .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) + .collect() +} + +fn slots(params: Vec<&AbiParam>) -> Vec { + params + .into_iter() + .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) + .collect() +} + +fn function<'a>(model: &'a Model, name: &str) -> &'a FnBinding { + model + .callables() + .map(|(_, f)| f) + .find(|f| f.name == name) + .unwrap_or_else(|| panic!("no callable {name}")) +} + +#[test] +fn sync_function_symbol_and_sig() { + let model = model( + r#" +modules: + - name: math + functions: + - { name: add, params: [{ name: a, type: i32 }, { name: b, type: i32 }], return: i32 } +"#, + ); + let f = &model.modules[0].functions[0]; + assert_eq!(f.shape, CallShape::Sync); + assert_eq!(f.abi.symbol, "weaveffi_math_add"); + assert_eq!(f.abi.ret, CType::Int32); + assert_eq!(f.ret_pass, RetPass::Direct); + assert_eq!(f.error, ErrorStrategy::Trap); + assert!(!f.has_self()); + assert_eq!( + rendered(&f.abi.params), + ["int32_t a", "int32_t b", "weaveffi_error* out_err"] + ); + assert!(matches!(&f.params[0].pass, ArgPass::Direct { slot } if slot.name == "a")); + + let model = model_with_prefix("modules: [{ name: net, functions: [{ name: f }] }]", "acme"); + assert_eq!(model.prefix(), "acme"); +} + +/// One document exercising OptDirect and Slice in every position. +const SHAPES: &str = r#" +modules: + - name: s + enums: + - { name: Level, variants: [{ name: Low, value: 0 }, { name: High, value: 1 }] } + callback_interfaces: + - name: Sink + methods: + - { name: limit, params: [{ name: n, type: "u32?" }], return: "f64?" } + - { name: samples, params: [{ name: xs, type: "[f32]" }], return: "[i16]" } + - { name: level, params: [{ name: l, type: "Level?" }], return: "Level?" } + functions: + - name: clamp + params: [{ name: x, type: "i32?" }, { name: on, type: "bool?" }, { name: lvl, type: "Level?" }] + return: "i64?" + - name: scale + params: [{ name: xs, type: "[f64]" }, { name: ids, type: "[u64]" }] + return: "[i32]" + - { name: later, params: [], return: "i64?", async: true } + - { name: later_xs, params: [], return: "[f32]", async: true, cancellable: true } + - { name: maybe_ints, params: [], return: "iter" } + - { name: chunks, params: [], return: "iter<[i32]>" } + - { name: feed, params: [{ name: sink, type: Sink }] } +"#; + +#[test] +fn opt_direct_and_slice_params() { + let model = model(SHAPES); + let clamp = function(&model, "clamp"); + assert_eq!( + rendered(&clamp.abi.params), + [ + "bool has_x", + "int32_t x", + "bool has_on", + "bool on", + "bool has_lvl", + "weaveffi_s_Level lvl", + "int64_t* out_value", + "weaveffi_error* out_err" + ] + ); + let ArgPass::OptDirect { has, value, inner } = &clamp.params[2].pass else { + panic!("expected OptDirect: {:?}", clamp.params[2].pass); + }; + assert_eq!((has.name.as_str(), value.name.as_str()), ("has_lvl", "lvl")); + assert_eq!(inner, &Ty::Enum("Level".into())); + + let scale = function(&model, "scale"); + assert_eq!( + rendered(&scale.abi.params), + [ + "const double* xs_ptr", + "size_t xs_len", + "const uint64_t* ids_ptr", + "size_t ids_len", + "size_t* out_len", + "weaveffi_error* out_err" + ] + ); + assert!(matches!( + &scale.params[1].pass, + ArgPass::Slice { elem: Prim::U64, ptr, len } if ptr.name == "ids_ptr" && len.name == "ids_len" + )); +} + +#[test] +fn opt_direct_and_slice_returns() { + let model = model(SHAPES); + let clamp = function(&model, "clamp"); + assert_eq!(clamp.abi.ret, CType::Bool); + assert!( + matches!(&clamp.ret_pass, RetPass::OptDirect { out_value } if out_value.name == "out_value") + ); + + let scale = function(&model, "scale"); + assert_eq!(scale.abi.ret.render_c("weaveffi"), "int32_t*"); + assert!(matches!( + &scale.ret_pass, + RetPass::Slice { elem: Prim::I32, out_len } if out_len.name == "out_len" + )); +} + +#[test] +fn opt_direct_and_slice_async_results() { + let model = model(SHAPES); + let later = function(&model, "later"); + let a = later.async_binding().expect("async"); + assert_eq!(later.ret_pass, RetPass::Void); + assert_eq!(later.abi.ret, CType::Void); + assert_eq!( + rendered(&a.callback_params), + [ + "void* context", + "weaveffi_error* err", + "bool has_result", + "int64_t result" + ] + ); + assert!( + matches!(&a.result, ResultPass::OptDirect { has, value } if has.name == "has_result" && value.name == "result") + ); + assert!(!a.cancellable() && !later.cancellable()); + + let xs = function(&model, "later_xs"); + let a = xs.async_binding().expect("async"); + assert_eq!( + rendered(&a.callback_params), + [ + "void* context", + "weaveffi_error* err", + "const float* result_ptr", + "size_t result_len" + ] + ); + assert!(matches!( + &a.result, + ResultPass::Slice { + elem: Prim::F32, + .. + } + )); + assert_eq!(a.callback_type, "weaveffi_s_later_xs_callback"); + assert!(xs.cancellable()); + assert_eq!( + rendered(&xs.abi.params), + [ + "weaveffi_cancel_token* cancel_token", + "weaveffi_s_later_xs_callback callback", + "void* context" + ] + ); +} + +#[test] +fn opt_direct_and_slice_iterator_items() { + let model = model(SHAPES); + let ints = function(&model, "maybe_ints"); + let it = ints.iterator().expect("iterator"); + assert_eq!(it.elem, Ty::Optional(Box::new(Ty::Prim(Prim::I32)))); + assert_eq!( + rendered(&it.next.params), + [ + "weaveffi_s_MaybeIntsIterator* iter", + "bool* out_has_item", + "int32_t* out_item", + "weaveffi_error* out_err" + ] + ); + assert!( + matches!(&it.item, ItemPass::OptDirect { out_has, out_item } if out_has.name == "out_has_item" && out_item.name == "out_item") + ); + + let chunks = function(&model, "chunks"); + let it = chunks.iterator().expect("iterator"); + assert_eq!( + slots(it.item.slots()), + ["int32_t** out_item", "size_t* out_len"] + ); + assert!(matches!( + &it.item, + ItemPass::Slice { + elem: Prim::I32, + .. + } + )); + assert_eq!( + chunks.abi.ret.render_c("weaveffi"), + "weaveffi_s_ChunksIterator*" + ); + assert_eq!( + chunks.ret, + Some(RetTy::Iterator(Ty::List(Box::new(Ty::Prim(Prim::I32))))) + ); +} + +#[test] +fn opt_direct_and_slice_callback_methods() { + let model = model(SHAPES); + let sink = model.callback_interface("Sink"); + let limit = &sink.methods[0]; + assert_eq!(limit.abi.symbol, "limit"); + assert_eq!(limit.abi.ret, CType::Bool); + assert_eq!( + rendered(&limit.abi.params), + [ + "void* ctx", + "bool has_n", + "uint32_t n", + "double* out_value", + "weaveffi_error* out_err" + ] + ); + assert!(matches!(&limit.params[0].pass, ArgPass::OptDirect { .. })); + assert!( + matches!(&limit.ret_pass, CallbackRetPass::OptDirect { out_value } if out_value.name == "out_value") + ); + + let samples = &sink.methods[1]; + assert_eq!(samples.abi.ret, CType::Void); + assert_eq!( + rendered(&samples.abi.params), + [ + "void* ctx", + "const float* xs_ptr", + "size_t xs_len", + "int16_t** out_ptr", + "size_t* out_len", + "weaveffi_error* out_err" + ] + ); + assert!(matches!( + &samples.params[0].pass, + ArgPass::Slice { + elem: Prim::F32, + .. + } + )); + assert!(matches!( + &samples.ret_pass, + CallbackRetPass::Slice { elem: Prim::I16, out_ptr, out_len } + if out_ptr.name == "out_ptr" && out_len.name == "out_len" + )); + + let level = &sink.methods[2]; + assert_eq!( + rendered(&level.abi.params), + [ + "void* ctx", + "bool has_l", + "weaveffi_s_Level l", + "weaveffi_s_Level* out_value", + "weaveffi_error* out_err" + ] + ); + // OptDirect and Slice values never need a buffer. + assert!(!model.has_buffers()); +} + +#[test] +fn user_types_resolve_to_kinds_and_contracts() { + let model = model( + r#" +modules: + - name: contacts + structs: + - name: Contact + deprecated: use Person + fields: + - { name: name, type: string } + - { name: status, type: Status } + - { name: store, type: "Store?" } + enums: + - { name: Status, variants: [{ name: Ok, value: 0 }] } + interfaces: + - name: Store + constructors: [{ name: open }] + methods: + - { name: save, params: [{ name: contact, type: Contact }], return: "[Contact]" } + - name: ops + functions: + - { name: status_of, params: [{ name: store, type: Store }], return: Status } + - { name: find, params: [], return: "Store?" } +"#, + ); + let contacts = &model.modules[0]; + let s = &contacts.structs[0]; + assert_eq!(s.c_tag, "weaveffi_contacts_Contact"); + assert_eq!(s.deprecated.as_deref(), Some("use Person")); + assert_eq!(s.fields[1].ty, Ty::Enum("Status".into())); + assert_eq!( + s.fields[2].ty, + Ty::Optional(Box::new(Ty::Interface("Store".into()))) + ); + assert_eq!(contacts.enums[0].c_tag, "weaveffi_contacts_Status"); + assert_eq!( + contacts.enums[0].variants[0].c_const, + "weaveffi_contacts_Status_Ok" + ); + + let iface = &contacts.interfaces[0]; + assert_eq!(iface.c_tag, "weaveffi_contacts_Store"); + assert_eq!(iface.clone_symbol, "weaveffi_contacts_Store_clone"); + assert_eq!(iface.destroy_symbol, "weaveffi_contacts_Store_destroy"); + let open = &iface.constructors[0]; + assert_eq!(open.ret, Some(RetTy::Value(Ty::Interface("Store".into())))); + assert_eq!( + open.ret_pass, + RetPass::Object { + nullable: false, + interface: "Store".into(), + destroy_symbol: "weaveffi_contacts_Store_destroy".into(), + } + ); + assert_eq!(open.abi.symbol, "weaveffi_contacts_Store_open"); + let save = &iface.methods[0]; + assert!(save.has_self()); + assert_eq!( + save.receiver.as_ref().map(|r| r.ty.render_c("weaveffi")), + Some("const weaveffi_contacts_Store*".into()) + ); + assert_eq!( + save.params[0].ty, + ParamTy::Value(Ty::Record("Contact".into())) + ); + assert_eq!( + rendered(&save.abi.params), + [ + "const weaveffi_contacts_Store* self", + "const uint8_t* contact_ptr", + "size_t contact_len", + "size_t* out_len", + "weaveffi_error* out_err" + ] + ); + assert_eq!(save.abi.ret.render_c("weaveffi"), "const uint8_t*"); + assert!(matches!(&save.ret_pass, RetPass::Buffer { .. })); + + let f = function(&model, "status_of"); + assert!( + matches!(&f.params[0].pass, ArgPass::Object { nullable: false, interface, .. } if interface == "Store") + ); + assert_eq!(f.ret, Some(RetTy::Value(Ty::Enum("Status".into())))); + assert_eq!( + rendered(&f.abi.params), + [ + "const weaveffi_contacts_Store* store", + "weaveffi_error* out_err" + ] + ); + assert_eq!(f.abi.ret.render_c("weaveffi"), "weaveffi_contacts_Status"); + assert!(matches!( + &function(&model, "find").ret_pass, + RetPass::Object { nullable: true, .. } + )); + assert!(model.has_buffers()); + assert!(model.has_interfaces()); + assert_eq!(model.interface("Store").c_tag, "weaveffi_contacts_Store"); + assert_eq!(model.record("Contact").c_tag, "weaveffi_contacts_Contact"); + assert_eq!(model.owner("Status").path, "contacts"); + assert_eq!( + model.enumeration("Status").c_tag, + "weaveffi_contacts_Status" + ); +} + +#[test] +fn callback_interfaces_lower_to_vtables() { + let model = model( + r#" +modules: + - name: events + interfaces: [{ name: Store, methods: [{ name: get }] }] + callback_interfaces: + - name: Listener + doc: Receives messages. + methods: + - { name: on_message, params: [{ name: text, type: string }, { name: from, type: Store }] } + - { name: should_stop, return: bool, throws: any } + - { name: store, return: "Store?" } + - { name: label, return: string } + functions: + - { name: subscribe, params: [{ name: listener, type: Listener }] } + - { name: maybe_subscribe, params: [{ name: listener, type: "Listener?" }] } +"#, + ); + let mb = &model.modules[0]; + let cb = &mb.callback_interfaces[0]; + assert_eq!(cb.c_tag, "weaveffi_events_Listener"); + assert_eq!(cb.vtable_tag, "weaveffi_events_Listener_vtable"); + let on_message = &cb.methods[0]; + assert_eq!( + rendered(&on_message.abi.params), + [ + "void* ctx", + "const uint8_t* text_ptr", + "size_t text_len", + "weaveffi_events_Store* from", + "weaveffi_error* out_err" + ] + ); + assert_eq!(on_message.abi.ret, CType::Void); + assert_eq!(on_message.error, ErrorStrategy::Trap); + assert_eq!(cb.methods[1].abi.ret, CType::Bool); + assert_eq!(cb.methods[1].error, ErrorStrategy::Untyped); + assert!(matches!( + &cb.methods[2].ret_pass, + CallbackRetPass::Object { nullable: true, clone_symbol, .. } if clone_symbol == "weaveffi_events_Store_clone" + )); + assert_eq!( + slots(cb.methods[3].ret_pass.out_slots()), + ["uint8_t** out_ptr", "size_t* out_len"] + ); + assert_eq!(model.callback_interface("Listener").c_tag, cb.c_tag); + assert!(model.has_callback_interfaces()); + + let f = &mb.functions[0]; + assert_eq!( + f.params[0].ty, + ParamTy::Callback { + name: "Listener".into(), + nullable: false + } + ); + assert_eq!( + rendered(&f.abi.params), + [ + "void* listener_ctx", + "const weaveffi_events_Listener_vtable* listener_vtable", + "weaveffi_error* out_err" + ] + ); + assert!(matches!( + &mb.functions[1].params[0].pass, + ArgPass::Callback { nullable: true, interface, .. } if interface == "Listener" + )); + assert!(!model.has_buffers()); +} + +#[test] +fn nested_modules_link_parents_and_children() { + let model = model( + r#" +modules: + - name: outer + doc: Outer module. + functions: [{ name: outer_fn }] + modules: + - name: inner + functions: [{ name: leaf_fn, doc: Leaf. }] + modules: + - name: deepest + - name: sibling + - name: other +"#, + ); + let paths: Vec<&str> = model.modules.iter().map(|m| m.path.as_str()).collect(); + assert_eq!( + paths, + [ + "outer", + "outer_inner", + "outer_inner_deepest", + "outer_sibling", + "other" + ] + ); + for (i, m) in model.modules.iter().enumerate() { + assert_eq!(m.index, i); + } + let roots: Vec<&str> = model.roots().map(|m| m.name.as_str()).collect(); + assert_eq!(roots, ["outer", "other"]); + let outer = &model.modules[0]; + assert_eq!(outer.children, [1, 3]); + let kids: Vec<&str> = model.children(outer).map(|m| m.name.as_str()).collect(); + assert_eq!(kids, ["inner", "sibling"]); + assert_eq!(model.modules[2].parent, Some(1)); + assert_eq!( + model.parent(&model.modules[2]).map(|m| m.name.as_str()), + Some("inner") + ); + assert!(model.parent(outer).is_none()); + assert_eq!(model.modules[1].dot_path, "outer.inner"); + assert_eq!( + model.modules[1].functions[0].abi.symbol, + "weaveffi_outer_inner_leaf_fn" + ); + assert_eq!(outer.doc.as_deref(), Some("Outer module.")); + assert_eq!(model.modules[1].doc, None); +} + +#[test] +fn error_domains_are_global_and_resolved_by_name() { + let model = model( + r#" +modules: + - name: outer + errors: + - name: OuterErrors + codes: [{ name: Bad, code: 1, message: bad }] + - name: IoError + codes: [{ name: Disk, code: 7, message: disk, fields: [{ name: path, type: string }] }] + modules: + - name: inner + functions: + - { name: fails, throws: OuterErrors } + - { name: reads, throws: IoError } + - { name: anything, throws: any } + - { name: never } + - name: other + functions: [{ name: elsewhere, throws: IoError }] +"#, + ); + let outer = &model.modules[0]; + assert_eq!(outer.errors.len(), 2); + let domain = &outer.errors[0]; + assert_eq!(domain.c_tag, "weaveffi_outer_OuterErrors"); + assert_eq!(domain.type_name, "OuterError"); + assert_eq!(domain.module, "outer"); + assert_eq!(domain.owner_path, "outer"); + assert_eq!(domain.codes[0].c_const, "weaveffi_outer_OuterErrors_Bad"); + let io = model.error_domain("IoError"); + assert_eq!(io.type_name, "IoError"); + assert_eq!( + io.codes[0].payload_tag(), + "weaveffi_outer_IoError_Disk_payload" + ); + assert_eq!( + function(&model, "fails").error, + ErrorStrategy::Domain("OuterErrors".into()) + ); + assert_eq!(model.error_domain("OuterErrors"), domain); + assert_eq!( + function(&model, "elsewhere").error.domain(), + Some("IoError") + ); + assert_eq!(function(&model, "anything").error, ErrorStrategy::Untyped); + assert!(function(&model, "anything").error.throws()); + assert_eq!(function(&model, "never").error, ErrorStrategy::Trap); + assert!(!function(&model, "never").error.throws()); + assert_eq!(model.error_domains().count(), 2); + assert_eq!(model.owner("IoError").name, "outer"); + // An error payload is a value buffer. + assert!(model.has_buffers()); +} + +#[test] +fn symbol_table_owners_are_typed() { + let model = model( + r#" +modules: + - name: kv + errors: [{ name: KvError, codes: [{ name: Gone, code: 1, message: gone, fields: [{ name: k, type: string }] }] }] + structs: [{ name: Entry, fields: [{ name: k, type: string }] }] + interfaces: [{ name: Store, methods: [{ name: keys, return: "iter" }] }] + functions: [{ name: fetch, async: true, return: i32 }] +"#, + ); + let symbols = model.c_symbols(); + let owner = |name: &str| { + symbols + .iter() + .find(|s| s.name == name) + .map(|s| s.owner.clone()) + .unwrap_or_else(|| panic!("no symbol {name}")) + }; + assert_eq!(owner("weaveffi_alloc"), SymbolOwner::Runtime); + assert_eq!( + owner("weaveffi_kv_contract"), + SymbolOwner::ContractTable { + module: "kv".into() + } + ); + assert_eq!( + owner("weaveffi_kv_fetch_callback"), + SymbolOwner::AsyncCallback { + path: "kv.fetch".into() + } + ); + assert_eq!( + owner("weaveffi_kv_Store_KeysIterator_next"), + SymbolOwner::IteratorNext { + path: "kv.Store.keys".into() + } + ); + assert_eq!( + owner("weaveffi_kv_KvError_Gone_payload"), + SymbolOwner::ErrorPayload { + path: "kv.KvError.Gone".into() + } + ); + let codec = owner("weaveffi_kv_Entry_read"); + assert_eq!( + codec, + SymbolOwner::Codec { + codec: "read", + of: Box::new(SymbolOwner::Record { + path: "kv.Entry".into() + }) + } + ); + assert_eq!( + codec.to_string(), + "the value-buffer codec 'read' of record 'kv.Entry'" + ); + assert_eq!( + owner("weaveffi_kv_Store_destroy").to_string(), + "the destructor of 'kv.Store'" + ); +} + +#[test] +fn foreign_names_are_explicit_and_recorded() { + let doc = format!( + "version: \"{CURRENT_SCHEMA_VERSION}\"\nmodules:\n - name: orders\n functions:\n - {{ name: total, params: [{{ name: p, type: Product }}], return: \"[Product]\" }}\n" + ); + let api: Api = parse_api_str(&doc, "yaml").unwrap(); + let identity = Identity::named("shop"); + // Without the option, the name is unknown. + assert!(validate_scoped(&api, &identity, &Options::default()).is_err()); + let options = Options { + foreign: api.undeclared_type_names(), + }; + let model = validate_scoped(&api, &identity, &options).unwrap(); + assert!(model.types.is_foreign("Product")); + assert_eq!(model.types.foreign().collect::>(), ["Product"]); + assert!(model.types.get("Product").is_none()); + let total = &model.modules[0].functions[0]; + assert_eq!( + total.params[0].ty, + ParamTy::Value(Ty::Record("Product".into())) + ); + assert!(matches!(total.params[0].pass, ArgPass::Buffer { .. })); +} diff --git a/crates/weaveffi-model/src/parse.rs b/crates/weaveffi-model/src/parse.rs index 3c1c7b86..20758efd 100644 --- a/crates/weaveffi-model/src/parse.rs +++ b/crates/weaveffi-model/src/parse.rs @@ -1,5 +1,4 @@ -//! Multi-format IDL parsing: turn YAML, JSON, or TOML source text into an -//! [`Api`]. +//! Multi-format IDL parsing: turn YAML or JSON source text into an [`Api`]. //! //! [`parse_api_str`] is the entry point. On failure it returns a [`ParseError`] //! that carries the source text and, where available, a `miette` span, so the @@ -15,10 +14,10 @@ use miette::{Diagnostic, SourceSpan}; /// span into the original source so the CLI can render a caret-annotated error. #[derive(Debug, thiserror::Error, Diagnostic)] pub enum ParseError { - /// The requested format wasn't one of `yaml`, `yml`, `json`, or `toml`. - /// Carries the unrecognized format string. + /// The requested format wasn't one of `yaml`, `yml`, or `json`. Carries + /// the unrecognized format string. #[error("unsupported format: {0}")] - #[diagnostic(help("supported formats are 'yaml', 'yml', 'json', and 'toml'"))] + #[diagnostic(help("supported formats are 'yaml', 'yml', and 'json'"))] UnsupportedFormat(String), /// The YAML deserializer rejected the document. #[error("YAML parse error at line {line}, column {column}: {message}")] @@ -39,21 +38,6 @@ pub enum ParseError { #[label("here")] span: Option, }, - /// The TOML deserializer rejected the document. - #[error("TOML parse error: {message}")] - #[diagnostic(help( - "check TOML syntax: keys, table headers, and that values use the correct types" - ))] - Toml { - /// Message reported by the underlying deserializer. - message: String, - /// Full source text, retained so the diagnostic can render a snippet. - #[source_code] - src: String, - /// Byte span of the offending location within `src`, when known. - #[label("here")] - span: Option, - }, /// The JSON deserializer rejected the document. #[error("JSON parse error at line {line}, column {column}: {message}")] #[diagnostic(help( @@ -102,23 +86,28 @@ pub fn line_col_to_offset(src: &str, line: usize, col: usize) -> usize { /// Parse IDL source text in the given format into an [`Api`]. /// -/// `format` selects the deserializer: `yaml` or `yml` for YAML, `json` for -/// JSON, and `toml` for TOML. On failure the returned [`ParseError`] captures +/// `format` selects the deserializer: `yaml` or `yml` for YAML and `json` +/// for JSON. On failure the returned [`ParseError`] captures /// the source text and, when the deserializer reports one, a span for a rich /// diagnostic. /// /// # Errors /// /// Returns [`ParseError::UnsupportedFormat`] when `format` isn't a recognized -/// format string, or the matching [`Yaml`](ParseError::Yaml), -/// [`Json`](ParseError::Json), or [`Toml`](ParseError::Toml) variant when the -/// source text is malformed or doesn't match the schema. +/// format string, or the matching [`Yaml`](ParseError::Yaml) or +/// [`Json`](ParseError::Json) variant when the source text is malformed or +/// doesn't match the schema. pub fn parse_api_str(s: &str, format: &str) -> Result { match format { - "yaml" | "yml" => serde_yaml::from_str(s).map_err(|e| { + "yaml" | "yml" => serde_saphyr::from_str(s).map_err(|e| { let (line, column) = e .location() - .map(|m| (m.line(), m.column())) + .map(|m| { + ( + usize::try_from(m.line()).unwrap_or(0), + usize::try_from(m.column()).unwrap_or(0), + ) + }) .unwrap_or((0, 0)); let span = if line > 0 && column > 0 { Some(SourceSpan::new( @@ -155,14 +144,6 @@ pub fn parse_api_str(s: &str, format: &str) -> Result { span, } }), - "toml" => toml::from_str(s).map_err(|e| { - let span = e.span().map(|r| SourceSpan::new(r.start.into(), r.len())); - ParseError::Toml { - message: e.to_string(), - src: s.to_string(), - span, - } - }), other => Err(ParseError::UnsupportedFormat(other.to_string())), } } @@ -195,7 +176,7 @@ mod tests { ], returns: Some(TypeRef::Prim(Prim::I32)), doc: Some("Adds two numbers".to_string()), - throws: false, + throws: None, r#async: false, cancellable: false, deprecated: None, @@ -204,7 +185,7 @@ mod tests { callback_interfaces: vec![], structs: vec![], enums: vec![], - errors: None, + errors: vec![], modules: vec![], }], } @@ -213,7 +194,7 @@ mod tests { #[test] fn every_format_parses_the_same_document() { let yaml = r#" -version: "0.11.0" +version: "0.12.0" modules: - name: math functions: @@ -227,7 +208,7 @@ modules: doc: "Adds two numbers" "#; let json = r#"{ - "version": "0.11.0", + "version": "0.12.0", "modules": [{ "name": "math", "functions": [{ @@ -241,29 +222,9 @@ modules: }] }] }"#; - let toml_str = r#" -version = "0.11.0" - -[[modules]] -name = "math" - -[[modules.functions]] -name = "add" -return = "i32" -doc = "Adds two numbers" - -[[modules.functions.params]] -name = "a" -type = "i32" - -[[modules.functions.params]] -name = "b" -type = "i32" -"#; assert_eq!(parse_api_str(yaml, "yaml").unwrap(), expected_api()); assert_eq!(parse_api_str(yaml, "yml").unwrap(), expected_api()); assert_eq!(parse_api_str(json, "json").unwrap(), expected_api()); - assert_eq!(parse_api_str(toml_str, "toml").unwrap(), expected_api()); } #[test] @@ -272,6 +233,10 @@ type = "i32" parse_api_str("", "xml"), Err(ParseError::UnsupportedFormat(f)) if f == "xml" )); + assert!(matches!( + parse_api_str("version = \"0.12.0\"", "toml"), + Err(ParseError::UnsupportedFormat(f)) if f == "toml" + )); } #[test] @@ -286,7 +251,7 @@ type = "i32" #[test] fn parse_errors_carry_spans() { - let yaml = "version: \"0.11.0\"\nmodules:\n - name: [oops\n"; + let yaml = "version: \"0.12.0\"\nmodules:\n - name: [oops\n"; match parse_api_str(yaml, "yaml").unwrap_err() { ParseError::Yaml { line, span, .. } => { assert!(line > 0); @@ -298,15 +263,11 @@ type = "i32" ParseError::Json { line, column, .. } => assert!(line > 0 && column > 0), other => panic!("expected JSON error, got {other:?}"), } - match parse_api_str("version = ", "toml").unwrap_err() { - ParseError::Toml { span, .. } => assert!(span.is_some()), - other => panic!("expected TOML error, got {other:?}"), - } } #[test] fn unknown_type_syntax_is_a_parse_error() { - let yaml = "version: \"0.11.0\"\nmodules:\n - name: m\n functions:\n - name: f\n params: [{ name: x, type: \"{string}\" }]\n"; + let yaml = "version: \"0.12.0\"\nmodules:\n - name: m\n functions:\n - name: f\n params: [{ name: x, type: \"{string}\" }]\n"; let err = parse_api_str(yaml, "yaml").unwrap_err(); assert!(err.to_string().contains("map type missing"), "{err}"); } diff --git a/crates/weaveffi-model/src/pkg.rs b/crates/weaveffi-model/src/pkg.rs index bce2264c..69ddffa6 100644 --- a/crates/weaveffi-model/src/pkg.rs +++ b/crates/weaveffi-model/src/pkg.rs @@ -281,12 +281,14 @@ mod tests { } #[test] - fn package_table_parses_from_toml() { + fn package_table_deserializes_strictly() { + // The CLI reads this table from `weaveffi.toml`; any serde format + // exercises the same derive. let pkg: Package = - toml::from_str("name = \"kv\"\nversion = \"2.0.0\"\nauthors = [\"A\", \"B\"]\n") + serde_json::from_str(r#"{"name": "kv", "version": "2.0.0", "authors": ["A", "B"]}"#) .unwrap(); assert_eq!(pkg.name.as_deref(), Some("kv")); assert_eq!(pkg.authors.len(), 2); - assert!(toml::from_str::("bogus = 1").is_err()); + assert!(serde_json::from_str::(r#"{"bogus": 1}"#).is_err()); } } diff --git a/crates/weaveffi-model/src/plan.rs b/crates/weaveffi-model/src/plan.rs index c1c3971e..8f029584 100644 --- a/crates/weaveffi-model/src/plan.rs +++ b/crates/weaveffi-model/src/plan.rs @@ -1,573 +1,581 @@ -//! The **marshalling plan**: the language-neutral calling contracts every -//! backend renders, stated once. +//! The **marshalling plan**: the language-neutral passing contracts every +//! backend renders, computed once and stored on the model. //! //! The [`crate::model`] layer answers *which symbols exist and what their C -//! signatures are*. This module answers the questions one level up, the ones -//! the eleven generators used to answer independently (and inconsistently): +//! signatures are*. This module holds the answers one level up, which the +//! model precomputes for every binding so no backend re-derives them: //! -//! * **Passing** ([`ArgPass`], [`RetPass`]): how each argument crosses into -//! its ABI slots (by value, pinned string/bytes, serialized value buffer, -//! borrowed object pointer, or callback context plus vtable) and what the -//! wrapper does with the result (use, copy, decode, or adopt) and which -//! release it owes (`{prefix}_free_bytes` or the interface's `_destroy`). -//! * **Errors** ([`ErrorStrategy`]): when a call reports through `out_err`, -//! is that a typed domain error the caller can catch, or a producer bug the -//! wrapper must trap on? -//! * **Iterators** ([`IteratorProtocol`]): the pull contract of `iter`, -//! including the requirement that wrappers stay **lazy** (one producer -//! `next` per consumer step, never a hidden drain into a list). -//! * **Async** ([`AsyncProtocol`]): the completion-callback contract, -//! including the rule that results and errors are owned by the consumer -//! and released through the runtime free symbols. -//! * **Callback interfaces** ([`CallbackProtocol`]): the contract for the -//! consumer-implemented vtable the producer calls back into. +//! * **Passing** ([`ArgPass`] on every parameter): which C slots one +//! argument occupies and how it fills them (by value, a presence flag plus +//! a value, a typed array, a pinned string or byte run, a serialized value +//! buffer, a borrowed object pointer, or a callback context plus vtable). +//! * **Receiving** ([`RetPass`] for a call's return, [`ResultPass`] for an +//! async completion's result, [`ItemPass`] for an iterator element, +//! [`CallbackRetPass`] for a callback method's return): which slots carry +//! the value back, what the receiver does with it (use, copy, decode, or +//! adopt), and the release it owes, with every obligation resolved (the +//! object's `_destroy` symbol, the slice's element type). +//! * **Errors** ([`ErrorStrategy`] on every callable): when a call reports +//! through `out_err`, is that a typed domain error the caller can catch, +//! an untyped error, or a producer bug the wrapper must trap on? //! -//! Every classification here derives from [`Ty::family`] and every symbol -//! comes from the [`Model`](crate::model::Model), so a backend that renders -//! these plans in its own syntax cannot drift from the others on semantics; -//! only the spelling differs. +//! Each variant names its own slots ([`AbiParam`]s, owned), so a backend +//! that dispatches on these enums never looks a slot up by position. The +//! slots are the same values that appear, in order, in the binding's +//! [`AbiFn`](crate::model::AbiFn). +//! +//! The protocols below (iterators, async, callback interfaces) are the +//! contract clauses every backend must satisfy; the C ABI reference +//! (`docs/src/reference/abi.md`) is normative. +//! +//! # Iterators +//! +//! The producer returns an opaque iterator handle +//! ([`RetPass::Iterator`]); the consumer calls +//! [`next`](crate::model::IteratorBinding::next) once per element and +//! [`destroy_symbol`](crate::model::IteratorBinding::destroy_symbol) exactly +//! once when done. Wrappers expose the target's native **lazy** iteration +//! idiom and issue one producer `next` per consumer step (draining the +//! producer into a hidden list is a contract violation). Each element is +//! received as its [`ItemPass`] says, and each `next` follows the owning +//! callable's [`ErrorStrategy`]. +//! +//! # Async +//! +//! The launcher returns immediately; the producer invokes the completion +//! callback exactly once, from an arbitrary producer thread. Everything +//! passed to the callback is owned by the consumer: results are received as +//! their [`ResultPass`] says, and a non-null `err` is heap-boxed and +//! released with `{prefix}_error_free`. Wrappers hop back to their native +//! scheduler before touching consumer state. +//! +//! # Callback interfaces +//! +//! The wrapper emits one static vtable per interface (the fixed `size`, +//! `flags`, and `free` header, then one trampoline per method from the +//! method's [`AbiFn`](crate::model::AbiFn)) and passes a handle-table key as +//! `ctx`. Inside a trampoline, arguments are received as their [`ArgPass`] +//! names them: strings, bytes, slices, and buffers are borrowed for the call +//! (copy or decode them, free nothing), and object arguments transfer one +//! strong reference the wrapper adopts. The method's return crosses back as +//! its [`CallbackRetPass`] says. A failure is reported through `out_err` +//! with `{prefix}_error_set` (never by unwinding through the C frame); see +//! [`ErrorStrategy`] for which codes a method may report. use crate::abi::AbiParam; -use crate::model::{ - AsyncBinding, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, IteratorBinding, - ParamBinding, -}; -use crate::ty::{Family, Ty}; +use crate::model::IteratorBinding; +use crate::ty::{Prim, Ty}; -/// How a callable's `out_err` slot is interpreted by idiomatic wrappers. +/// How a callable's `out_err` slot (or an async completion's `err`) is +/// interpreted by idiomatic wrappers. /// /// Every synchronous C ABI entry point carries a trailing `out_err`, and every -/// async completion callback carries an `err` slot, regardless of `throws`. +/// async completion callback carries an `err` slot, whatever the strategy. /// What differs is the *meaning* of a non-zero code, and every backend must -/// agree on it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// agree on it. A negative code (generic `-1`, panic `-2`, marshalling +/// failure `-3`, foreign callback failure `-4`, cancelled `-5`) is a runtime +/// code under every strategy. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ErrorStrategy { - /// The function declares `throws: true`: a non-zero code is a typed - /// domain error. The wrapper maps the code onto the module's error - /// domain (an exception subclass, a Swift `Error` enum case, a Go - /// `error` value, ...), decodes any payload fields from the error's - /// `payload_ptr`/`payload_len` buffer, and surfaces it through the - /// target's normal error channel so callers can catch and match on it. - /// A *negative* code (generic `-1`, panic `-2`, marshalling failure - /// `-3`, foreign callback failure `-4`, cancelled `-5`) surfaces through - /// the same channel as the target's runtime error type, distinct from - /// every domain error type, and `-5` as the language's cancellation error - /// where it has one. - Throws, - /// The function does not throw: the only way `out_err` reports failure - /// is a producer bug or a runtime trap (a caught panic, code `-2`; a - /// consumer callback that raised, code `-4`). The wrapper surfaces it - /// through the target's *programming-error* idiom (an unchecked exception - /// where the language has them, a Go `panic`, a Swift `fatalError`). - /// It must never be silently ignored, and it must never be dressed up as - /// a typed domain error. + /// The callable doesn't throw: the only way `out_err` reports failure is + /// a producer bug or a runtime trap (a caught panic, `-2`; a consumer + /// callback that raised, `-4`). The wrapper surfaces it through the + /// target's *programming-error* idiom (an unchecked exception where the + /// language has them, a Go `panic`, a Swift `fatalError`), never as a + /// typed error, and never ignores it. A callback method with this + /// strategy reports any failure as `-4`. Trap, + /// The callable throws the named error domain (`throws: KvError`), a + /// name [`Model::error_domain`](crate::model::Model::error_domain) + /// resolves. A positive code is one of the domain's codes: the wrapper + /// maps it to the code's typed error, decoding any payload fields from + /// the error's payload buffer, and maps a positive code it doesn't know + /// to the domain's base error type, keeping the code and message + /// (domains are open). Runtime codes surface through the same channel + /// as the target's runtime error type (`-5` as the language's + /// cancellation error where it has one). A callback method with this + /// strategy may report the domain's positive codes with their payloads. + Domain(String), + /// The callable throws an untyped error (`throws: any`): a failure is + /// the generic runtime code `-1` with a message, surfaced through the + /// target's normal error channel as its runtime error type; there are + /// no positive codes. A callback method with this strategy reports a + /// failure as `-1` with its message. + Untyped, } -impl FnBinding { - /// The error strategy of this callable: [`ErrorStrategy::Throws`] when the - /// IDL declares `throws: true`, otherwise [`ErrorStrategy::Trap`]. - pub fn error_strategy(&self) -> ErrorStrategy { - if self.throws { - ErrorStrategy::Throws - } else { - ErrorStrategy::Trap +impl ErrorStrategy { + /// `true` unless the strategy is [`Trap`](Self::Trap): the idiomatic + /// signature declares that the callable can fail. + #[must_use] + pub fn throws(&self) -> bool { + !matches!(self, ErrorStrategy::Trap) + } + + /// The error domain's name for [`Domain`](Self::Domain), else `None`. + #[must_use] + pub fn domain(&self) -> Option<&str> { + match self { + ErrorStrategy::Domain(name) => Some(name), + ErrorStrategy::Trap | ErrorStrategy::Untyped => None, } } } -/// How one parameter crosses the call boundary: the passing contract a -/// wrapper renders when marshalling its native argument into ABI slots. +/// How one parameter crosses the call boundary: the C slots it occupies, in +/// order, and how the wrapper fills them. /// -/// Exactly one variant applies to any parameter, and the borrowed -/// [`AbiParam`] references point at the parameter's own precomputed slots, -/// so a backend that dispatches on this enum cannot disagree with the C -/// header about arity or slot order. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ArgPass<'a> { - /// One slot passed by value: scalars, bools, and C-style enums. +/// As a callable's parameter, the argument is borrowed by the producer for +/// the call. As a callback method's parameter, the same slots carry the +/// producer's argument to the consumer's trampoline: borrowed for the call, +/// except an object, whose strong reference the consumer adopts. A callback +/// method's parameter is never [`Callback`](Self::Callback). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArgPass { + /// One slot passed by value: scalars, bools, and C-style enums + /// (`int32_t`). Direct { - /// The single ABI slot. - slot: &'a AbiParam, + /// The value slot, `T {name}`. + slot: AbiParam, }, - /// A borrowed `(ptr, len)` pair of UTF-8 bytes, not NUL-terminated. The - /// wrapper encodes the string and keeps the encoding alive for the - /// duration of the call; the producer copies what it needs. + /// An optional scalar, bool, or C-style enum: `bool has_{name}` then + /// `T {name}`. When `has` is false, `value` is ignored (pass `0`). + OptDirect { + /// The presence slot, `bool has_{name}`. + has: AbiParam, + /// The value slot, `T {name}`. + value: AbiParam, + /// The inner type: a [`Ty::Prim`] scalar or a [`Ty::Enum`]. + inner: Ty, + }, + /// A list of a numeric primitive passed as a typed array: + /// `const T* {name}_ptr` then `size_t {name}_len` (the element count). + /// The array is borrowed, must be aligned for `T`, and may be null when + /// the count is `0`. + Slice { + /// The `const T*` slot. + ptr: AbiParam, + /// The `size_t` element-count slot. + len: AbiParam, + /// The element primitive. + elem: Prim, + }, + /// A `(ptr, len)` pair of UTF-8 bytes, not NUL-terminated: + /// `const uint8_t* {name}_ptr`, `size_t {name}_len`. The wrapper encodes + /// the string and keeps the encoding alive for the call. String { /// The `const uint8_t*` slot. - ptr: &'a AbiParam, - /// The `size_t` length slot. - len: &'a AbiParam, + ptr: AbiParam, + /// The `size_t` byte-length slot. + len: AbiParam, }, - /// A borrowed `(ptr, len)` byte pair. The wrapper pins its native byte - /// storage for the call; the producer copies what it needs. + /// A `(ptr, len)` byte pair: `const uint8_t* {name}_ptr`, + /// `size_t {name}_len`. The wrapper pins its native byte storage for + /// the call. Bytes { - /// The `uint8_t*` data slot. - ptr: &'a AbiParam, - /// The `size_t` length slot. - len: &'a AbiParam, - }, - /// A buffered value (record, rich enum, optional, list, or map): the - /// wrapper serializes it into the value-buffer wire format - /// ([`Ty::wire`]), passes the encoding as a borrowed `(ptr, len)` pair, - /// and releases its own encoding after the call returns. Any object - /// token written into the encoding must be a freshly cloned reference - /// (the interface's - /// [`clone_symbol`](crate::model::InterfaceBinding::clone_symbol)). + /// The `const uint8_t*` slot. + ptr: AbiParam, + /// The `size_t` byte-length slot. + len: AbiParam, + }, + /// A buffered value (record, rich enum, or an optional, list, or map of + /// any other family): the wrapper serializes it into the value-buffer + /// format ([`Ty::wire`]) and passes the encoding as + /// `const uint8_t* {name}_ptr`, `size_t {name}_len`. Any object token in + /// the encoding is a freshly cloned reference. Buffer { - /// The `const uint8_t*` data slot. - ptr: &'a AbiParam, - /// The `size_t` length slot. - len: &'a AbiParam, - }, - /// A borrowed object pointer: the wrapper passes the wrapped object's - /// native handle and retains its own reference. When `nullable`, the IDL - /// type is `Interface?` and null means none. + /// The `const uint8_t*` slot. + ptr: AbiParam, + /// The `size_t` byte-length slot. + len: AbiParam, + }, + /// An object pointer: `const {tag}* {name}` as a callable's parameter + /// (borrowed; the wrapper keeps its own reference), or `{tag}* {name}` + /// as a callback method's parameter (one strong reference the consumer + /// adopts). Object { - /// The single object-pointer slot. - slot: &'a AbiParam, + /// The pointer slot. + slot: AbiParam, /// `true` for `Interface?`: null is a legal "none" argument. nullable: bool, + /// The interface's bare name. + interface: String, }, - /// A callback interface: the wrapper registers its native implementation - /// in a handle table, passes the table key as `ctx` and the interface's - /// static vtable as `vtable`, and removes the entry when the producer - /// calls the vtable's `free` (see [`CallbackProtocol`]). When - /// `nullable`, the IDL type is `Cb?` and a null `vtable` means none. + /// A callback interface: `void* {name}_ctx` then + /// `const {vtable}* {name}_vtable`. The wrapper registers its native + /// implementation in a handle table, passes the key as `ctx` and the + /// interface's static vtable as `vtable`, and removes the entry when the + /// producer calls the vtable's `free`. Callback { /// The `void*` context slot. - ctx: &'a AbiParam, + ctx: AbiParam, /// The `const {vtable}*` slot. - vtable: &'a AbiParam, + vtable: AbiParam, /// `true` for `Cb?`: a null vtable is a legal "none" argument. nullable: bool, + /// The callback interface's bare name. + interface: String, }, } -impl ParamBinding { - /// The passing contract for this parameter. - /// - /// # Panics - /// - /// Panics if the parameter's precomputed ABI slots disagree with its - /// type's family, which would be a bug in the model construction, not a - /// user error. - pub fn arg_pass(&self) -> ArgPass<'_> { - let pair = || { - assert!( - self.abi.len() == 2, - "two-slot parameter '{}' must have exactly two ABI slots", - self.name - ); - (&self.abi[0], &self.abi[1]) - }; - let single = || { - assert!( - self.abi.len() == 1, - "single-slot parameter '{}' must have exactly one ABI slot", - self.name - ); - &self.abi[0] - }; - match self.ty.family() { - Family::Direct => ArgPass::Direct { slot: single() }, - Family::String => { - let (ptr, len) = pair(); - ArgPass::String { ptr, len } - } - Family::Bytes => { - let (ptr, len) = pair(); - ArgPass::Bytes { ptr, len } - } - Family::Buffer => { - let (ptr, len) = pair(); - ArgPass::Buffer { ptr, len } - } - Family::Object { nullable } => ArgPass::Object { - slot: single(), - nullable, - }, - Family::Callback { nullable } => { - let (ctx, vtable) = pair(); - ArgPass::Callback { - ctx, - vtable, - nullable, - } - } - Family::Iterator => unreachable!("iterators are never parameters"), +impl ArgPass { + /// The slots this argument occupies, in signature order. + #[must_use] + pub fn slots(&self) -> Vec<&AbiParam> { + match self { + ArgPass::Direct { slot } | ArgPass::Object { slot, .. } => vec![slot], + ArgPass::OptDirect { has, value, .. } => vec![has, value], + ArgPass::Slice { ptr, len, .. } + | ArgPass::String { ptr, len } + | ArgPass::Bytes { ptr, len } + | ArgPass::Buffer { ptr, len } => vec![ptr, len], + ArgPass::Callback { ctx, vtable, .. } => vec![ctx, vtable], } } } -/// How a value produced by the producer crosses back to the wrapper: the -/// receiving contract, including the decode step and the release obligation. +/// How a synchronous call's return crosses back to the wrapper: the C +/// return, any trailing out slots (which precede `out_err`), what the +/// wrapper does with the value, and the release it owes. /// -/// Sync returns, async results, and iterator elements all use this. +/// On failure every C return is a zero sentinel the wrapper must not use. +// A model holds one of these per callable and never moves them in bulk, so +// boxing the iterator variant would only cost consumers a deref. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RetPass { - /// No return value. + /// No return value: the C return is `void`. Also an async launcher's + /// return, whose result arrives through + /// [`AsyncBinding::result`](crate::model::AsyncBinding::result). Void, - /// By-value return (scalar, bool, C-style enum): use directly, nothing to - /// free. + /// By-value return (scalar, bool, C-style enum): the C return is the + /// value. Nothing to free. Direct, - /// Owned `(const uint8_t*, out_len)` UTF-8: decode into the native string, - /// then release with - /// `{prefix}_free_bytes`. - String, - /// Owned `(const uint8_t*, out_len)` raw bytes: copy, then release with - /// `{prefix}_free_bytes`. - Bytes, - /// Owned `(const uint8_t*, out_len)` value buffer: decode via the wire - /// format ([`Ty::wire`]), adopting any object tokens it carries, then - /// release with `{prefix}_free_bytes`. - Buffer, - /// One strong object reference the wrapper adopts into its disposal - /// idiom, eventually released with the interface's `_destroy` symbol - /// ([`InterfaceBinding::destroy_symbol`], from - /// [`Model::interface`](crate::model::Model::interface)). When - /// `nullable`, the IDL type is `Interface?` and a null return means - /// none. - /// - /// [`InterfaceBinding::destroy_symbol`]: crate::model::InterfaceBinding::destroy_symbol + /// An optional scalar, bool, or C-style enum: the C return is a `bool` + /// (present), and the value is written to `T* out_value`. Nothing to + /// free; on failure the return is `false`. + OptDirect { + /// The `T* out_value` slot. + out_value: AbiParam, + }, + /// A list of a numeric primitive returned as a typed array: the C + /// return is `T*` and the element count is written to `size_t* + /// out_len`. Copy the elements, then release the run with + /// `{prefix}_free_bytes((uint8_t*)ptr, len * sizeof(T))` (see + /// [`Prim::size`]). + Slice { + /// The `size_t* out_len` slot (element count). + out_len: AbiParam, + /// The element primitive. + elem: Prim, + }, + /// Owned UTF-8: the C return is `const uint8_t*` and the byte length is + /// written to `size_t* out_len`. Decode into the native string, then + /// release with `{prefix}_free_bytes(ptr, len)`. + String { + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// Owned raw bytes, returned like [`String`](Self::String): copy, then + /// release with `{prefix}_free_bytes(ptr, len)`. + Bytes { + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// An owned value buffer, returned like [`String`](Self::String): + /// decode via the wire format ([`Ty::wire`]), adopting any object tokens + /// it carries, then release with `{prefix}_free_bytes(ptr, len)`. + Buffer { + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// One strong object reference (`{tag}*`) the wrapper adopts into its + /// disposal idiom, eventually released with `destroy_symbol`. Object { /// `true` for `Interface?`: a null return is a legal "none" result. nullable: bool, + /// The interface's bare name. + interface: String, + /// The interface's `{tag}_destroy` symbol. + destroy_symbol: String, }, + /// An iterator handle (`{iter_tag}*`) the wrapper drives lazily with + /// [`IteratorBinding::next`] and releases exactly once with + /// [`IteratorBinding::destroy_symbol`]. + Iterator(IteratorBinding), } impl RetPass { - /// The receiving contract for a value of type `ty` produced by a - /// callable. `None` (a void return) is [`RetPass::Void`]. - /// - /// # Panics - /// - /// Panics on an iterator return, whose contract is [`IteratorProtocol`], - /// not a value-passing plan (backends dispatch on - /// [`CallShape`](crate::model::CallShape) before consulting this), and on - /// a callback interface, which validation never admits as a return. - pub fn of(ty: Option<&Ty>) -> RetPass { - let Some(ty) = ty else { - return RetPass::Void; - }; - match ty.family() { - Family::Direct => RetPass::Direct, - Family::String => RetPass::String, - Family::Bytes => RetPass::Bytes, - Family::Buffer => RetPass::Buffer, - Family::Object { nullable } => RetPass::Object { nullable }, - Family::Callback { .. } => panic!("callback interfaces are never returned"), - Family::Iterator => panic!("iterator returns follow IteratorProtocol, not a RetPass"), + /// The trailing out slots this return adds before `out_err`, in order. + #[must_use] + pub fn out_slots(&self) -> Vec<&AbiParam> { + match self { + RetPass::OptDirect { out_value } => vec![out_value], + RetPass::Slice { out_len, .. } + | RetPass::String { out_len } + | RetPass::Bytes { out_len } + | RetPass::Buffer { out_len } => vec![out_len], + RetPass::Void | RetPass::Direct | RetPass::Object { .. } | RetPass::Iterator(_) => { + vec![] + } } } } -/// The `iter` pull contract every backend renders. -/// -/// The producer returns an opaque iterator handle; the consumer then calls -/// `next` once per element and `destroy` exactly once when done. The binding -/// contract has three clauses every wrapper must satisfy: -/// -/// 1. **Laziness.** The wrapper exposes the target's native lazy iteration -/// idiom (a Python iterator, a Ruby `Enumerator`, a Go `iter.Seq2`, a C# -/// `IEnumerable`, a Dart `Iterable`, a JS iterable, a Swift `Sequence`, a -/// Kotlin `Iterator`) and issues **one producer `next` call per consumer -/// step**. Draining the producer into a hidden list defeats the point of -/// `iter` (constant-memory streaming) and is a contract violation. -/// 2. **Element ownership.** Each `next` writes an element the consumer now -/// owns; the wrapper receives it exactly as it would a return of the same -/// type ([`elem`](Self::elem)): copy and free a string or bytes, decode -/// and free a buffer, adopt an object. -/// 3. **Handle lifecycle.** `destroy` is called exactly once: eagerly on -/// exhaustion, and from the wrapper's disposal idiom (RAII destructor, -/// finalizer, `close()`, generator cleanup) when iteration is abandoned -/// early. +/// How an async call's result arrives in its completion callback, after the +/// `(void* context, {prefix}_error* err)` prefix. /// -/// Each `next` call also carries `out_err` and follows the owning function's -/// [`ErrorStrategy`]. +/// Everything the callback receives is owned by the consumer. On failure +/// (`err` non-null) the result slots hold zero values the consumer must not +/// use. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct IteratorProtocol<'a> { - /// The lowered iterator surface: launcher, `next`, and destroy symbols. - pub binding: &'a IteratorBinding, - /// How each element written by `next` is received. - pub elem: RetPass, - /// How `out_err` reports from the launcher and each `next` call are - /// interpreted. - pub error: ErrorStrategy, +pub enum ResultPass { + /// A `void` function: no result slots. + Void, + /// By value: `T result`. + Direct { + /// The `T result` slot. + result: AbiParam, + }, + /// An optional scalar, bool, or C-style enum: `bool has_result`, then + /// `T result` (ignored when `has_result` is false). + OptDirect { + /// The `bool has_result` slot. + has: AbiParam, + /// The `T result` slot. + value: AbiParam, + }, + /// A typed array: `const T* result_ptr`, `size_t result_len` (the + /// element count). Copy, then release with + /// `{prefix}_free_bytes((uint8_t*)ptr, len * sizeof(T))`. + Slice { + /// The `const T*` slot. + ptr: AbiParam, + /// The `size_t` element-count slot. + len: AbiParam, + /// The element primitive. + elem: Prim, + }, + /// Owned UTF-8: `const uint8_t* result_ptr`, `size_t result_len`. + /// Decode, then release with `{prefix}_free_bytes(ptr, len)`. + String { + /// The `const uint8_t*` slot. + ptr: AbiParam, + /// The `size_t` byte-length slot. + len: AbiParam, + }, + /// Owned raw bytes, delivered like [`String`](Self::String). + Bytes { + /// The `const uint8_t*` slot. + ptr: AbiParam, + /// The `size_t` byte-length slot. + len: AbiParam, + }, + /// An owned value buffer, delivered like [`String`](Self::String): + /// decode (adopting object tokens), then release. + Buffer { + /// The `const uint8_t*` slot. + ptr: AbiParam, + /// The `size_t` byte-length slot. + len: AbiParam, + }, + /// One strong object reference: `{tag}* result`, adopted and eventually + /// released with `destroy_symbol`. + Object { + /// The `{tag}* result` slot. + result: AbiParam, + /// `true` for `Interface?`: null is a legal "none" result. + nullable: bool, + /// The interface's bare name. + interface: String, + /// The interface's `{tag}_destroy` symbol. + destroy_symbol: String, + }, } -impl IteratorBinding { - /// Build the full pull contract for this iterator of `f`. - pub fn protocol<'a>(&'a self, f: &FnBinding) -> IteratorProtocol<'a> { - IteratorProtocol { - binding: self, - elem: RetPass::of(Some(&self.elem)), - error: f.error_strategy(), +impl ResultPass { + /// The result slots, in signature order. + #[must_use] + pub fn slots(&self) -> Vec<&AbiParam> { + match self { + ResultPass::Void => vec![], + ResultPass::Direct { result } | ResultPass::Object { result, .. } => vec![result], + ResultPass::OptDirect { has, value } => vec![has, value], + ResultPass::Slice { ptr, len, .. } + | ResultPass::String { ptr, len } + | ResultPass::Bytes { ptr, len } + | ResultPass::Buffer { ptr, len } => vec![ptr, len], } } } -/// The async completion contract every backend renders. -/// -/// The launcher returns immediately; the producer later invokes the completion -/// callback exactly once, from an arbitrary producer thread. The contract has -/// three clauses: +/// How one iterator element arrives through `{iter_tag}_next`'s out slots +/// (after `iter`, before `out_err`). `next` returns `1` when it wrote an +/// element and `0` when the iterator is exhausted (or failed). /// -/// 1. **Single completion.** The callback fires exactly once per launch; the -/// wrapper resolves its native future idiom (a Python `asyncio` future, a -/// JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go -/// channel) exactly once and then releases the registration. -/// 2. **Owned results.** Everything passed to the callback is owned by the -/// consumer. String results are released with `{prefix}_free_bytes`, -/// byte and buffered-value results with `{prefix}_free_bytes`, and -/// interface-object results transfer one strong reference (the wrapper -/// adopts the pointer). This is what lets runtimes that defer callback -/// bodies past the native return (Dart's `NativeCallable.listener`, for -/// example) decode safely; a wrapper that processes results inline still -/// copies or decodes first and then frees. -/// 3. **Foreign-thread delivery.** The callback runs on a producer thread, -/// so the wrapper must hop back to its native scheduler before touching -/// consumer state (`call_soon_threadsafe`, a threadsafe function, a -/// dispatched continuation) rather than resolving inline where the -/// target's runtime forbids it. -/// -/// The callback's `err` slot follows the owning function's [`ErrorStrategy`]. -/// A non-null error is heap-boxed and owned by the consumer: the wrapper -/// copies the code, message, and payload, then releases the box with -/// `{prefix}_error_free` exactly once. +/// Each element is owned by the consumer, exactly like a return of its +/// type. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AsyncProtocol<'a> { - /// The lowered async surface: launcher and callback typedef. - pub binding: &'a AsyncBinding, - /// Whether the launcher carries a `cancel_token` slot before - /// `callback`/`context`. - pub cancellable: bool, - /// How the callback's result slots are received, including the release - /// owed (`{prefix}_free_bytes` or the destroy symbol an adopted object - /// owes). - pub result: RetPass, - /// How the callback's `err` slot is interpreted. - pub error: ErrorStrategy, +pub enum ItemPass { + /// By value: `T* out_item`. + Direct { + /// The `T* out_item` slot. + out_item: AbiParam, + }, + /// An optional scalar, bool, or C-style enum: `bool* out_has_item`, + /// then `T* out_item` (meaningful when `*out_has_item`). + OptDirect { + /// The `bool* out_has_item` slot. + out_has: AbiParam, + /// The `T* out_item` slot. + out_item: AbiParam, + }, + /// A typed array: `T** out_item`, `size_t* out_len` (the element + /// count). Copy, then release with + /// `{prefix}_free_bytes((uint8_t*)ptr, len * sizeof(T))`. + Slice { + /// The `T** out_item` slot. + out_item: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + /// The element primitive. + elem: Prim, + }, + /// Owned UTF-8: `const uint8_t** out_item`, `size_t* out_len`. Decode, + /// then release with `{prefix}_free_bytes(ptr, len)`. + String { + /// The `const uint8_t** out_item` slot. + out_item: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// Owned raw bytes, delivered like [`String`](Self::String). + Bytes { + /// The `const uint8_t** out_item` slot. + out_item: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// An owned value buffer, delivered like [`String`](Self::String): + /// decode (adopting object tokens), then release. + Buffer { + /// The `const uint8_t** out_item` slot. + out_item: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// One strong object reference: `{tag}** out_item`, adopted and + /// eventually released with `destroy_symbol`. + Object { + /// The `{tag}** out_item` slot. + out_item: AbiParam, + /// `true` for `Interface?`: a null element is a legal "none". + nullable: bool, + /// The interface's bare name. + interface: String, + /// The interface's `{tag}_destroy` symbol. + destroy_symbol: String, + }, } -impl AsyncBinding { - /// Build the full completion contract for this async function `f`. - pub fn protocol<'a>(&'a self, f: &FnBinding) -> AsyncProtocol<'a> { - AsyncProtocol { - binding: self, - cancellable: f.cancellable, - result: RetPass::of(f.ret.as_ref()), - error: f.error_strategy(), +impl ItemPass { + /// The item's out slots, in signature order. + #[must_use] + pub fn slots(&self) -> Vec<&AbiParam> { + match self { + ItemPass::Direct { out_item } | ItemPass::Object { out_item, .. } => vec![out_item], + ItemPass::OptDirect { out_has, out_item } => vec![out_has, out_item], + ItemPass::Slice { + out_item, out_len, .. + } + | ItemPass::String { out_item, out_len } + | ItemPass::Bytes { out_item, out_len } + | ItemPass::Buffer { out_item, out_len } => vec![out_item, out_len], } } } -/// The callback-interface contract every backend renders. -/// -/// A callback interface is the consumer's side of the boundary: the consumer -/// supplies an implementation, the producer calls it. The contract has five -/// clauses: -/// -/// 1. **One static vtable per interface.** The wrapper emits exactly one -/// process-wide vtable value for the interface. It starts with the fixed -/// header, `size` (`sizeof` the vtable as the wrapper compiled it), -/// `flags` (`0`), and `free`, followed by one trampoline per method from -/// the C signature ([`CallbackMethodBinding::abi_params`]) into the native -/// implementation. The producer rejects a vtable smaller than the one it -/// was built with, so a stale binding fails with `-3` instead of calling -/// through a missing slot. -/// 2. **Context is a handle-table key.** The wrapper stores the native -/// implementation in a table keyed by an integer or pointer it passes as -/// `ctx`, so the implementation stays alive as long as the producer holds -/// the callback and garbage collectors never see a raw pointer. `free` -/// may run on any producer thread. -/// 3. **Arguments are received like returns.** Strings, bytes, and buffers -/// arriving in a trampoline are borrowed for the call: the wrapper copies -/// or decodes them before returning and frees nothing. Object arguments -/// transfer one strong reference the wrapper adopts. -/// 4. **Returns transfer to the producer** ([`method_returns`](Self::method_returns)). -/// A direct value is the C return. An object is returned as one strong -/// reference (a fresh `_clone` of the wrapper's handle). A string, bytes, -/// or buffer is written to the trailing `out_ptr`/`out_len` slots as a run -/// the wrapper allocates with `{prefix}_alloc`; the producer adopts it. -/// 5. **Failures go through `out_err`.** When the native implementation -/// raises, the trampoline calls `{prefix}_error_set(out_err, code, -/// message)` and returns a zero value; it must never let an exception -/// unwind through the C frame. A method whose -/// [`method_errors`](Self::method_errors) entry is -/// [`ErrorStrategy::Throws`] reports a declared domain error with its -/// positive code and its payload fields (`{prefix}_error_set_payload`); -/// every other failure uses `-4`, and the producer treats any code it -/// can't attribute to the method's domain as `-4`. -/// -/// Trampolines may be invoked from any producer thread; the wrapper is -/// responsible for whatever thread affinity its runtime demands (a GIL -/// acquisition, a JNI attach, a threadsafe-function hop). +/// How a callback method's return crosses back from the consumer's +/// trampoline to the producer, which adopts it. /// -/// [`CallbackMethodBinding::abi_params`]: crate::model::CallbackMethodBinding::abi_params +/// The producer adopts whatever the return and out slots hold whether or +/// not the method failed, so a trampoline that fails after allocating +/// doesn't leak. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct CallbackProtocol<'a> { - /// The lowered callback interface: vtable tag and methods. - pub binding: &'a CallbackInterfaceBinding, - /// How each method's parameters are received inside a trampoline, in - /// method order then parameter order. - pub method_args: Vec>, - /// How each method's return value crosses back to the producer, in - /// method order. [`RetPass::String`], [`RetPass::Bytes`], and - /// [`RetPass::Buffer`] are written to the `out_ptr`/`out_len` slots as a - /// `{prefix}_alloc` run. - pub method_returns: Vec, - /// How each method may report failure, in method order. - pub method_errors: Vec, -} - -impl CallbackMethodBinding { - /// The error strategy of this method: [`ErrorStrategy::Throws`] when it - /// declares `throws: true`, otherwise [`ErrorStrategy::Trap`] (any - /// failure reaches the producer as `-4`). - pub fn error_strategy(&self) -> ErrorStrategy { - if self.throws { - ErrorStrategy::Throws - } else { - ErrorStrategy::Trap - } - } -} - -impl CallbackInterfaceBinding { - /// Build the full contract for this callback interface. - pub fn protocol(&self) -> CallbackProtocol<'_> { - CallbackProtocol { - binding: self, - method_args: self - .methods - .iter() - .map(|m| m.params.iter().map(|p| RetPass::of(Some(&p.ty))).collect()) - .collect(), - method_returns: self - .methods - .iter() - .map(|m| RetPass::of(m.ret.as_ref())) - .collect(), - method_errors: self - .methods - .iter() - .map(CallbackMethodBinding::error_strategy) - .collect(), - } - } +pub enum CallbackRetPass { + /// No return value: the C return is `void`. + Void, + /// By value: the C return is the value. + Direct, + /// An optional scalar, bool, or C-style enum: the C return is a `bool` + /// (present), and the trampoline writes the value to `T* out_value`. + OptDirect { + /// The `T* out_value` slot. + out_value: AbiParam, + }, + /// A typed array: the C return is `void`; the trampoline allocates + /// `len * sizeof(T)` bytes with `{prefix}_alloc`, copies the elements + /// in, and writes the pointer to `T** out_ptr` and the element count to + /// `size_t* out_len`. Null with count `0` is the empty list. + Slice { + /// The `T** out_ptr` slot. + out_ptr: AbiParam, + /// The `size_t* out_len` slot (element count). + out_len: AbiParam, + /// The element primitive. + elem: Prim, + }, + /// UTF-8: the C return is `void`; the trampoline allocates the run with + /// `{prefix}_alloc` and writes it to `uint8_t** out_ptr` and its byte + /// length to `size_t* out_len`. + String { + /// The `uint8_t** out_ptr` slot. + out_ptr: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// Raw bytes, returned like [`String`](Self::String). + Bytes { + /// The `uint8_t** out_ptr` slot. + out_ptr: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// A value buffer, returned like [`String`](Self::String); any object + /// token in it is a freshly cloned reference. + Buffer { + /// The `uint8_t** out_ptr` slot. + out_ptr: AbiParam, + /// The `size_t* out_len` slot. + out_len: AbiParam, + }, + /// One strong object reference as the C return (`{tag}*`): a fresh + /// `clone_symbol` of the wrapper's handle, which the producer adopts. + Object { + /// `true` for `Interface?`: the trampoline may return null. + nullable: bool, + /// The interface's bare name. + interface: String, + /// The interface's `{tag}_clone` symbol. + clone_symbol: String, + }, } -#[cfg(test)] -mod tests { - use super::*; - use crate::ir::{Api, Function, Param, TypeRef}; - use crate::model::Model; - use crate::pkg::Identity; - use crate::ty::Prim; - - fn param(name: &str, ty: &str) -> Param { - Param { - name: name.into(), - ty: crate::ir::parse_type_ref(ty).unwrap(), - doc: None, - } - } - - /// A model with interface `Store` and callback interface `Listener` in - /// `kv`, a record `Contact`, and one function `f` taking `params`. - fn model(params: Vec) -> Model { - let yaml = r#" -version: "0.11.0" -modules: - - name: kv - structs: [{ name: Contact, fields: [{ name: id, type: i64 }] }] - interfaces: [{ name: Store, methods: [{ name: get }] }] - callback_interfaces: [{ name: Listener, methods: [{ name: on }] }] -"#; - let mut api: Api = serde_yaml::from_str(yaml).unwrap(); - api.modules[0].functions.push(Function { - name: "f".into(), - params, - returns: Some(TypeRef::Prim(Prim::I32)), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - }); - Model::assume_valid(&api, Identity::named("weaveffi")) - } - - #[test] - fn arg_pass_classifies_every_family() { - let m = model(vec![ - param("x", "i32"), - param("s", "string"), - param("data", "bytes"), - param("c", "Contact"), - param("o", "i32?"), - param("store", "Store"), - param("maybe", "Store?"), - param("l", "Listener"), - param("ml", "Listener?"), - ]); - let p = &m.modules[0].functions[0].params; - assert!(matches!(p[0].arg_pass(), ArgPass::Direct { slot } if slot.name == "x")); - assert!(matches!(p[1].arg_pass(), ArgPass::String { ptr, .. } if ptr.name == "s_ptr")); - assert!(matches!( - p[2].arg_pass(), - ArgPass::Bytes { ptr, len } if ptr.name == "data_ptr" && len.name == "data_len" - )); - assert!(matches!( - p[3].arg_pass(), - ArgPass::Buffer { ptr, len } if ptr.name == "c_ptr" && len.name == "c_len" - )); - assert!(matches!(p[4].arg_pass(), ArgPass::Buffer { .. })); - assert!(matches!( - p[5].arg_pass(), - ArgPass::Object { - nullable: false, - .. +impl CallbackRetPass { + /// The trailing out slots this return adds before `out_err`, in order. + #[must_use] + pub fn out_slots(&self) -> Vec<&AbiParam> { + match self { + CallbackRetPass::OptDirect { out_value } => vec![out_value], + CallbackRetPass::Slice { + out_ptr, out_len, .. + } + | CallbackRetPass::String { out_ptr, out_len } + | CallbackRetPass::Bytes { out_ptr, out_len } + | CallbackRetPass::Buffer { out_ptr, out_len } => vec![out_ptr, out_len], + CallbackRetPass::Void | CallbackRetPass::Direct | CallbackRetPass::Object { .. } => { + vec![] } - )); - assert!(matches!( - p[6].arg_pass(), - ArgPass::Object { nullable: true, .. } - )); - assert!(matches!( - p[7].arg_pass(), - ArgPass::Callback { ctx, vtable, nullable: false } - if ctx.name == "l_ctx" && vtable.name == "l_vtable" - )); - assert!(matches!( - p[8].arg_pass(), - ArgPass::Callback { nullable: true, .. } - )); - } - - #[test] - fn ret_pass_distinguishes_copy_decode_and_adopt() { - assert_eq!(RetPass::of(None), RetPass::Void); - assert_eq!(RetPass::of(Some(&Ty::Prim(Prim::I64))), RetPass::Direct); - assert_eq!(RetPass::of(Some(&Ty::Prim(Prim::String))), RetPass::String); - assert_eq!(RetPass::of(Some(&Ty::Prim(Prim::Bytes))), RetPass::Bytes); - for ty in [ - Ty::Record("Contact".into()), - Ty::RichEnum("Shape".into()), - Ty::List(Box::new(Ty::Prim(Prim::String))), - Ty::List(Box::new(Ty::Interface("Store".into()))), - Ty::Optional(Box::new(Ty::Prim(Prim::I64))), - ] { - assert_eq!(RetPass::of(Some(&ty)), RetPass::Buffer, "{ty}"); } - assert_eq!( - RetPass::of(Some(&Ty::Interface("Store".into()))), - RetPass::Object { nullable: false } - ); - assert_eq!( - RetPass::of(Some(&Ty::Optional(Box::new(Ty::Interface("Store".into()))))), - RetPass::Object { nullable: true } - ); } } diff --git a/crates/weaveffi-model/src/ty.rs b/crates/weaveffi-model/src/ty.rs index 375415a9..3f9ecc57 100644 --- a/crates/weaveffi-model/src/ty.rs +++ b/crates/weaveffi-model/src/ty.rs @@ -1,29 +1,37 @@ -//! The **resolved type** every backend consumes, the single taxonomy that -//! classifies it for the C ABI and the value-buffer wire format, and the +//! The **resolved types** every backend consumes, the single taxonomy that +//! classifies them for the C ABI and the value-buffer wire format, and the //! [`TypeIndex`] that maps each user type name to its declaration. //! //! The IDL's [`TypeRef`](crate::ir::TypeRef) is the type *as written*: a //! user-defined type is a bare `Named` string because the parser can't know //! whether it names a record, an enum, an interface, or a callback -//! interface. [`Ty`] is the type *as resolved* by validation: every user -//! reference carries its kind, and there is no "unresolved" variant for a -//! backend to trip over. Type names are global (validation rejects two -//! declarations with one name), so a [`Ty`] carries the bare name and the -//! [`TypeIndex`] answers where it's declared. +//! interface. The types here are the types *as resolved* by validation: +//! every user reference carries its kind, and there is no "unresolved" +//! variant for a backend to trip over. Type names are global (validation +//! rejects two declarations with one name), so a resolved type carries the +//! bare name and the [`TypeIndex`] answers where it's declared. //! -//! Three questions about a type are answered once here: +//! Types are split by **position**, so a type that can't appear somewhere +//! can't be written there: //! -//! * [`Ty::family`]: how the type crosses a **call boundary** (by value, as a -//! pinned string, as a `(ptr, len)` byte pair, as a serialized value -//! buffer, as an object pointer, or as a callback vtable pair). The ABI -//! lowering, the marshalling plan, and every backend's argument and return -//! handling dispatch on it. +//! * [`Ty`] is a **value type**: legal everywhere, including inside value +//! buffers (record fields, list elements, error payloads, and so on). +//! * [`ParamTy`] is a callable's parameter: a value, or a callback interface +//! (the one position a callback interface may appear). +//! * [`RetTy`] is a callable's return: a value, or an iterator (the one +//! position an iterator may appear). +//! +//! Two questions about a value type are answered once here: +//! +//! * [`Ty::family`]: how the type crosses a **call boundary** (by value, as +//! a presence flag plus a value, as a typed array, as a `(ptr, len)` byte +//! pair, as a serialized value buffer, or as an object pointer). The ABI +//! lowering dispatches on it, and the model stores the result on every +//! binding so backends never have to. //! * [`Ty::wire`]: how the type is encoded **inside a value buffer**. Every //! backend's codec emitter dispatches on it. -//! * [`Ty::contains_user_type`] and [`Ty::contains_object`]: whether encoding -//! the type needs a user-defined codec function or object-token support. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; /// A built-in primitive type: the fixed vocabulary the IDL's @@ -87,6 +95,21 @@ impl Prim { Prim::Bytes, ]; + /// The element types a list may have to cross a call boundary as a + /// typed array ([`Family::Slice`]): every integer and float but `u8` + /// (`[u8]` is `bytes`). + pub const SLICE_ELEMS: [Prim; 9] = [ + Prim::I8, + Prim::I16, + Prim::I32, + Prim::I64, + Prim::U16, + Prim::U32, + Prim::U64, + Prim::F32, + Prim::F64, + ]; + /// The primitive an IDL spelling names (`i32`, `string`, `bytes`), or /// `None` when `name` isn't a primitive. #[must_use] @@ -148,6 +171,36 @@ impl Prim { pub fn is_integer(self) -> bool { self.is_numeric() && !matches!(self, Prim::F32 | Prim::F64) } + + /// `true` for the primitives that cross by value in one C slot: the + /// integers, the floats, and `bool` (everything but `string` and + /// `bytes`). + #[must_use] + pub fn is_scalar(self) -> bool { + !matches!(self, Prim::String | Prim::Bytes) + } + + /// `true` when `[self]` crosses a call boundary as a typed array + /// ([`Family::Slice`]); see [`SLICE_ELEMS`](Self::SLICE_ELEMS). + #[must_use] + pub fn is_slice_elem(self) -> bool { + Prim::SLICE_ELEMS.contains(&self) + } + + /// The width in bytes of one value of a scalar primitive, both inside a + /// value buffer and as a C slot (C `bool` is one byte on every platform + /// WeaveFFI targets), or `None` for `string` and `bytes`. A typed-array + /// run of `len` elements spans `len * size` bytes. + #[must_use] + pub fn size(self) -> Option { + match self { + Prim::Bool | Prim::I8 | Prim::U8 => Some(1), + Prim::I16 | Prim::U16 => Some(2), + Prim::I32 | Prim::U32 | Prim::F32 => Some(4), + Prim::I64 | Prim::U64 | Prim::F64 => Some(8), + Prim::String | Prim::Bytes => None, + } + } } impl fmt::Display for Prim { @@ -156,77 +209,93 @@ impl fmt::Display for Prim { } } -/// A fully resolved type: the shape generators render and the ABI lowers. +/// A fully resolved **value type**: the shape generators render and the ABI +/// lowers, legal in every position (parameters, returns, async results, +/// iterator items, callback-method parameters and returns, record fields, +/// list elements, map keys and values, and error payloads). /// /// User types carry their bare declared name; [`TypeIndex`] (on the /// [`Model`](crate::model::Model)) maps the name to its declaration. +/// Callback interfaces and iterators are not value types: they're the +/// [`ParamTy::Callback`] and [`RetTy::Iterator`] positions. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Ty { /// A built-in primitive: a scalar, `bool`, `string`, or `bytes`. Prim(Prim), - /// A user record (struct): a plain value type. Crosses the C ABI by value - /// as a serialized buffer (`ptr` + `len`), borrowed for a call as a - /// parameter and owned (freed with `{prefix}_free_bytes`) as a return. + /// A user record (struct): a plain value type that crosses the C ABI as + /// a serialized value buffer. Record(String), /// An algebraic (rich) enum: a sum type with at least one payload-carrying /// variant. A value type that crosses the C ABI as a serialized buffer /// (an `i32` tag followed by the active variant's fields), exactly like a /// [`Record`](Self::Record). RichEnum(String), - /// A C-style integer enum (no variant payloads). Lowers by value. + /// A C-style integer enum (no variant payloads). Crosses by value as an + /// `int32_t`. Enum(String), /// A user interface: a reference-counted object. As a parameter the /// object is borrowed for the call; as a return (or inside a buffer) the /// receiver adopts one strong reference it must eventually release. Interface(String), - /// A callback interface: a method set the consumer implements. Only valid - /// as a top-level parameter (bare, or optional as `Cb?`), where it lowers - /// to a context pointer plus a vtable pointer. - CallbackInterface(String), /// Optional value (`T?`): either the inner type or nothing. Optional(Box), /// Homogeneous list (`[T]`) of the inner element type. List(Box), /// Map (`{K:V}`) from a key type to a value type. Map(Box, Box), - /// Lazy sequence (`iter`) of the inner type, lowered to a next/destroy - /// iterator object rather than a materialized collection. - Iterator(Box), +} + +/// The type of a callable's parameter: a value, or a callback interface. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ParamTy { + /// A value type. + Value(Ty), + /// A callback interface the consumer implements (`Listener`), or an + /// optional one (`Listener?`) when `nullable`. + Callback { + /// The callback interface's bare name. + name: String, + /// `true` for `Listener?`: a null vtable means none. + nullable: bool, + }, +} + +/// The type of a callable's return: a value, or an iterator. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum RetTy { + /// A value type. + Value(Ty), + /// A lazy sequence (`iter`) of the element type, lowered to an + /// iterator handle with its own `next`/`destroy` protocol. + Iterator(Ty), } /// How a value of some [`Ty`] crosses a **call boundary**: the one -/// classification the ABI lowering, the marshalling plan, and every backend's -/// argument and return handling agree on. +/// classification the ABI lowering and every stored passing contract agree +/// on. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Family { /// One C slot passed by value: scalars, bools, and C-style enums. Direct, - /// A `(ptr, len)` pair of UTF-8 bytes, never NUL-terminated. Borrowed for - /// a call as a parameter; producer-owned and released with - /// `{prefix}_free_bytes` as a return. + /// An optional [`Direct`](Self::Direct) value (`i32?`, `bool?`, + /// `Color?`): a `bool` presence flag plus the value, with no buffer. + OptDirect, + /// A list of a numeric primitive (`[i32]`, `[f64]`; see + /// [`Prim::SLICE_ELEMS`]): a typed array, a pointer to packed native + /// elements plus an element count. + Slice(Prim), + /// A `(ptr, len)` pair of UTF-8 bytes, never NUL-terminated. String, - /// A `(ptr, len)` raw byte pair. Borrowed as a parameter; producer-owned - /// and released with `{prefix}_free_bytes` as a return. + /// A `(ptr, len)` raw byte pair. Bytes, - /// A `(ptr, len)` serialized value buffer (record, rich enum, optional, - /// list, or map). Borrowed as a parameter; producer-owned and released - /// with `{prefix}_free_bytes` after decoding as a return. + /// A `(ptr, len)` serialized value buffer: a record, a rich enum, or an + /// optional, list, or map that isn't one of the families above. Buffer, - /// An object pointer to an interface. Borrowed as a parameter; one - /// strong reference (released with its `_destroy` symbol) as a return. + /// An object pointer to an interface. Object { /// `true` for `Interface?`: null is a legal "none" value. nullable: bool, }, - /// A callback interface: a `void* ctx` plus `const {tag}_vtable*` pair. - /// Only ever a top-level parameter. - Callback { - /// `true` for `Cb?`: a null vtable pointer is a legal "none" value. - nullable: bool, - }, - /// An `iter` return: an opaque iterator handle with its own - /// `next`/`destroy` protocol. Never a parameter. - Iterator, } /// The closed set of shapes a value inside a value buffer can take. @@ -263,51 +332,48 @@ pub enum WireType<'a> { impl Ty { /// How this type crosses a call boundary. Total over every `Ty`. + #[must_use] pub fn family(&self) -> Family { match self { Ty::Prim(Prim::String) => Family::String, Ty::Prim(Prim::Bytes) => Family::Bytes, Ty::Prim(_) | Ty::Enum(_) => Family::Direct, - Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => Family::Buffer, Ty::Interface(_) => Family::Object { nullable: false }, - // The one optional that is not buffered: `Interface?` stays a - // nullable pointer so the common "maybe an object" shape needs no - // encoding step. - Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { - Family::Object { nullable: true } - } - // Likewise `Cb?` stays a `(ctx, vtable)` pair with a null vtable - // meaning none; callback interfaces never enter a value buffer. - Ty::Optional(inner) if matches!(inner.as_ref(), Ty::CallbackInterface(_)) => { - Family::Callback { nullable: true } - } - Ty::Optional(_) => Family::Buffer, - Ty::CallbackInterface(_) => Family::Callback { nullable: false }, - Ty::Iterator(_) => Family::Iterator, + Ty::Optional(inner) => match inner.family() { + Family::Direct => Family::OptDirect, + // `Interface?` stays a nullable pointer so the common + // "maybe an object" shape needs no encoding step. + Family::Object { nullable: false } => Family::Object { nullable: true }, + _ => Family::Buffer, + }, + Ty::List(inner) => match inner.as_ref() { + Ty::Prim(p) if p.is_slice_elem() => Family::Slice(*p), + _ => Family::Buffer, + }, + Ty::Record(_) | Ty::RichEnum(_) | Ty::Map(_, _) => Family::Buffer, } } - /// `true` when this type crosses the C ABI as a serialized value buffer - /// (`const uint8_t*` + `size_t`) rather than as dedicated C slots. + /// `true` when this type crosses a call boundary as a serialized value + /// buffer (`const uint8_t*` + `size_t`) rather than as dedicated C slots. + #[must_use] pub fn is_buffered(&self) -> bool { self.family() == Family::Buffer } - /// The referenced user-type name for a record, rich enum, enum, - /// interface, or callback interface, or `None` for every other type. + /// The referenced user-type name for a record, rich enum, enum, or + /// interface, or `None` for every other type. + #[must_use] pub fn user_name(&self) -> Option<&str> { match self { - Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Enum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(n) => Some(n), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => Some(n), _ => None, } } /// The interface name inside a bare or optional interface type, or /// `None` when the type is not an object reference. + #[must_use] pub fn interface_name(&self) -> Option<&str> { match self { Ty::Interface(n) => Some(n), @@ -316,27 +382,9 @@ impl Ty { } } - /// The callback interface name inside a bare or optional callback - /// interface type, or `None` for every other type. - pub fn callback_interface_name(&self) -> Option<&str> { - match self { - Ty::CallbackInterface(n) => Some(n), - Ty::Optional(inner) => inner.callback_interface_name(), - _ => None, - } - } - - /// Classify this type's encoding inside a value buffer. - /// - /// Total over every type validation admits inside a buffered position. - /// Callback interfaces and iterators never appear inside value buffers - /// (validation rejects them there), so those inputs are bugs in the - /// caller's pipeline, not user errors. - /// - /// # Panics - /// - /// Panics when `self` is a callback interface or an iterator, neither of - /// which can legally appear inside a value buffer. + /// Classify this type's encoding inside a value buffer. Total over every + /// `Ty`. + #[must_use] pub fn wire(&self) -> WireType<'_> { match self { Ty::Prim(p) => WireType::Prim(*p), @@ -346,63 +394,126 @@ impl Ty { Ty::Optional(inner) => WireType::Optional(inner), Ty::List(inner) => WireType::List(inner), Ty::Map(k, v) => WireType::Map(k, v), - Ty::CallbackInterface(_) | Ty::Iterator(_) => { - panic!("{self} cannot appear inside value buffers") - } } } /// `true` when a value of this type needs a user-defined codec function /// somewhere in its encoding: it is (or transitively contains) a record /// or rich enum. + #[must_use] pub fn contains_user_type(&self) -> bool { self.any(&|t| matches!(t, Ty::Record(_) | Ty::RichEnum(_))) } /// `true` when this type is (or transitively contains) an interface, so /// encoding it needs object-token support in the codec. + #[must_use] pub fn contains_object(&self) -> bool { self.any(&|t| matches!(t, Ty::Interface(_))) } /// `true` when `pred` holds for this type or any type nested inside it - /// (optional payloads, list and iterator elements, map keys and values). + /// (optional payloads, list elements, map keys and values). pub fn any(&self, pred: &dyn Fn(&Ty) -> bool) -> bool { if pred(self) { return true; } match self { - Ty::Optional(inner) | Ty::List(inner) | Ty::Iterator(inner) => inner.any(pred), + Ty::Optional(inner) | Ty::List(inner) => inner.any(pred), Ty::Map(k, v) => k.any(pred) || v.any(pred), _ => false, } } +} - /// The element type of an `iter`, or `None` for any other type. +impl ParamTy { + /// The value type, or `None` for a callback interface. + #[must_use] + pub fn value(&self) -> Option<&Ty> { + match self { + ParamTy::Value(ty) => Some(ty), + ParamTy::Callback { .. } => None, + } + } + + /// The callback interface's name and nullability, or `None` for a value. + #[must_use] + pub fn callback(&self) -> Option<(&str, bool)> { + match self { + ParamTy::Callback { name, nullable } => Some((name, *nullable)), + ParamTy::Value(_) => None, + } + } +} + +impl RetTy { + /// The value type, or `None` for an iterator. + #[must_use] + pub fn value(&self) -> Option<&Ty> { + match self { + RetTy::Value(ty) => Some(ty), + RetTy::Iterator(_) => None, + } + } + + /// The element type of an `iter`, or `None` for a value. + #[must_use] pub fn iterator_elem(&self) -> Option<&Ty> { match self { - Ty::Iterator(inner) => Some(inner), - _ => None, + RetTy::Iterator(elem) => Some(elem), + RetTy::Value(_) => None, + } + } + + /// The value type, or an iterator's element type: the type every value + /// this return produces has. + #[must_use] + pub fn elem(&self) -> &Ty { + match self { + RetTy::Value(ty) | RetTy::Iterator(ty) => ty, } } } impl fmt::Display for Ty { /// Renders the IDL spelling (`i32`, `[string]`, `{string:i32}`, - /// `Contact?`, `iter`), which is what diagnostics and generated - /// doc comments quote. + /// `Contact?`), which is what diagnostics, contract signatures, and + /// generated doc comments quote. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Ty::Prim(p) => f.write_str(p.snake()), - Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Enum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(n) => f.write_str(n), + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::Interface(n) => f.write_str(n), Ty::Optional(inner) => write!(f, "{inner}?"), Ty::List(inner) => write!(f, "[{inner}]"), Ty::Map(k, v) => write!(f, "{{{k}:{v}}}"), - Ty::Iterator(inner) => write!(f, "iter<{inner}>"), + } + } +} + +impl fmt::Display for ParamTy { + /// Renders the IDL spelling: the value type's, or the callback + /// interface's name (with `?` when nullable). + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParamTy::Value(ty) => ty.fmt(f), + ParamTy::Callback { + name, + nullable: false, + } => f.write_str(name), + ParamTy::Callback { + name, + nullable: true, + } => write!(f, "{name}?"), + } + } +} + +impl fmt::Display for RetTy { + /// Renders the IDL spelling: the value type's, or `iter`. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RetTy::Value(ty) => ty.fmt(f), + RetTy::Iterator(elem) => write!(f, "iter<{elem}>"), } } } @@ -420,6 +531,8 @@ pub enum TypeKind { Interface, /// A `callback_interfaces:` entry. CallbackInterface, + /// An `errors:` entry (error domain names share the type namespace). + ErrorDomain, } /// Where a user type is declared. @@ -431,7 +544,8 @@ pub struct TypeDecl { /// [`Model::modules`](crate::model::Model::modules). pub module: usize, /// Position of the declaration in its module's list for its kind - /// (`structs`, `enums`, `interfaces`, or `callback_interfaces`). + /// (`structs`, `enums`, `interfaces`, `callback_interfaces`, or + /// `errors`). pub index: usize, } @@ -439,13 +553,19 @@ pub struct TypeDecl { /// /// Type names are unique across the whole API, so a bare name identifies /// one declaration. The [`Model`](crate::model::Model) owns the index and -/// exposes typed lookups (`interface`, `record`, `owner`, and so on) on top -/// of it. +/// exposes typed lookups (`interface`, `record`, `error_domain`, `owner`, +/// and so on) on top of it. +/// +/// The index also records the **foreign** names a scoped validation +/// accepted ([`Options::foreign`](crate::validate::Options::foreign)): +/// records or rich enums declared outside the validated document, which +/// resolve to [`Ty::Record`] and have no declaration here. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct TypeIndex { decls: BTreeMap, /// Underscore-joined C path of every module, by position. module_paths: Vec, + foreign: BTreeSet, } impl TypeIndex { @@ -462,20 +582,50 @@ impl TypeIndex { self.decls.entry(name.to_string()).or_insert(decl); } - /// The declaration `name` refers to, or `None` for an undeclared name. + /// Record the foreign names a scoped validation accepts. A name declared + /// in the document stays its declaration. + pub(crate) fn set_foreign(&mut self, names: &BTreeSet) { + self.foreign = names + .iter() + .filter(|n| !self.decls.contains_key(n.as_str())) + .cloned() + .collect(); + } + + /// The declaration `name` refers to, or `None` for an undeclared (or + /// foreign) name. + #[must_use] pub fn get(&self, name: &str) -> Option<&TypeDecl> { self.decls.get(name) } /// The kind of the declaration `name` refers to. + #[must_use] pub fn kind(&self, name: &str) -> Option { self.get(name).map(|d| d.kind) } + /// `true` when `name` is a foreign record or rich enum: declared outside + /// the validated document and accepted through + /// [`Options::foreign`](crate::validate::Options::foreign). + #[must_use] + pub fn is_foreign(&self, name: &str) -> bool { + self.foreign.contains(name) + } + + /// Every foreign name, in sorted order. + pub fn foreign(&self) -> impl Iterator { + self.foreign.iter().map(String::as_str) + } + /// The underscore-joined C path of the module declaring `name` (the - /// `outer_inner` in `{prefix}_outer_inner_Name`). + /// `outer_inner` in `{prefix}_outer_inner_Name`), or `None` for an + /// undeclared or foreign name. + #[must_use] pub fn module_path(&self, name: &str) -> Option<&str> { - self.get(name).map(|d| self.module_paths[d.module].as_str()) + self.get(name) + .and_then(|d| self.module_paths.get(d.module)) + .map(String::as_str) } } @@ -486,6 +636,14 @@ mod tests { const I32: Ty = Ty::Prim(Prim::I32); const STRING: Ty = Ty::Prim(Prim::String); + fn opt(t: Ty) -> Ty { + Ty::Optional(Box::new(t)) + } + + fn list(t: Ty) -> Ty { + Ty::List(Box::new(t)) + } + #[test] fn families_are_total_and_agree_with_the_abi_contract() { assert_eq!(I32.family(), Family::Direct); @@ -493,15 +651,33 @@ mod tests { assert_eq!(Ty::Enum("Color".into()).family(), Family::Direct); assert_eq!(STRING.family(), Family::String); assert_eq!(Ty::Prim(Prim::Bytes).family(), Family::Bytes); + for ty in [ + opt(I32), + opt(Ty::Prim(Prim::Bool)), + opt(Ty::Prim(Prim::F64)), + opt(Ty::Prim(Prim::U8)), + opt(Ty::Enum("Color".into())), + ] { + assert_eq!(ty.family(), Family::OptDirect, "{ty}"); + } + for p in Prim::SLICE_ELEMS { + assert_eq!(list(Ty::Prim(p)).family(), Family::Slice(p)); + } for ty in [ Ty::Record("C".into()), Ty::RichEnum("S".into()), - Ty::List(Box::new(I32)), - Ty::List(Box::new(Ty::Interface("Store".into()))), + list(Ty::Prim(Prim::Bool)), + list(Ty::Prim(Prim::U8)), + list(STRING), + list(list(I32)), + list(opt(I32)), + list(Ty::Enum("Color".into())), + list(Ty::Interface("Store".into())), Ty::Map(Box::new(STRING), Box::new(I32)), - Ty::Optional(Box::new(I32)), - Ty::Optional(Box::new(STRING)), - Ty::Optional(Box::new(Ty::Record("C".into()))), + opt(STRING), + opt(opt(I32)), + opt(list(I32)), + opt(Ty::Record("C".into())), ] { assert_eq!(ty.family(), Family::Buffer, "{ty}"); assert!(ty.is_buffered()); @@ -511,18 +687,9 @@ mod tests { Family::Object { nullable: false } ); assert_eq!( - Ty::Optional(Box::new(Ty::Interface("Store".into()))).family(), + opt(Ty::Interface("Store".into())).family(), Family::Object { nullable: true } ); - assert_eq!( - Ty::CallbackInterface("Listener".into()).family(), - Family::Callback { nullable: false } - ); - assert_eq!( - Ty::Optional(Box::new(Ty::CallbackInterface("Listener".into()))).family(), - Family::Callback { nullable: true } - ); - assert_eq!(Ty::Iterator(Box::new(I32)).family(), Family::Iterator); } #[test] @@ -536,50 +703,74 @@ mod tests { ); assert_eq!(Ty::RichEnum("Shape".into()).wire(), WireType::User("Shape")); assert_eq!(Ty::Enum("Color".into()).wire(), WireType::Enum("Color")); - let list = Ty::List(Box::new(I32)); - assert_eq!(list.wire(), WireType::List(&I32)); - } - - #[test] - #[should_panic(expected = "cannot appear inside value buffers")] - fn callback_interfaces_have_no_wire_shape() { - Ty::CallbackInterface("Listener".into()).wire(); + let l = list(I32); + assert_eq!(l.wire(), WireType::List(&I32)); + let o = opt(I32); + assert_eq!(o.wire(), WireType::Optional(&I32)); } #[test] fn containment_recurses() { assert!(Ty::Record("C".into()).contains_user_type()); - assert!(Ty::Map( - Box::new(STRING), - Box::new(Ty::Optional(Box::new(Ty::RichEnum("S".into())))) - ) - .contains_user_type()); - assert!(!Ty::List(Box::new(Ty::Enum("Color".into()))).contains_user_type()); - assert!(Ty::List(Box::new(Ty::Interface("Store".into()))).contains_object()); - assert!(!Ty::List(Box::new(Ty::Record("C".into()))).contains_object()); + assert!( + Ty::Map(Box::new(STRING), Box::new(opt(Ty::RichEnum("S".into())))).contains_user_type() + ); + assert!(!list(Ty::Enum("Color".into())).contains_user_type()); + assert!(list(Ty::Interface("Store".into())).contains_object()); + assert!(!list(Ty::Record("C".into())).contains_object()); } #[test] fn display_is_the_idl_spelling() { - let ty = Ty::List(Box::new(Ty::Map( + let ty = list(Ty::Map( Box::new(STRING), - Box::new(Ty::Optional(Box::new(Ty::Record("Contact".into())))), - ))); + Box::new(opt(Ty::Record("Contact".into()))), + )); assert_eq!(ty.to_string(), "[{string:Contact?}]"); assert_eq!( - Ty::Iterator(Box::new(Ty::Interface("Store".into()))).to_string(), + RetTy::Iterator(Ty::Interface("Store".into())).to_string(), "iter" ); + assert_eq!(RetTy::Value(opt(I32)).to_string(), "i32?"); + assert_eq!( + ParamTy::Callback { + name: "Listener".into(), + nullable: true + } + .to_string(), + "Listener?" + ); + assert_eq!(ParamTy::Value(list(I32)).to_string(), "[i32]"); assert_eq!(Prim::String.snake(), "string"); assert_eq!(Prim::I32.pascal(), "I32"); } #[test] - fn primitive_names_round_trip() { + fn position_accessors() { + let cb = ParamTy::Callback { + name: "L".into(), + nullable: false, + }; + assert_eq!(cb.callback(), Some(("L", false))); + assert_eq!(cb.value(), None); + assert_eq!(ParamTy::Value(I32).value(), Some(&I32)); + let it = RetTy::Iterator(I32); + assert_eq!(it.iterator_elem(), Some(&I32)); + assert_eq!(it.value(), None); + assert_eq!(it.elem(), &I32); + assert_eq!(RetTy::Value(STRING).elem(), &STRING); + } + + #[test] + fn primitive_names_and_sizes() { for p in Prim::ALL { assert_eq!(Prim::from_name(p.snake()), Some(p)); + assert_eq!(p.size().is_some(), p.is_scalar()); } assert_eq!(Prim::from_name("usize"), None); assert!(Prim::U8.is_integer() && !Prim::F64.is_integer() && !Prim::Bool.is_numeric()); + assert!(!Prim::U8.is_slice_elem() && !Prim::Bool.is_slice_elem()); + assert_eq!(Prim::F64.size(), Some(8)); + assert_eq!(Prim::U16.size(), Some(2)); } } diff --git a/crates/weaveffi-model/src/validate.rs b/crates/weaveffi-model/src/validate.rs index 68e53b09..92227dbe 100644 --- a/crates/weaveffi-model/src/validate.rs +++ b/crates/weaveffi-model/src/validate.rs @@ -9,11 +9,11 @@ //! invocation. use crate::ir::{Api, SUPPORTED_VERSIONS}; -use crate::model::{Model, RESERVED_SYMBOL_FAMILIES, RUNTIME_SYMBOLS}; +use crate::model::{Model, SymbolOwner, RESERVED_SYMBOL_FAMILIES}; use crate::pkg::Identity; #[cfg(feature = "idl")] use miette::Diagnostic; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; mod diagnostic; mod rules; @@ -210,6 +210,29 @@ pub enum ValidationError { /// The declaration that collided with it. second: String, }, + /// Two C slots of one lowered signature share a name: a parameter's + /// slot collides with another parameter's (a `string` parameter `name` + /// lowers to `name_ptr`, which collides with a parameter `name_ptr`; an + /// optional `x` adds `has_x`) or with a slot the ABI adds (`self`, + /// `ctx`, `out_err`, `out_len`, `out_value`, `callback`, `context`, + /// `cancel_token`). + #[error("C slot collision in '{function}': two slots are named '{slot}'")] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "a parameter lowers to C slots named after it (`x`, `has_x`, `x_ptr`, `x_len`, \ + `x_ctx`, `x_vtable`) next to the slots the ABI adds (`self`, `ctx`, `out_err`, \ + `out_len`, `out_value`, `out_ptr`, `callback`, `context`, `cancel_token`); rename the \ + parameter" + )) + )] + SlotCollision { + /// The callable's dotted path (`m.f`, `m.Store.get`, + /// `m.Listener.on_event`). + function: String, + /// The colliding slot name. + slot: String, + }, /// Two parameters of one function share a name. #[error("duplicate param name in function '{function}' of module '{module}': {param}")] #[cfg_attr( @@ -241,22 +264,23 @@ pub enum ValidationError { /// The offending function. function: String, }, - /// A function or callback-interface method declares `throws: true` but - /// no error domain is in scope. - #[error("function '{module}::{function}' declares throws but no error domain is in scope")] + /// A callable's `throws` names an error domain that doesn't exist. + #[error("'{module}::{function}' throws '{domain}', which is not an error domain")] #[cfg_attr( feature = "idl", diagnostic(help( - "a throwing function (or callback-interface method) reports codes from its \ - module's error domain; declare an `errors:` block on this module (or an ancestor \ - module), or remove `throws: true`" + "`throws` names an error domain declared in any module's `errors:` list (domain \ + names are global), or is `any` for an untyped error; declare the domain or fix the \ + name" )) )] - ThrowsWithoutErrorDomain { - /// Module that contains the function. + UnknownErrorDomain { + /// Module that contains the callable. module: String, - /// Function marked `throws` with no domain in scope. + /// The callable (`f`, `Store.get`, or `Listener.on_event`). function: String, + /// The name `throws` gave. + domain: String, }, /// An async function tries to return an iterator, which has no async ABI. #[error("async function '{module}::{function}' cannot return an iterator")] @@ -284,7 +308,7 @@ pub enum ValidationError { module: String, }, /// Two error codes in one domain share a numeric value. - #[error("duplicate error numeric code in module '{module}': {value}")] + #[error("duplicate error numeric code in error domain '{module}.{domain}': {value}")] #[cfg_attr( feature = "idl", diagnostic(help( @@ -294,6 +318,8 @@ pub enum ValidationError { DuplicateErrorCode { /// Module that declares the error domain. module: String, + /// The error domain. + domain: String, /// Conflicting numeric error code. value: i32, }, @@ -518,6 +544,19 @@ pub enum ValidationError { /// Unresolved type name. name: String, }, + /// A type reference names an error domain, which isn't a value type. + #[error("error domain '{name}' is not a value type")] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "an error domain is what a callable's `throws` names; it can't be a parameter, \ + return, or field type. Declare a record or enum for the value instead" + )) + )] + ErrorDomainAsType { + /// The error domain's name. + name: String, + }, /// A type reference is module-qualified (`a.b.T`). #[error("qualified type reference '{name}': type names are global, so refer to a type by its bare name")] #[cfg_attr( @@ -556,8 +595,8 @@ pub enum ValidationError { #[cfg_attr( feature = "idl", diagnostic(help( - "interface objects may appear anywhere except as map keys; key a map by a scalar, \ - string, or C-style enum instead" + "interface objects may appear anywhere except as map keys (and in error payloads); key \ + a map by a scalar, string, or C-style enum instead" )) )] InterfaceInInvalidPosition { @@ -584,6 +623,23 @@ pub enum ValidationError { /// Position where the reference appeared. location: String, }, + /// An error code's payload field carries an interface object, directly or + /// through a record or rich enum. + #[error("error payloads can't carry objects, but {location} reaches '{name}'")] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "error payloads are plain data: error domains are open, so a consumer generated \ + before a code existed discards its payload without decoding it, and an object inside \ + would never be released; carry an identifier or a record of plain data instead" + )) + )] + ObjectInErrorPayload { + /// The interface, or the record or rich enum that carries one. + name: String, + /// Position where the reference appeared. + location: String, + }, /// An iterator type appears somewhere other than a function return. #[error("iterator type is only valid as a function return type, found in {location}")] #[cfg_attr(feature = "idl", diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields")))] @@ -669,14 +725,14 @@ impl Diagnostic for ValidationDiagnostics { /// Returns [`ValidationDiagnostics`] carrying one [`ValidationDiagnostic`] /// per violation: an unsupported schema version, a duplicate or invalid name, /// an unknown, qualified, or misplaced type, an empty struct or enum, a -/// `throws` without an error domain, a C symbol collision, or any other rule -/// violation in the catalog above. +/// `throws` naming no error domain, a C symbol or slot collision, or any +/// other rule violation in the catalog above. pub fn validate( api: &Api, identity: &Identity, source: Option<(&str, &str)>, ) -> Result { - validate_scoped(api, identity, Options::default()).map_err(|errors| ValidationDiagnostics { + validate_scoped(api, identity, &Options::default()).map_err(|errors| ValidationDiagnostics { diagnostics: errors .into_iter() .map(|(error, scope)| ValidationDiagnostic::new(error, &scope, source)) @@ -685,14 +741,21 @@ pub fn validate( } /// Options for [`validate_scoped`]. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Options { - /// Accept a type name that no declaration in the document provides, - /// assuming it's a record or rich enum declared elsewhere, and resolve - /// it to [`Ty::Record`](crate::ty::Ty::Record). The `#[weaveffi::module]` - /// macro sets this: it validates one module tree at a time, and asserts - /// at compile time that each such name really crosses as a value buffer. - pub foreign_names: bool, + /// Type names declared outside the validated document that it may + /// reference: records or rich enums from another module tree, which + /// cross as value buffers. Each resolves to + /// [`Ty::Record`](crate::ty::Ty::Record) and is recorded in the model's + /// [`TypeIndex`](crate::ty::TypeIndex) as foreign + /// ([`TypeIndex::is_foreign`](crate::ty::TypeIndex::is_foreign)). A + /// name the document declares itself is its own declaration. + /// + /// The `#[weaveffi::module]` macro validates one module tree at a time: + /// it passes [`Api::undeclared_type_names`] here and asserts at compile + /// time that each such name really crosses as a value buffer. Any other + /// undeclared name is an [`UnknownTypeRef`](ValidationError::UnknownTypeRef). + pub foreign: BTreeSet, } /// A rule violation paired with the declaration path that encloses it @@ -712,7 +775,7 @@ pub type Found = (ValidationError, Vec); pub fn validate_scoped( api: &Api, identity: &Identity, - options: Options, + options: &Options, ) -> Result> { if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) { // A wrong-schema document is checked no further: the rules below @@ -725,16 +788,21 @@ pub fn validate_scoped( vec![], )]); } - let types = crate::model::index(api); + let mut types = crate::model::index(api); + types.set_foreign(&options.foreign); let mut found = Vec::new(); - rules::check(api, &types, options, &mut found); + rules::check(api, &types, &mut found); if !found.is_empty() { // The model's lowering assumes every rule holds, so the symbol // table is only built for an otherwise valid document. return Err(found); } - let model = crate::model::build_indexed(api, identity.clone(), types); + let model = match crate::model::build(api, identity.clone(), types) { + Ok(model) => model, + Err(error) => return Err(vec![(error, vec![])]), + }; check_symbol_collisions(&model, &mut found); + check_slot_collisions(&model, &mut found); if found.is_empty() { Ok(model) } else { @@ -746,34 +814,82 @@ pub fn validate_scoped( /// family the generated C value-buffer helpers reserve. fn check_symbol_collisions(model: &Model, found: &mut Vec) { let prefix = model.prefix(); - let mut seen: BTreeMap = BTreeMap::new(); - for (i, (symbol, origin)) in model.c_symbols().into_iter().enumerate() { - let family = symbol.strip_prefix(&format!("{prefix}_")).and_then(|rest| { - RESERVED_SYMBOL_FAMILIES - .iter() - .find(|f| rest.starts_with(*f)) - }); - if let (Some(family), true) = (family, i >= RUNTIME_SYMBOLS.len()) { - found.push(( - ValidationError::SymbolCollision { - symbol: symbol.clone(), - first: format!("the C value-buffer helpers ('{prefix}_{family}*')"), - second: origin.clone(), - }, - vec![], - )); + let mut seen: BTreeMap = BTreeMap::new(); + for symbol in model.c_symbols() { + if symbol.owner != SymbolOwner::Runtime { + let family = symbol + .name + .strip_prefix(&format!("{prefix}_")) + .and_then(|rest| { + RESERVED_SYMBOL_FAMILIES + .iter() + .find(|f| rest.starts_with(*f)) + }); + if let Some(family) = family { + found.push(( + ValidationError::SymbolCollision { + symbol: symbol.name.clone(), + first: format!("the C value-buffer helpers ('{prefix}_{family}*')"), + second: symbol.owner.to_string(), + }, + vec![], + )); + } } - match seen.get(&symbol) { + match seen.get(&symbol.name) { Some(first) => found.push(( ValidationError::SymbolCollision { - symbol, - first: first.clone(), - second: origin, + first: first.to_string(), + second: symbol.owner.to_string(), + symbol: symbol.name, }, vec![], )), None => { - seen.insert(symbol, origin); + seen.insert(symbol.name, symbol.owner); + } + } + } +} + +/// Reject any lowered signature with two slots of one name. +fn check_slot_collisions(model: &Model, found: &mut Vec) { + let mut check = |scope: Vec, slots: &[crate::abi::AbiParam]| { + let mut seen = BTreeSet::new(); + let mut reported = BTreeSet::new(); + for slot in slots { + if !seen.insert(slot.name.as_str()) && reported.insert(slot.name.as_str()) { + found.push(( + ValidationError::SlotCollision { + function: scope.join("."), + slot: slot.name.clone(), + }, + scope.clone(), + )); + } + } + }; + for m in &model.modules { + let callable = |owner: &[&str], f: &crate::model::FnBinding| { + let mut scope = m.segments.clone(); + scope.extend(owner.iter().map(|s| (*s).to_string())); + scope.push(f.name.clone()); + scope + }; + for f in &m.functions { + check(callable(&[], f), &f.abi.params); + } + for i in &m.interfaces { + for f in i.members() { + check(callable(&[&i.name], f), &f.abi.params); + } + } + for c in &m.callback_interfaces { + for method in &c.methods { + let mut scope = m.segments.clone(); + scope.push(c.name.clone()); + scope.push(method.name.clone()); + check(scope, &method.abi.params); } } } diff --git a/crates/weaveffi-model/src/validate/diagnostic.rs b/crates/weaveffi-model/src/validate/diagnostic.rs index 1f1d0f80..55652fa3 100644 --- a/crates/weaveffi-model/src/validate/diagnostic.rs +++ b/crates/weaveffi-model/src/validate/diagnostic.rs @@ -188,8 +188,9 @@ impl ValidationError { | Self::DuplicateInterfaceMember { name, .. } | Self::DuplicateCallbackMethod { name, .. } => Second(name), Self::CancellableNotAsync { .. } - | Self::ThrowsWithoutErrorDomain { .. } - | Self::AsyncIteratorReturn { .. } => Scope, + | Self::AsyncIteratorReturn { .. } + | Self::SlotCollision { .. } => Scope, + Self::UnknownErrorDomain { domain, .. } => Text(domain), Self::DuplicateEnumValue { enum_name, .. } => Decl(enum_name), Self::ConstructorHasReturn { constructor, .. } | Self::AsyncConstructor { constructor, .. } => Decl(constructor), @@ -197,8 +198,10 @@ impl ValidationError { Self::UnknownTypeRef { name } | Self::QualifiedTypeRef { name } | Self::UnsupportedPrimitive { name } + | Self::ErrorDomainAsType { name } | Self::InterfaceInInvalidPosition { name, .. } - | Self::CallbackInterfaceInInvalidPosition { name, .. } => Text(name), + | Self::CallbackInterfaceInInvalidPosition { name, .. } + | Self::ObjectInErrorPayload { name, .. } => Text(name), Self::InvalidMapKey { key_type } => Text(key_type), Self::IteratorInInvalidPosition { .. } => Text("iter"), Self::NoModuleName @@ -265,15 +268,15 @@ fn find_token(src: &str, from: usize, needle: &str) -> Option { } /// The byte offset of the first occurrence of `name` at or after `from` that -/// is the value of a `name` key, in YAML (`name: x`), JSON -/// (`"name": "x"`), or TOML (`name = "x"`) syntax. +/// is the value of a `name` key, in YAML (`name: x`) or JSON +/// (`"name": "x"`) syntax. #[cfg(feature = "idl")] fn find_decl(src: &str, from: usize, name: &str) -> Option { let mut at = from; loop { let hit = find_token(src, at, name)?; let before = src[..hit].trim_end_matches(['"', '\'']).trim_end(); - if let Some(key) = before.strip_suffix([':', '=']) { + if let Some(key) = before.strip_suffix(':') { let key = key.trim_end().trim_end_matches(['"', '\'']); if key .strip_suffix("name") diff --git a/crates/weaveffi-model/src/validate/rules.rs b/crates/weaveffi-model/src/validate/rules.rs index 87c83719..c8e11356 100644 --- a/crates/weaveffi-model/src/validate/rules.rs +++ b/crates/weaveffi-model/src/validate/rules.rs @@ -9,9 +9,10 @@ //! declaration and member names), which the diagnostic uses to locate the //! offending text. -use super::{Found, Options, ValidationError}; +use super::{Found, ValidationError}; use crate::ir::{ - Api, CallbackInterfaceDef, ErrorDomain, Function, InterfaceDef, Module, StructField, TypeRef, + Api, CallbackInterfaceDef, ErrorDomain, Function, InterfaceDef, Module, StructField, Throws, + TypeRef, }; use crate::ty::{Prim, TypeIndex, TypeKind}; use std::collections::{BTreeMap, BTreeSet}; @@ -46,10 +47,11 @@ fn within(scope: &[String], names: &[&str]) -> Vec { } /// Run every rule over `api`, whose declarations `types` indexes. -pub(super) fn check(api: &Api, types: &TypeIndex, options: Options, found: &mut Vec) { +pub(super) fn check(api: &Api, types: &TypeIndex, found: &mut Vec) { + let carriers = object_carriers(&api.modules, types); let mut cx = Cx { types, - options, + carriers: &carriers, found, }; let mut names = BTreeSet::new(); @@ -62,7 +64,7 @@ pub(super) fn check(api: &Api, types: &TypeIndex, options: Options, found: &mut }, ); } - cx.module(m, &[], false); + cx.module(m, &[]); } check_global_names(&api.modules, found); } @@ -130,7 +132,7 @@ fn check_global_names(modules: &[Module], found: &mut Vec) { seen.all_functions .push((&f.name, path.clone(), segments.clone())); } - if let Some(domain) = &m.errors { + for domain in &m.errors { seen.domains.entry(&domain.name).or_insert(path.clone()); let owner = format!("{path}.{}", domain.name); for code in &domain.codes { @@ -168,10 +170,12 @@ fn check_global_names(modules: &[Module], found: &mut Vec) { } } -/// The rule context: the type index, the options, and the violation sink. +/// The rule context: the type index and the violation sink. struct Cx<'a> { types: &'a TypeIndex, - options: Options, + /// Records and rich enums that carry an interface object somewhere in + /// their fields (see [`object_carriers`]). + carriers: &'a BTreeSet, found: &'a mut Vec, } @@ -204,7 +208,7 @@ impl Cx<'_> { self.types.kind(name) } - fn module(&mut self, module: &Module, parent: &[String], ancestor_has_domain: bool) { + fn module(&mut self, module: &Module, parent: &[String]) { if module.name.trim().is_empty() { self.push(parent, ValidationError::NoModuleName); return; @@ -228,10 +232,9 @@ impl Cx<'_> { } let scope = within(parent, &[&module.name]); let path = scope.join("."); - let has_domain = ancestor_has_domain || module.errors.is_some(); for f in &module.functions { - self.function(&scope, &path, &f.name, f, has_domain); + self.function(&scope, &path, &f.name, f); self.callable_types(&within(&scope, &[&f.name]), &path, &f.name, f); } @@ -321,15 +324,15 @@ impl Cx<'_> { for i in &module.interfaces { self.identifier(&scope, &i.name); - self.interface(&scope, &path, i, has_domain); + self.interface(&scope, &path, i); } for cb in &module.callback_interfaces { self.identifier(&scope, &cb.name); - self.callback_interface(&scope, &path, cb, has_domain); + self.callback_interface(&scope, &path, cb); } - if let Some(domain) = &module.errors { + for domain in &module.errors { self.error_domain(&scope, &path, domain); } @@ -343,7 +346,7 @@ impl Cx<'_> { }, ); } - self.module(sub, &scope, has_domain); + self.module(sub, &scope); } } @@ -370,7 +373,7 @@ impl Cx<'_> { /// constructors, methods, and statics; constructor restrictions; /// per-member signature rules. C symbol collisions are checked API-wide /// once the model is built. - fn interface(&mut self, scope: &[String], path: &str, iface: &InterfaceDef, has_domain: bool) { + fn interface(&mut self, scope: &[String], path: &str, iface: &InterfaceDef) { if iface.constructors.is_empty() && iface.methods.is_empty() && iface.statics.is_empty() { self.push( scope, @@ -399,7 +402,7 @@ impl Cx<'_> { ); } let display = format!("{}.{}", iface.name, f.name); - self.function(&decl, path, &display, f, has_domain); + self.function(&decl, path, &display, f); self.callable_types(&within(&decl, &[&f.name]), path, &display, f); if constructor && f.returns.is_some() { self.push( @@ -427,14 +430,8 @@ impl Cx<'_> { /// by the consumer, so it is synchronous, never takes a cancel token, /// returns nothing or any value but an iterator or a callback interface, /// takes no callback interface or iterator as a parameter, and may - /// declare `throws` only when an error domain is in scope. - fn callback_interface( - &mut self, - scope: &[String], - path: &str, - cb: &CallbackInterfaceDef, - has_domain: bool, - ) { + /// throw only an error domain that exists (or `any`). + fn callback_interface(&mut self, scope: &[String], path: &str, cb: &CallbackInterfaceDef) { if cb.methods.is_empty() { self.push( scope, @@ -474,15 +471,7 @@ impl Cx<'_> { reject(self, "cannot be cancellable"); } let method = within(&decl, &[&m.name]); - if m.throws && !has_domain { - self.push( - &method, - ValidationError::ThrowsWithoutErrorDomain { - module: path.to_string(), - function: format!("{}.{}", cb.name, m.name), - }, - ); - } + self.throws(&method, path, &format!("{}.{}", cb.name, m.name), m); if let Some(ret) = &m.returns { let location = || format!("return type of {path}::{}.{}", cb.name, m.name); if contains_iterator(ret) { @@ -528,16 +517,9 @@ impl Cx<'_> { } /// Name-level checks for one callable declared in `scope`: a valid - /// identifier, unique parameter names, and an error domain in scope when - /// the callable declares `throws`. - fn function( - &mut self, - scope: &[String], - path: &str, - display: &str, - f: &Function, - has_domain: bool, - ) { + /// identifier, unique parameter names, and an existing error domain when + /// the callable throws one. + fn function(&mut self, scope: &[String], path: &str, display: &str, f: &Function) { self.identifier(scope, &f.name); let decl = within(scope, &[&f.name]); if f.cancellable && !f.r#async { @@ -549,15 +531,7 @@ impl Cx<'_> { }, ); } - if f.throws && !has_domain { - self.push( - &decl, - ValidationError::ThrowsWithoutErrorDomain { - module: path.to_string(), - function: display.to_string(), - }, - ); - } + self.throws(&decl, path, display, f); let mut names = BTreeSet::new(); for p in &f.params { self.identifier(&decl, &p.name); @@ -574,6 +548,23 @@ impl Cx<'_> { } } + /// Check that a callable's `throws` names an error domain (declared in + /// any module; domain names are global) or is `any`. + fn throws(&mut self, scope: &[String], path: &str, display: &str, f: &Function) { + if let Some(Throws::Domain(domain)) = &f.throws { + if self.kind(domain) != Some(TypeKind::ErrorDomain) { + self.push( + scope, + ValidationError::UnknownErrorDomain { + module: path.to_string(), + function: display.to_string(), + domain: domain.clone(), + }, + ); + } + } + } + /// Type-level checks for one callable's parameters and return: iterator /// positions, async-iterator exclusion, reference resolution, element /// shapes, interface map keys, and callback-interface positions. `scope` @@ -672,6 +663,35 @@ impl Cx<'_> { } } + /// Reject an error-code field that carries an interface object, directly + /// or through a record or rich enum. Error domains are open: a consumer + /// generated before the code existed discards its payload undecoded, so + /// an object token inside it would never be released. + fn no_object_in_payload( + &mut self, + scope: &[String], + f: &StructField, + location: &dyn Fn() -> String, + ) { + let mut names = Vec::new(); + f.ty.walk(&mut |t| { + if let TypeRef::Named(name) = t { + names.push(name.clone()); + } + }); + for name in names { + if self.kind(&name) == Some(TypeKind::Interface) || self.carriers.contains(&name) { + self.push( + &within(scope, &[&f.name]), + ValidationError::ObjectInErrorPayload { + name, + location: location(), + }, + ); + } + } + } + /// Reject any callback-interface reference reachable from `ty`. Callers /// that permit a bare top-level callback interface unwrap it first. fn no_callback_interface( @@ -706,13 +726,14 @@ impl Cx<'_> { TypeRef::Named(name) => { let error = if name.contains('.') { ValidationError::QualifiedTypeRef { name: name.clone() } - } else if self.kind(name).is_some() { + } else if self.kind(name) == Some(TypeKind::ErrorDomain) { + ValidationError::ErrorDomainAsType { name: name.clone() } + } else if self.kind(name).is_some() || self.types.is_foreign(name) { + // Declared here, or a record or rich enum from another + // module tree the caller named explicitly. return; } else if UNSUPPORTED_PRIMITIVES.contains(&name.as_str()) { ValidationError::UnsupportedPrimitive { name: name.clone() } - } else if self.options.foreign_names { - // A record or rich enum from another module tree. - return; } else { ValidationError::UnknownTypeRef { name: name.clone() } }; @@ -761,6 +782,16 @@ impl Cx<'_> { return; } self.identifier(scope, &domain.name); + if domain.name == "any" { + // `throws: any` means an untyped error, so no domain can be + // named `any`. + self.push( + scope, + ValidationError::ReservedKeyword { + name: domain.name.clone(), + }, + ); + } let decl = within(scope, &[&domain.name]); let mut values = BTreeSet::new(); for c in &domain.codes { @@ -783,6 +814,7 @@ impl Cx<'_> { &decl, ValidationError::DuplicateErrorCode { module: path.to_string(), + domain: domain.name.clone(), value: c.code, }, ); @@ -796,7 +828,52 @@ impl Cx<'_> { ) }; self.buffered_field(&code, f, &location); + self.no_object_in_payload(&code, f, &location); + } + } + } +} + +/// Every record and rich enum whose fields reach an interface object, directly +/// or through other records and rich enums (a fixed point over the whole API, +/// since types may reference each other in any order). +fn object_carriers(modules: &[Module], types: &TypeIndex) -> BTreeSet { + fn collect<'a>(modules: &'a [Module], out: &mut Vec<(&'a str, Vec<&'a TypeRef>)>) { + for m in modules { + for s in &m.structs { + out.push((&s.name, s.fields.iter().map(|f| &f.ty).collect())); + } + for e in &m.enums { + let fields = e.variants.iter().flat_map(|v| &v.fields); + out.push((&e.name, fields.map(|f| &f.ty).collect())); + } + collect(&m.modules, out); + } + } + let mut decls = Vec::new(); + collect(modules, &mut decls); + let mut carriers = BTreeSet::new(); + loop { + let before = carriers.len(); + for (name, fields) in &decls { + if carriers.contains(*name) { + continue; + } + let mut carries = false; + for ty in fields { + ty.walk(&mut |t| { + if let TypeRef::Named(n) = t { + carries |= + types.kind(n) == Some(TypeKind::Interface) || carriers.contains(n); + } + }); } + if carries { + carriers.insert((*name).to_string()); + } + } + if carriers.len() == before { + return carriers; } } } diff --git a/crates/weaveffi-model/src/validate/tests.rs b/crates/weaveffi-model/src/validate/tests.rs index af754190..51f872a5 100644 --- a/crates/weaveffi-model/src/validate/tests.rs +++ b/crates/weaveffi-model/src/validate/tests.rs @@ -82,15 +82,16 @@ modules: - { name: by_status, type: "{Status:i32}" } - name: geo errors: - name: GeoError - codes: - - { name: NotFound, code: 1, message: "missing" } - - { name: Bad, code: 2, message: "bad", fields: [{ name: why, type: string }] } + - name: GeoError + codes: + - { name: NotFound, code: 1, message: "missing" } + - { name: Bad, code: 2, message: "bad", fields: [{ name: why, type: string }] } interfaces: - name: Store constructors: [{ name: open, params: [{ name: path, type: string }] }] methods: - - { name: get, params: [{ name: k, type: string }], return: "Point?", throws: true } + - { name: get, params: [{ name: k, type: string }], return: "Point?", throws: GeoError } + - { name: nearest, params: [{ name: limit, type: "u32?" }, { name: weights, type: "[f64]" }], return: "[i32]" } - { name: scan, params: [], return: "iter" } - { name: stores, params: [], return: "iter" } - { name: sibling, params: [{ name: other, type: "Store?" }], return: Store } @@ -108,7 +109,9 @@ modules: params: [{ name: p, type: Point }, { name: raw, type: bytes }, { name: from, type: Store }] - { name: wants_more, params: [{ name: seen, type: u32 }], return: bool } - { name: status, params: [], return: Status } - - { name: label, params: [], return: string, throws: true } + - { name: label, params: [], return: string, throws: GeoError } + - { name: guess, params: [{ name: hint, type: "i64?" }], return: "f64?", throws: any } + - { name: samples, params: [{ name: xs, type: "[u16]" }], return: "[i16]" } - { name: nearest, params: [], return: "Point?" } - { name: store, params: [], return: "Store?" } - { name: payload, params: [], return: bytes } @@ -119,7 +122,11 @@ modules: modules: - name: inner functions: - - { name: fails, params: [], throws: true } + - { name: fails, params: [], throws: GeoError } + - { name: anything, params: [], return: "bool?", throws: any } + - { name: later, params: [], return: "[f32]", async: true, throws: any } + - { name: chunks, params: [], return: "iter<[i32]>" } + - { name: maybe, params: [], return: "iter" } "#, ]; for doc in ok { @@ -190,7 +197,7 @@ modules: r#" modules: - name: m - errors: { name: ' ', codes: [] } + errors: [{ name: ' ', codes: [] }] "#, ), ( @@ -199,10 +206,10 @@ modules: modules: - name: m errors: - name: E - codes: - - { name: A, code: 1, message: a } - - { name: A, code: 2, message: a } + - name: E + codes: + - { name: A, code: 1, message: a } + - { name: A, code: 2, message: a } "#, ), ( @@ -211,10 +218,10 @@ modules: modules: - name: m errors: - name: E - codes: - - { name: A, code: 1, message: a } - - { name: B, code: 1, message: b } + - name: E + codes: + - { name: A, code: 1, message: a } + - { name: B, code: 1, message: b } "#, ), ( @@ -223,8 +230,8 @@ modules: modules: - name: m errors: - name: E - codes: [{ name: A, code: 0, message: a }] + - name: E + codes: [{ name: A, code: 0, message: a }] "#, ), ( @@ -233,8 +240,8 @@ modules: modules: - name: m errors: - name: E - codes: [{ name: A, code: -2, message: a }] + - name: E + codes: [{ name: A, code: -2, message: a }] "#, ), ( @@ -243,7 +250,7 @@ modules: modules: - name: m functions: [{ name: E, params: [] }] - errors: { name: E, codes: [{ name: A, code: 1, message: a }] } + errors: [{ name: E, codes: [{ name: A, code: 1, message: a }] }] "#, ), ( @@ -251,7 +258,7 @@ modules: r#" modules: - name: m - errors: { name: E, codes: [{ name: A, code: 1, message: a }] } + errors: [{ name: E, codes: [{ name: A, code: 1, message: a }] }] - name: n functions: [{ name: E, params: [] }] "#, @@ -362,8 +369,111 @@ modules: "#, ), ( - "ThrowsWithoutErrorDomain", - "modules: [{ name: m, functions: [{ name: f, params: [], throws: true }] }]", + "UnknownErrorDomain", + "modules: [{ name: m, functions: [{ name: f, params: [], throws: Nope }] }]", + ), + ( + "UnknownErrorDomain", + r#" +modules: + - name: m + structs: [{ name: S, fields: [{ name: x, type: i32 }] }] + functions: [{ name: f, throws: S }] +"#, + ), + ( + "ErrorDomainAsType", + r#" +modules: + - name: m + errors: [{ name: E, codes: [{ name: A, code: 1, message: a }] }] + functions: [{ name: f, params: [{ name: e, type: "E?" }] }] +"#, + ), + ( + "ReservedKeyword", + "modules: [{ name: m, errors: [{ name: any, codes: [{ name: A, code: 1, message: a }] }] }]", + ), + ( + "DuplicateTypeName", + r#" +modules: + - name: a + errors: [{ name: Fault, codes: [{ name: A, code: 1, message: a }] }] + - name: b + interfaces: [{ name: Fault, methods: [{ name: m }] }] +"#, + ), + ( + "DuplicateTypeName", + r#" +modules: + - name: a + errors: + - { name: E, codes: [{ name: A, code: 1, message: a }] } + - { name: E, codes: [{ name: B, code: 1, message: b }] } +"#, + ), + ( + "SlotCollision", + r#" +modules: + - name: m + functions: [{ name: f, params: [{ name: name, type: string }, { name: name_ptr, type: i32 }] }] +"#, + ), + ( + "SlotCollision", + r#" +modules: + - name: m + functions: [{ name: f, params: [{ name: x, type: "i32?" }, { name: has_x, type: bool }] }] +"#, + ), + ( + "SlotCollision", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: out_err, type: i32 }] }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: out_len, type: i32 }], return: string }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: out_value, type: i32 }], return: 'i32?' }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: callback, type: i32 }], async: true }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: context, type: i32 }], async: true }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: cancel_token, type: i32 }], async: true, cancellable: true }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, interfaces: [{ name: I, methods: [{ name: f, params: [{ name: self, type: i32 }] }] }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, callback_interfaces: [{ name: L, methods: [{ name: f, params: [{ name: ctx, type: i32 }] }] }] }]", + ), + ( + "SlotCollision", + "modules: [{ name: m, callback_interfaces: [{ name: L, methods: [{ name: f, params: [{ name: out_ptr, type: i32 }], return: '[i32]' }] }] }]", + ), + ( + "SlotCollision", + r#" +modules: + - name: m + callback_interfaces: [{ name: L, methods: [{ name: a }] }] + functions: [{ name: f, params: [{ name: l, type: L }, { name: l_ctx, type: i32 }] }] +"#, ), ( "DuplicateTypeName", @@ -381,7 +491,7 @@ modules: modules: - name: a structs: [{ name: E, fields: [{ name: x, type: i32 }] }] - errors: { name: E, codes: [{ name: A, code: 1, message: a }] } + errors: [{ name: E, codes: [{ name: A, code: 1, message: a }] }] "#, ), ( @@ -389,9 +499,9 @@ modules: r#" modules: - name: a - errors: { name: AErr, codes: [{ name: NotFound, code: 1, message: a }] } + errors: [{ name: AErr, codes: [{ name: NotFound, code: 1, message: a }] }] - name: b - errors: { name: BErr, codes: [{ name: NotFound, code: 1, message: b }] } + errors: [{ name: BErr, codes: [{ name: NotFound, code: 1, message: b }] }] "#, ), ( @@ -521,6 +631,31 @@ modules: - name: m interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] structs: [{ name: S, fields: [{ name: m, type: "{I:i32}" }] }] +"#, + ), + ( + "ObjectInErrorPayload", + r#" +modules: + - name: m + interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] + errors: + - name: E + codes: [{ name: C, code: 1, message: c, fields: [{ name: o, type: "[I?]" }] }] +"#, + ), + ( + "ObjectInErrorPayload", + r#" +modules: + - name: m + interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] + structs: + - { name: Outer, fields: [{ name: inner, type: Inner }] } + - { name: Inner, fields: [{ name: o, type: I }] } + errors: + - name: E + codes: [{ name: C, code: 1, message: c, fields: [{ name: r, type: Outer }] }] "#, ), ( @@ -583,8 +718,56 @@ modules: "modules: [{ name: m, callback_interfaces: [{ name: L, methods: [{ name: a, params: [], async: true }] }] }]", ), ( - "ThrowsWithoutErrorDomain", - "modules: [{ name: m, callback_interfaces: [{ name: L, methods: [{ name: a, params: [], throws: true }] }] }]", + "UnknownErrorDomain", + "modules: [{ name: m, callback_interfaces: [{ name: L, methods: [{ name: a, params: [], throws: Nope }] }] }]", + ), + ( + "IteratorInInvalidPosition", + "modules: [{ name: m, callback_interfaces: [{ name: L, methods: [{ name: a, params: [{ name: x, type: '[iter]' }] }] }] }]", + ), + ( + "CallbackInterfaceInInvalidPosition", + r#" +modules: + - name: m + callback_interfaces: [{ name: L, methods: [{ name: a, params: [] }] }] + functions: [{ name: f, params: [], return: "iter" }] +"#, + ), + ( + "CallbackInterfaceInInvalidPosition", + r#" +modules: + - name: m + callback_interfaces: [{ name: L, methods: [{ name: a, params: [] }] }] + functions: [{ name: f, params: [], return: "L?", async: true }] +"#, + ), + ( + "CallbackInterfaceInInvalidPosition", + r#" +modules: + - name: m + callback_interfaces: [{ name: L, methods: [{ name: a, params: [] }] }] + functions: [{ name: f, params: [{ name: ls, type: "{string:L}" }] }] +"#, + ), + ( + "CallbackInterfaceInInvalidPosition", + r#" +modules: + - name: m + callback_interfaces: [{ name: L, methods: [{ name: a, params: [] }] }] + errors: [{ name: E, codes: [{ name: A, code: 1, message: a, fields: [{ name: l, type: "L?" }] }] }] +"#, + ), + ( + "IteratorInInvalidPosition", + "modules: [{ name: m, functions: [{ name: f, params: [], return: '{string:iter}' }] }]", + ), + ( + "IteratorInInvalidPosition", + "modules: [{ name: m, errors: [{ name: E, codes: [{ name: A, code: 1, message: a, fields: [{ name: x, type: 'iter' }] }] }] }]", ), ( "InvalidCallbackMethod", @@ -668,11 +851,11 @@ modules: ), ( "ReservedKeyword", - "modules: [{ name: m, errors: { name: 'if', codes: [{ name: A, code: 1, message: a }] } }]", + "modules: [{ name: m, errors: [{ name: 'if', codes: [{ name: A, code: 1, message: a }] }] }]", ), ( "InvalidIdentifier", - "modules: [{ name: m, errors: { name: E, codes: [{ name: '1bad', code: 1, message: a }] } }]", + "modules: [{ name: m, errors: [{ name: E, codes: [{ name: '1bad', code: 1, message: a }] }] }]", ), ]; for (expected, doc) in cases { @@ -707,14 +890,14 @@ modules: - name: m functions: - { name: f, params: [], return: Nope } - - { name: f, params: [], throws: true } + - { name: f, params: [], throws: Missing } structs: [{ name: S, fields: [] }] "#, ); for expected in [ "DuplicateFunctionName", "UnknownTypeRef", - "ThrowsWithoutErrorDomain", + "UnknownErrorDomain", "EmptyStruct", ] { assert!( @@ -749,10 +932,10 @@ modules: - { name: f, params: [] } - { name: f, params: [] } errors: - name: E - codes: - - { name: A, code: 1, message: a } - - { name: A, code: 2, message: a } + - name: E + codes: + - { name: A, code: 1, message: a } + - { name: A, code: 2, message: a } "#, ); assert_eq!( @@ -784,14 +967,14 @@ fn buffer_header_structs_and_codecs_are_reserved_symbols() { // and `_free`; a function spelled like any of them would redefine it. let cases = [ ( - "errors: { name: E, codes: [{ name: Busy, code: 1, message: b, fields: [{ name: n, type: i32 }] }] }\n functions: [{ name: E_Busy_payload }]", + "errors: [{ name: E, codes: [{ name: Busy, code: 1, message: b, fields: [{ name: n, type: i32 }] }] }]\n functions: [{ name: E_Busy_payload }]", "'kv_m_E_Busy_payload'", "the payload struct of error code 'm.E.Busy'", ), ( - "errors: { name: E, codes: [{ name: Busy, code: 1, message: b, fields: [{ name: n, type: i32 }] }] }\n functions: [{ name: E_Busy_payload_read }]", + "errors: [{ name: E, codes: [{ name: Busy, code: 1, message: b, fields: [{ name: n, type: i32 }] }] }]\n functions: [{ name: E_Busy_payload_read }]", "'kv_m_E_Busy_payload_read'", - "codec 'kv_m_E_Busy_payload_read'", + "codec 'read' of the payload struct of error code 'm.E.Busy'", ), ( "structs: [{ name: S, fields: [{ name: x, type: i32 }] }]\n functions: [{ name: S_free }]", @@ -814,7 +997,7 @@ fn buffer_header_structs_and_codecs_are_reserved_symbols() { } // An error code without fields has no payload struct. let doc = format!( - "version: \"{CURRENT_SCHEMA_VERSION}\"\nmodules:\n - name: m\n errors: {{ name: E, codes: [{{ name: Busy, code: 1, message: b }}] }}\n functions: [{{ name: E_Busy_payload }}]\n" + "version: \"{CURRENT_SCHEMA_VERSION}\"\nmodules:\n - name: m\n errors: [{{ name: E, codes: [{{ name: Busy, code: 1, message: b }}] }}]\n functions: [{{ name: E_Busy_payload }}]\n" ); let api = parse_api_str(&doc, "yaml").unwrap(); assert!(validate(&api, &Identity::named("kv"), None).is_ok()); @@ -894,12 +1077,24 @@ modules: "#, ) .unwrap(); - use crate::ty::Ty; + use crate::ty::{ParamTy, RetTy, Ty}; let f = &model.modules[1].functions[0]; - assert_eq!(f.params[0].ty, Ty::Interface("Store".into())); - assert_eq!(f.params[1].ty, Ty::Enum("Status".into())); - assert_eq!(f.params[2].ty, Ty::CallbackInterface("Watcher".into())); - assert_eq!(f.ret, Some(Ty::List(Box::new(Ty::Record("Point".into()))))); + assert_eq!( + f.params[0].ty, + ParamTy::Value(Ty::Interface("Store".into())) + ); + assert_eq!(f.params[1].ty, ParamTy::Value(Ty::Enum("Status".into()))); + assert_eq!( + f.params[2].ty, + ParamTy::Callback { + name: "Watcher".into(), + nullable: false + } + ); + assert_eq!( + f.ret, + Some(RetTy::Value(Ty::List(Box::new(Ty::Record("Point".into()))))) + ); assert_eq!(model.owner("Point").dot_path, "shared"); assert_eq!(model.prefix(), "api"); assert_eq!(model.version, CURRENT_SCHEMA_VERSION); @@ -942,3 +1137,43 @@ modules: assert!(warnings[0].to_string().contains("depth 4")); assert!(warnings[2].to_string().contains("use new")); } + +#[test] +fn slot_collisions_name_the_callable_and_slot() { + let err = check( + r#" +modules: + - name: m + interfaces: + - name: I + methods: + - { name: f, params: [{ name: name, type: string }, { name: name_len, type: u32 }] } +"#, + ) + .unwrap_err(); + assert_eq!(err.diagnostics.len(), 1); + assert_eq!( + err.to_string(), + "C slot collision in 'm.I.f': two slots are named 'name_len'" + ); +} + +#[test] +fn throws_spans_underline_the_domain_name() { + let doc = format!( + r#"version: "{CURRENT_SCHEMA_VERSION}" +modules: + - name: a + errors: + - name: Real + codes: [{{ name: A, code: 1, message: a }}] + functions: + - {{ name: ok, throws: Real }} + - {{ name: bad, throws: Nope }} +"# + ); + assert_eq!( + spans(&doc), + [("UnknownErrorDomain".to_string(), "9:Nope".to_string())] + ); +} diff --git a/crates/weaveffi/Cargo.toml b/crates/weaveffi/Cargo.toml index 7ce16461..2e5af2c3 100644 --- a/crates/weaveffi/Cargo.toml +++ b/crates/weaveffi/Cargo.toml @@ -12,23 +12,32 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-macros = { workspace = true } + +# Tokio has no multi-thread runtime on wasm32, where async calls are polled +# inline instead, so the dependency (and the `tokio` feature's effect) is +# native-only. +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", default-features = false, features = ["rt-multi-thread"], optional = true } [features] +default = ["tokio"] # Count live objects, callbacks, iterators, cancel tokens, and returned byte # allocations so `{prefix}_debug_live` can report leaks. Off by default # because every counted operation pays for an atomic update. Every sample # enables it. leak-check = [] -# Run exported `async fn`s on Tokio: the current runtime when a launcher is -# called from inside one, otherwise a multi-thread runtime created on first -# use. `set_spawner` still overrides it. +# Run exported `async fn`s on Tokio (on by default): the current runtime +# when a launcher is called from inside one, otherwise a multi-thread +# runtime created on first use. Without it, each call runs on a thread of +# its own. `set_spawner` overrides either. tokio = ["dep:tokio"] [dev-dependencies] # The leak-counter test needs counting on. A path-only self dependency turns -# the feature on for this crate's tests, and `cargo publish` strips it. -weaveffi = { path = ".", features = ["leak-check"] } +# the feature on for this crate's tests, and `cargo publish` strips it. It +# leaves the default features to the command line, so +# `--no-default-features` tests the thread-per-call executor. +weaveffi = { path = ".", default-features = false, features = ["leak-check"] } proptest = "1.11.0" tokio = { version = "1", features = ["rt-multi-thread", "time", "sync", "macros"] } diff --git a/crates/weaveffi/src/abi/buffer.rs b/crates/weaveffi/src/abi/buffer.rs index 10d5f83a..392c05d3 100644 --- a/crates/weaveffi/src/abi/buffer.rs +++ b/crates/weaveffi/src/abi/buffer.rs @@ -622,6 +622,55 @@ fixed_width_buffer_value! { f64 => (write_f64, read_f64), } +/// A `usize` encodes as a `u64` (the IDL's `u64`); decoding one past +/// `usize::MAX` fails. +impl BufferValue for usize { + fn encoded_len(&self) -> usize { + 8 + } + fn write_value(&self, w: &mut BufferWriter) { + w.write_u64(crate::abi::Scalar::to_abi(self)); + } + unsafe fn read_value(r: &mut BufferReader<'_>) -> Result { + usize::try_from(r.read_u64()?).map_err(|_| BufferDecodeError { + context: "u64 out of range for usize", + }) + } +} + +/// An `isize` encodes as an `i64` (the IDL's `i64`); decoding one outside +/// `isize`'s range fails. +impl BufferValue for isize { + fn encoded_len(&self) -> usize { + 8 + } + fn write_value(&self, w: &mut BufferWriter) { + w.write_i64(crate::abi::Scalar::to_abi(self)); + } + unsafe fn read_value(r: &mut BufferReader<'_>) -> Result { + isize::try_from(r.read_i64()?).map_err(|_| BufferDecodeError { + context: "i64 out of range for isize", + }) + } +} + +/// A `char` encodes as a one-scalar `string`; decoding any other string +/// fails. +impl BufferValue for char { + fn encoded_len(&self) -> usize { + 4 + self.len_utf8() + } + fn write_value(&self, w: &mut BufferWriter) { + w.write_string(self.encode_utf8(&mut [0; 4])); + } + unsafe fn read_value(r: &mut BufferReader<'_>) -> Result { + let text = r.read_string()?; + ::from_text(&text).ok_or(BufferDecodeError { + context: "string is not exactly one char", + }) + } +} + impl BufferValue for String { fn encoded_len(&self) -> usize { 4 + self.len() @@ -877,6 +926,18 @@ mod tests { roundtrip(-2.25f64); } + #[test] + fn sizes_and_chars_use_their_idl_encodings() { + roundtrip(usize::MAX); + roundtrip(isize::MIN); + roundtrip(vec![1usize, 2]); + roundtrip('\u{1F980}'); + assert_eq!(encode_value(&7usize), encode_value(&7u64)); + assert_eq!(encode_value(&'a'), encode_value(&"a".to_string())); + assert!(decode::(&encode_value(&"ab".to_string())).is_err()); + assert!(decode::(&encode_value(&String::new())).is_err()); + } + #[test] fn strings_roundtrip_including_interior_nul() { roundtrip(String::new()); diff --git a/crates/weaveffi/src/abi/callback.rs b/crates/weaveffi/src/abi/callback.rs index cf5fffc3..ed175f96 100644 --- a/crates/weaveffi/src/abi/callback.rs +++ b/crates/weaveffi/src/abi/callback.rs @@ -18,17 +18,37 @@ //! accepted, which lets a newer consumer pass methods this producer doesn't //! know yet. //! -//! A callback method returns `Result`. A failure the -//! consumer reports through `out_err` becomes the `Err`: a method that -//! `throws` keeps a declared code of its module's error domain (decode it -//! with [`ForeignError::domain`]), and every other failure has the code -//! [`FOREIGN_ERROR_CODE`]. +//! A vtable whose `flags` include [`VTABLE_THREAD_AFFINE`] may only have its +//! value-returning methods called on the thread that passed it to the +//! producer (a Dart isolate's callbacks, say). The runtime records that +//! thread when it adopts the vtable, and a generated wrapper calls +//! [`ForeignCallback::check_thread`] before such a method: called from +//! another thread, the method fails with [`FOREIGN_ERROR_CODE`] and the +//! message `callback called off its thread` without calling the consumer. +//! Methods with no return value may still be called from any thread. +//! +//! A callback method returns `Result` where `E: From`. +//! A failure the consumer reports through `out_err` becomes the `Err`: when +//! `E` is a declared error domain, a code of that domain (with a payload +//! that decodes) arrives as the typed variant, and every other failure is a +//! [`ForeignError`] with the code [`FOREIGN_ERROR_CODE`] converted with +//! `From`. use std::ffi::c_void; use std::sync::Arc; use crate::abi::buffer::BufferReader; use crate::abi::error::{ErrorDomain, FfiError, FOREIGN_ERROR_CODE, MARSHAL_ERROR_CODE}; +use crate::abi::scalar::{Scalar, Text}; + +/// The vtable `flags` bit (`{P}_VTABLE_THREAD_AFFINE` in C) declaring that +/// the consumer's value-returning methods may only be called on the thread +/// that passed the vtable to the producer. +pub const VTABLE_THREAD_AFFINE: u32 = 1; + +/// The message a thread-affine callback method fails with when it's called +/// from another thread. +pub const OFF_THREAD_MESSAGE: &str = "callback called off its thread"; /// The fixed header every callback-interface vtable starts with, in this /// order and at these offsets (`{prefix}_..._vtable` in C). @@ -37,7 +57,8 @@ use crate::abi::error::{ErrorDomain, FfiError, FOREIGN_ERROR_CODE, MARSHAL_ERROR pub struct VtableHeader { /// `sizeof` the whole vtable as the consumer compiled it. pub size: u32, - /// Reserved; consumers set it to `0`. + /// Behavior flags: [`VTABLE_THREAD_AFFINE`], or `0`. Other bits are + /// reserved and ignored. pub flags: u32, /// The consumer's release hook, called exactly once (from any producer /// thread) when the producer drops its last reference to the callback. @@ -145,6 +166,8 @@ pub struct VtableTooSmall { pub struct ForeignCallback { ctx: *mut c_void, vtable: *const V, + /// The adopting thread, for a vtable flagged [`VTABLE_THREAD_AFFINE`]. + owner: Option, } // SAFETY: the ABI contract obliges the consumer to make every vtable entry @@ -156,7 +179,8 @@ unsafe impl Sync for ForeignCallback {} impl ForeignCallback { /// Adopt a `(ctx, vtable)` pair lifted from a callback-interface - /// parameter's slots. Returns `Ok(None)` when the vtable pointer is null. + /// parameter's slots, recording the calling thread when the vtable is + /// thread-affine. Returns `Ok(None)` when the vtable pointer is null. /// /// # Errors /// @@ -195,7 +219,25 @@ impl ForeignCallback { }); } crate::abi::leak::track(crate::abi::leak::CALLBACKS, 1); - Ok(Some(Self { ctx, vtable })) + let owner = (header.flags & VTABLE_THREAD_AFFINE != 0).then(|| std::thread::current().id()); + Ok(Some(Self { ctx, vtable, owner })) + } + + /// Whether a value-returning method may be called on this thread: always + /// for an ordinary vtable, and only on the adopting thread for one + /// flagged [`VTABLE_THREAD_AFFINE`]. + /// + /// # Errors + /// + /// Returns a [`FOREIGN_ERROR_CODE`] failure with the message + /// [`OFF_THREAD_MESSAGE`] on any other thread. + pub fn check_thread(&self) -> Result<(), ForeignError> { + match self.owner { + Some(owner) if owner != std::thread::current().id() => { + Err(ForeignError::new(FOREIGN_ERROR_CODE, OFF_THREAD_MESSAGE)) + } + _ => Ok(()), + } } /// The consumer's context pointer, passed as the first argument of every @@ -229,12 +271,14 @@ impl Drop for ForeignCallback { /// A failure a consumer's callback-interface implementation reported, or a /// value it returned that the producer couldn't accept. /// -/// Every callback trait method returns `Result`. `code` is -/// [`FOREIGN_ERROR_CODE`] for any consumer failure, except that a method -/// declared `throws` keeps a code of its module's error domain, whose fields -/// [`domain`](Self::domain) decodes. A malformed return value (a string that -/// isn't UTF-8, a null object, a bad enum value or buffer) has the code -/// [`MARSHAL_ERROR_CODE`]. +/// A callback method's error type converts from it (`E: +/// From`), and `ForeignError` itself is a valid error type. +/// `code` is [`FOREIGN_ERROR_CODE`] for any consumer failure (and for a +/// thread-affine method called off its thread), and +/// [`MARSHAL_ERROR_CODE`] for a return value the producer couldn't accept (a +/// string that isn't UTF-8, a null object, a bad enum value, array, or +/// buffer). A declared code of the method's own error domain never arrives +/// as a `ForeignError`; it's decoded into the typed error instead. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForeignError { /// The failure's code (see the type's docs). @@ -286,27 +330,13 @@ impl std::fmt::Display for ForeignError { impl std::error::Error for ForeignError {} -/// A `ForeignError` is reported with its own code, message, and payload, so -/// a producer function whose error type is `ForeignError` can propagate a -/// callback failure with `?`. -impl crate::abi::ErrorReport for ForeignError { - fn code(&self) -> i32 { - self.code - } - fn message(&self) -> String { - self.message.clone() - } - fn payload(&self) -> Vec { - self.payload.clone() - } -} - fn failure(err: &FfiError, code: i32) -> ForeignError { // SAFETY: the consumer fills `out_err` only through `{prefix}_error_set`, - // so a non-null message is a NUL-terminated string this runtime owns. - let message = unsafe { err.message_str() } - .unwrap_or("callback interface implementation failed") - .to_string(); + // so a non-null message is a run this runtime owns. + let message = match unsafe { err.message_str() } { + Some(m) if !m.is_empty() => m.to_string(), + _ => "callback interface implementation failed".to_string(), + }; ForeignError { code, message, @@ -318,48 +348,136 @@ fn failure(err: &FfiError, code: i32) -> ForeignError { } } -/// Read the `out_err` slot a vtable entry of a method that doesn't `throw` -/// wrote: `Ok(())` when the consumer succeeded, otherwise its failure with -/// the code [`FOREIGN_ERROR_CODE`] (whatever code it wrote). +/// Read the `out_err` slot a vtable entry wrote, for a method whose error +/// type isn't a declared domain (`throws any`, or `ForeignError` itself): +/// `Ok(())` when the consumer succeeded, otherwise its failure with the code +/// [`FOREIGN_ERROR_CODE`] (whatever code it wrote), converted to `E`. /// /// # Errors /// /// Returns the consumer's failure when `err.code` isn't `0`. -pub fn callback_status(err: &FfiError) -> Result<(), ForeignError> { +pub fn callback_status>(err: &FfiError) -> Result<(), E> { if err.code == 0 { Ok(()) } else { - Err(failure(err, FOREIGN_ERROR_CODE)) + Err(E::from(failure(err, FOREIGN_ERROR_CODE))) } } -/// Read the `out_err` slot a vtable entry of a `throws` method wrote: a -/// positive code that `E` declares (with a payload that decodes) is kept, -/// and every other failure gets the code [`FOREIGN_ERROR_CODE`]. +/// Read the `out_err` slot a vtable entry wrote, for a method that throws +/// the domain `E`: a positive code `E` declares (with a payload that +/// decodes) becomes that typed error, and every other failure is a +/// [`FOREIGN_ERROR_CODE`] [`ForeignError`] converted to `E`. /// /// # Errors /// /// Returns the consumer's failure when `err.code` isn't `0`. -pub fn callback_status_in(err: &FfiError) -> Result<(), ForeignError> { +pub fn callback_status_in>(err: &FfiError) -> Result<(), E> { if err.code == 0 { return Ok(()); } - let kept = failure(err, err.code); - if kept.domain::().is_some() { - Err(kept) + match failure(err, err.code).domain::() { + Some(typed) => Err(typed), + None => Err(E::from(failure(err, FOREIGN_ERROR_CODE))), + } +} + +/// Convert a [`ForeignError`] into a callback method's error type. The +/// generated wrappers convert through this one function so a missing +/// `From` is reported once, at the error type. +pub fn convert_foreign>(e: ForeignError) -> E { + E::from(e) +} + +/// Lift a scalar (an integer, float, `bool`, `usize`, `isize`, or C-style +/// enum) a vtable entry returned. +/// +/// # Errors +/// +/// Returns a [`MARSHAL_ERROR_CODE`] failure for a value with no `T` +/// counterpart (an undeclared enum value, say). +pub fn callback_ret_scalar(value: T::Abi) -> Result { + T::from_abi(value).ok_or_else(|| { + ForeignError::marshal(&format!( + "callback interface returned an invalid value ({value:?})" + )) + }) +} + +/// Lift an optional scalar (OptDirect) a vtable entry returned as its +/// `bool` C return and `*out_value`. +/// +/// # Errors +/// +/// Same as [`callback_ret_scalar`] for a present value. +pub fn callback_ret_opt( + present: bool, + value: T::Abi, +) -> Result, ForeignError> { + if present { + callback_ret_scalar(value).map(Some) } else { - Err(failure(err, FOREIGN_ERROR_CODE)) + Ok(None) } } -/// Lift a C-style enum a vtable entry returned. +/// Adopt a typed array (Slice) a vtable entry wrote to its `out_ptr` and +/// `out_len` slots (`len` is the element count), converting each element. +/// The run is released whether or not it's accepted. /// /// # Errors /// -/// Returns a [`MARSHAL_ERROR_CODE`] failure for a value `E` doesn't declare. -pub fn callback_ret_enum(value: i32) -> Result { - E::from_i32(value) - .ok_or_else(|| ForeignError::marshal("callback interface returned an invalid enum value")) +/// Returns a [`MARSHAL_ERROR_CODE`] failure for a null pointer with a +/// non-zero count, a pointer not aligned for the element, or an element +/// with no `T` counterpart. +/// +/// # Safety +/// +/// `ptr` must be null or a run of `len * size_of::()` bytes from +/// `{prefix}_alloc`, which this adopts and frees. +pub unsafe fn callback_ret_slice( + ptr: *mut T::Abi, + len: usize, +) -> Result, ForeignError> { + if ptr.is_null() { + return if len == 0 { + Ok(Vec::new()) + } else { + Err(ForeignError::marshal( + "callback interface returned a null array", + )) + }; + } + let Some(size) = len.checked_mul(std::mem::size_of::()) else { + return Err(ForeignError::marshal( + "callback interface returned an array too large for memory", + )); + }; + let result = if ptr.is_aligned() { + // SAFETY: non-null and aligned; the caller guarantees `len` + // initialized elements. + let items = unsafe { std::slice::from_raw_parts(ptr, len) }; + items.iter().map(|v| callback_ret_scalar(*v)).collect() + } else { + Err(ForeignError::marshal( + "callback interface returned an array that isn't aligned for its element type", + )) + }; + // SAFETY: the run is ours to release, exactly once. + unsafe { crate::abi::free_bytes(ptr.cast(), size) }; + result +} + +/// Lift a custom type's value from the repr a vtable entry returned. +/// +/// # Errors +/// +/// Returns a [`MARSHAL_ERROR_CODE`] failure with the `lift` function's +/// message when it rejects the value. +pub fn lift_custom_returned( + repr: C::Repr, +) -> Result { + C::lift(repr).map_err(|e| ForeignError::marshal(&format!("callback interface returned {e}"))) } /// Adopt the strong object reference a vtable entry returned. @@ -407,20 +525,26 @@ pub unsafe fn callback_ret_bytes(ptr: *mut u8, len: usize) -> Result, Fo .ok_or_else(|| ForeignError::marshal("callback interface returned a null run")) } -/// Adopt a string a vtable entry wrote to its `out_ptr`/`out_len` slots. +/// Adopt text (a `String`, or a `char`) a vtable entry wrote to its +/// `out_ptr`/`out_len` slots. /// /// # Errors /// /// Returns a [`MARSHAL_ERROR_CODE`] failure for a null pointer with a -/// non-zero length or bytes that aren't UTF-8. +/// non-zero length, bytes that aren't UTF-8, or text with no `T` +/// counterpart (anything but one Unicode scalar value for a `char`). /// /// # Safety /// /// Same contract as [`callback_ret_bytes`]. -pub unsafe fn callback_ret_string(ptr: *mut u8, len: usize) -> Result { +pub unsafe fn callback_ret_text(ptr: *mut u8, len: usize) -> Result { // SAFETY: forwarded from the caller. - String::from_utf8(unsafe { callback_ret_bytes(ptr, len) }?) - .map_err(|_| ForeignError::marshal("callback interface returned a string that isn't UTF-8")) + let bytes = unsafe { callback_ret_bytes(ptr, len) }?; + let text = std::str::from_utf8(&bytes).map_err(|_| { + ForeignError::marshal("callback interface returned a string that isn't UTF-8") + })?; + T::from_text(text) + .ok_or_else(|| ForeignError::marshal(&format!("callback interface returned {text:?}"))) } /// Adopt and decode a value buffer a vtable entry wrote to its @@ -491,7 +615,7 @@ mod tests { fn call(cb: &ForeignCallback, x: i32) -> Result { let mut err = FfiError::default(); let out = unsafe { (cb.vtable().ping)(cb.ctx(), x, &mut err) }; - callback_status(&err).map(|()| out) + callback_status::(&err).map(|()| out) } #[test] @@ -520,7 +644,10 @@ mod tests { assert_eq!(err.code, FOREIGN_ERROR_CODE); assert_eq!(err.message, "negative"); let err = FfiError::new(MARSHAL_ERROR_CODE, "kept?"); - assert_eq!(callback_status(&err).unwrap_err().code, FOREIGN_ERROR_CODE); + assert_eq!( + callback_status::(&err).unwrap_err().code, + FOREIGN_ERROR_CODE + ); } #[test] @@ -567,7 +694,10 @@ mod tests { fn returned_runs_are_adopted() { let ptr = crate::abi::alloc(5); unsafe { std::ptr::copy_nonoverlapping(b"hello".as_ptr(), ptr, 5) }; - assert_eq!(unsafe { callback_ret_string(ptr, 5) }.unwrap(), "hello"); + assert_eq!( + unsafe { callback_ret_text::(ptr, 5) }.unwrap(), + "hello" + ); assert_eq!( unsafe { callback_ret_bytes(std::ptr::null_mut(), 0) }, Ok(vec![]) @@ -580,6 +710,64 @@ mod tests { ); let bad = crate::abi::alloc(1); unsafe { *bad = 0xff }; - assert!(unsafe { callback_ret_string(bad, 1) }.is_err()); + assert!(unsafe { callback_ret_text::(bad, 1) }.is_err()); + let c = crate::abi::alloc(1); + unsafe { *c = b'z' }; + assert_eq!(unsafe { callback_ret_text::(c, 1) }, Ok('z')); + } + + #[test] + fn returned_arrays_and_optionals_are_adopted() { + let ptr = crate::abi::alloc(16).cast::(); + unsafe { ptr.write(3) }; + unsafe { ptr.add(1).write(4) }; + let sizes: Vec = unsafe { callback_ret_slice(ptr, 2) }.unwrap(); + assert_eq!(sizes, [3, 4]); + assert_eq!( + unsafe { callback_ret_slice::(std::ptr::null_mut(), 0) }, + Ok(vec![]) + ); + assert_eq!( + unsafe { callback_ret_slice::(std::ptr::null_mut(), 1) } + .unwrap_err() + .code, + MARSHAL_ERROR_CODE + ); + assert_eq!(callback_ret_opt::(true, 5), Ok(Some(5))); + assert_eq!(callback_ret_opt::(false, 5), Ok(None)); + assert_eq!(callback_ret_scalar::(9), Ok(9)); + } + + #[test] + fn thread_affine_vtables_reject_other_threads() { + let freed = AtomicUsize::new(0); + let affine = TestVtable { + header: VtableHeader { + size: std::mem::size_of::() as u32, + flags: VTABLE_THREAD_AFFINE, + free, + }, + ping, + }; + let cb = unsafe { ForeignCallback::from_raw(ctx(&freed), &affine) } + .unwrap() + .unwrap(); + assert_eq!(cb.check_thread(), Ok(())); + std::thread::scope(|s| { + s.spawn(|| { + let err = cb.check_thread().unwrap_err(); + assert_eq!(err.code, FOREIGN_ERROR_CODE); + assert_eq!(err.message, OFF_THREAD_MESSAGE); + }); + }); + drop(cb); + let free_threaded = unsafe { ForeignCallback::from_raw(ctx(&freed), &VTABLE) } + .unwrap() + .unwrap(); + std::thread::scope(|s| { + s.spawn(|| assert_eq!(free_threaded.check_thread(), Ok(()))); + }); + drop(free_threaded); + assert_eq!(freed.load(Ordering::SeqCst), 2); } } diff --git a/crates/weaveffi/src/abi/convert.rs b/crates/weaveffi/src/abi/convert.rs index d7ea71ea..6b95ec40 100644 --- a/crates/weaveffi/src/abi/convert.rs +++ b/crates/weaveffi/src/abi/convert.rs @@ -1,27 +1,45 @@ -//! Marshalling helpers that bridge Rust values and the C ABI's `(ptr, len)` -//! slots. +//! Byte runs: the producer-allocated `(ptr, len)` memory every string, +//! bytes, value buffer, typed array, error message, and error payload +//! crosses in. //! //! These functions are the audited home of the pointer operations a WeaveFFI -//! producer performs on strings, bytes, and value buffers. The -//! `#[weaveffi::module]` expansion wires the generated thunks to them: +//! producer performs on runs. The `#[weaveffi::module]` expansion wires the +//! generated thunks to them: //! //! * **lift** functions read a borrowed `(ptr, len)` parameter. They never //! take ownership of caller memory; the borrowing variants //! ([`lift_str`], [`lift_byte_slice`]) don't even copy. -//! * **lower** functions hand an owned value to the consumer as a -//! producer-allocated `(ptr, len)` run, which the consumer releases with -//! `{prefix}_free_bytes` (see [`free_bytes`]). +//! * [`bytes_into_raw`] and [`slice_into_raw`] copy a value into a new run +//! the consumer releases with `{prefix}_free_bytes` (see [`free_bytes`]). //! * [`alloc`] is the body of `{prefix}_alloc`: a run the consumer fills and //! either frees with `{prefix}_free_bytes` or hands to the producer (a -//! callback's string, bytes, or buffer return), which adopts it with -//! [`adopt_bytes`]. Every run is a `Box<[u8]>` allocation, so one release -//! function covers both directions. +//! callback's string, bytes, buffer, or typed-array return), which adopts +//! it with [`adopt_bytes`]. +//! +//! Every run is allocated with **alignment 8** (`Layout::from_size_align(len, +//! 8)`) and released with the same layout, so a run can hold any element +//! type a typed array uses and both directions share one release function. +//! A run is always a fresh allocation: returning a `Vec` or `String` copies +//! its bytes out instead of handing over the vector's own storage, whose +//! alignment is only 1. //! //! Strings, bytes, and serialized value buffers all share this one //! representation: a string is its UTF-8 bytes, never NUL-terminated, so an //! interior NUL round-trips intact. A null pointer is valid only with a //! length of `0` and denotes the empty run. +use std::alloc::Layout; + +/// The alignment of every byte run, in bytes: enough for any element of a +/// typed array (`i64`, `u64`, `f64`). +pub const RUN_ALIGN: usize = 8; + +/// The layout of a run of `len` bytes, or `None` when `len` is too large to +/// allocate. +fn run_layout(len: usize) -> Option { + Layout::from_size_align(len, RUN_ALIGN).ok() +} + /// Borrow a `(ptr, len)` parameter as a byte slice for a caller-chosen /// lifetime, without copying. /// @@ -86,24 +104,38 @@ pub unsafe fn lift_string(ptr: *const u8, len: usize) -> Option { unsafe { lift_str(ptr, len) }.map(str::to_owned) } -/// Turn an owned byte vector into the producer-allocated `(ptr, len)` run a -/// return slot or async result carries, to be released with -/// [`free_bytes`]. An empty vector yields `(null, 0)` and allocates nothing. -/// -/// When the vector's capacity equals its length (as it does for every -/// encoding [`encode_value`](crate::abi::encode_value) produces) the conversion -/// reuses the allocation as is; otherwise it's shrunk to fit first. +/// Copy `data` into a new producer-allocated run, the `(ptr, len)` a return +/// slot, an async result, or an error message carries, to be released with +/// [`free_bytes`]. Empty data yields `(null, 0)` and allocates nothing. #[must_use] -pub fn bytes_into_raw(data: Vec) -> (*const u8, usize) { +pub fn bytes_into_raw(data: &[u8]) -> (*const u8, usize) { if data.is_empty() { return (std::ptr::null(), 0); } - let len = data.len(); - let ptr = Box::into_raw(data.into_boxed_slice()) - .cast::() - .cast_const(); - crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, 1); - (ptr, len) + let ptr = alloc_uninit(data.len()); + // SAFETY: `ptr` is a fresh allocation of `data.len()` bytes, which can't + // overlap `data`. + unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len()) }; + (ptr.cast_const(), data.len()) +} + +/// Copy `items` into a new producer-allocated run holding a typed array +/// (8-aligned, so any element type is aligned), returning the pointer and +/// the **element count**. The consumer releases it with +/// `{prefix}_free_bytes((uint8_t*)ptr, count * sizeof(T))`. An empty slice +/// yields `(null, 0)`. +#[must_use] +pub fn slice_into_raw(items: &[T]) -> (*mut T, usize) { + const { assert!(std::mem::align_of::() <= RUN_ALIGN) }; + let size = std::mem::size_of_val(items); + if size == 0 { + return (std::ptr::null_mut(), 0); + } + let ptr = alloc_uninit(size); + // SAFETY: `ptr` is a fresh, 8-aligned allocation of `size` bytes, which + // can't overlap `items`; `T: Copy` has no drop obligations. + unsafe { std::ptr::copy_nonoverlapping(items.as_ptr().cast::(), ptr, size) }; + (ptr.cast::(), items.len()) } /// Lower an owned byte vector into a return value plus the trailing @@ -112,7 +144,7 @@ pub fn bytes_into_raw(data: Vec) -> (*const u8, usize) { /// # Safety /// /// `out_len` must be null or point to a writable `usize`. -pub unsafe fn lower_bytes(data: Vec, out_len: *mut usize) -> *const u8 { +pub unsafe fn lower_bytes(data: &[u8], out_len: *mut usize) -> *const u8 { let (ptr, len) = bytes_into_raw(data); if !out_len.is_null() { // SAFETY: the caller guarantees `out_len` is writable when non-null. @@ -121,32 +153,53 @@ pub unsafe fn lower_bytes(data: Vec, out_len: *mut usize) -> *const u8 { ptr } -/// Lower an owned string as its UTF-8 bytes, exactly like [`lower_bytes`]. +/// Lower a string as its UTF-8 bytes, exactly like [`lower_bytes`]. /// /// # Safety /// /// Same contract as [`lower_bytes`]. -pub unsafe fn lower_string(s: String, out_len: *mut usize) -> *const u8 { +pub unsafe fn lower_string(s: &str, out_len: *mut usize) -> *const u8 { // SAFETY: forwarded from the caller. - unsafe { lower_bytes(s.into_bytes(), out_len) } + unsafe { lower_bytes(s.as_bytes(), out_len) } } -/// The body of `{prefix}_alloc`: allocate a zero-filled run of `len` bytes -/// the consumer fills, released with [`free_bytes`] (the same `len`) or -/// handed to the producer, which adopts it. A `len` of `0` returns null, -/// which is the empty run. +/// Allocate an uninitialized run of `len > 0` bytes, counting it. +fn alloc_uninit(len: usize) -> *mut u8 { + let layout = run_layout(len).unwrap_or_else(|| { + // An object this large can't exist in memory, so neither can its + // encoding; this is the allocator's own capacity-overflow failure. + panic!("WeaveFFI run of {len} bytes exceeds the address space") + }); + // SAFETY: `len > 0`, so the layout has a non-zero size. + let ptr = unsafe { std::alloc::alloc(layout) }; + if ptr.is_null() { + std::alloc::handle_alloc_error(layout); + } + crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, 1); + ptr +} + +/// The body of `{prefix}_alloc`: allocate a zero-filled, 8-aligned run of +/// `len` bytes the consumer fills, released with [`free_bytes`] (the same +/// `len`) or handed to the producer, which adopts it. A `len` of `0`, or one +/// too large to allocate, returns null. #[must_use] pub fn alloc(len: usize) -> *mut u8 { - if len == 0 { + let Some(layout) = run_layout(len).filter(|_| len > 0) else { return std::ptr::null_mut(); + }; + // SAFETY: the layout has a non-zero size. + let ptr = unsafe { std::alloc::alloc_zeroed(layout) }; + if ptr.is_null() { + std::alloc::handle_alloc_error(layout); } crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, 1); - Box::into_raw(vec![0u8; len].into_boxed_slice()).cast::() + ptr } /// Adopt a run from [`alloc`] (or one this runtime returned) as an owned -/// vector. Null with a length of `0` is the empty run; null with any other -/// length yields `None`. +/// vector, releasing the run. Null with a length of `0` is the empty run; +/// null with any other length yields `None`. /// /// # Safety /// @@ -157,31 +210,36 @@ pub unsafe fn adopt_bytes(ptr: *mut u8, len: usize) -> Option> { if ptr.is_null() { return (len == 0).then(Vec::new); } - if len == 0 { - return Some(Vec::new()); - } - crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, -1); - // SAFETY: the caller guarantees `ptr` is a `Box<[u8]>` of `len` bytes - // transferred to us. - Some(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) }.into_vec()) + // SAFETY: the caller guarantees `ptr` covers `len` initialized bytes. + let out = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec(); + // SAFETY: the run is ours to release, exactly once. + unsafe { free_bytes(ptr, len) }; + Some(out) } /// The body of `{prefix}_free_bytes`: release a run returned by this runtime -/// (a string, bytes, or a value buffer) or allocated with [`alloc`]. A null -/// `ptr` or a `len` of `0` is a no-op. +/// (a string, bytes, a value buffer, a typed array, or an error's message or +/// payload) or allocated with [`alloc`]. A null `ptr` or a `len` of `0` is a +/// no-op. /// /// # Safety /// /// `ptr` must be null or a pointer this runtime returned together with -/// `len` (or [`alloc`] returned for `len`), released exactly once. +/// `len` (for a typed array, `len` is its byte size), or [`alloc`] returned +/// for `len`, released exactly once. pub unsafe fn free_bytes(ptr: *mut u8, len: usize) { if ptr.is_null() || len == 0 { return; } + let Some(layout) = run_layout(len) else { + // No run this large was ever allocated, so `ptr` isn't one of ours; + // leaking is the only safe response to the contract violation. + return; + }; crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, -1); - // SAFETY: `bytes_into_raw` produced `ptr` from a `Box<[u8]>` of `len` - // bytes, and the caller hands it back exactly once. - drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) }); + // SAFETY: the caller hands back a run allocated with this layout, + // exactly once. + unsafe { std::alloc::dealloc(ptr, layout) }; } #[cfg(test)] @@ -191,20 +249,37 @@ mod tests { #[test] fn bytes_roundtrip() { let mut len = 0usize; - let ptr = unsafe { lower_bytes(vec![1u8, 2, 3, 4], &mut len) }; + let ptr = unsafe { lower_bytes(&[1u8, 2, 3, 4], &mut len) }; assert_eq!(len, 4); assert_eq!(unsafe { lift_bytes(ptr, len) }, Some(vec![1, 2, 3, 4])); unsafe { free_bytes(ptr.cast_mut(), len) }; } + #[test] + fn every_run_is_8_aligned() { + for len in [1usize, 3, 8, 13] { + let (p, n) = bytes_into_raw(&vec![7u8; len]); + assert_eq!(p as usize % RUN_ALIGN, 0); + unsafe { free_bytes(p.cast_mut(), n) }; + let a = alloc(len); + assert_eq!(a as usize % RUN_ALIGN, 0); + unsafe { free_bytes(a, len) }; + } + let (p, n) = slice_into_raw(&[1.5f64, -2.0]); + assert_eq!((p as usize % RUN_ALIGN, n), (0, 2)); + assert_eq!(unsafe { std::slice::from_raw_parts(p, n) }, [1.5, -2.0]); + unsafe { free_bytes(p.cast(), n * 8) }; + } + #[test] fn empty_is_null_and_null_is_empty() { let mut len = 99usize; - let ptr = unsafe { lower_bytes(Vec::new(), &mut len) }; + let ptr = unsafe { lower_bytes(&[], &mut len) }; assert!(ptr.is_null()); assert_eq!(len, 0); assert_eq!(unsafe { lift_str(std::ptr::null(), 0) }, Some("")); assert_eq!(unsafe { lift_bytes(std::ptr::null(), 0) }, Some(vec![])); + assert_eq!(slice_into_raw::(&[]), (std::ptr::null_mut(), 0)); unsafe { free_bytes(std::ptr::null_mut(), 0) }; } @@ -222,7 +297,7 @@ mod tests { assert_eq!(s, text); let mut len = 0usize; - let ptr = unsafe { lower_string(text.to_string(), &mut len) }; + let ptr = unsafe { lower_string(text, &mut len) }; assert_eq!(unsafe { lift_string(ptr, len) }.as_deref(), Some(text)); unsafe { free_bytes(ptr.cast_mut(), len) }; } @@ -230,6 +305,7 @@ mod tests { #[test] fn alloc_runs_free_and_adopt() { assert!(alloc(0).is_null()); + assert!(alloc(usize::MAX).is_null()); let p = alloc(4); assert_eq!(unsafe { std::slice::from_raw_parts(p, 4) }, [0; 4]); unsafe { free_bytes(p, 4) }; diff --git a/crates/weaveffi/src/abi/error.rs b/crates/weaveffi/src/abi/error.rs index 6bfe26a8..4e51eea2 100644 --- a/crates/weaveffi/src/abi/error.rs +++ b/crates/weaveffi/src/abi/error.rs @@ -1,21 +1,21 @@ //! The error struct every fallible symbol reports through, the reserved -//! runtime codes, and the [`ErrorReport`] mapping from producer errors. +//! runtime codes, and the [`ErrorDomain`] trait of a declared error domain. //! //! A synchronous call takes a caller-owned `{prefix}_error* out_err` slot; an //! async completion receives a heap-boxed one it releases with //! `{prefix}_error_free`. Either way the producer allocates the message (a -//! NUL-terminated C string) and the optional payload (a value buffer), so the -//! producer is also the one that frees them. +//! UTF-8 run, not NUL-terminated) and the optional payload (a value buffer), +//! both as ordinary 8-aligned byte runs, so the producer is also the one +//! that frees them. -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; use std::ptr; use crate::abi::buffer::{BufferDecodeError, BufferReader}; -/// The reserved error code for an **untyped producer error**: the default -/// [`ErrorReport::code`], used by `Result` and other error types -/// that don't map onto a declared domain code. +/// The reserved error code for an **untyped producer error**: a callable +/// declared `throws any` reports every failure with it, its message the +/// error's `Display` output. An async call the executor couldn't start +/// completes with it too. pub const GENERIC_ERROR_CODE: i32 = -1; /// The reserved error code reporting a producer **panic**. @@ -29,8 +29,9 @@ pub const PANIC_ERROR_CODE: i32 = -2; /// The reserved error code reporting a **marshalling failure**: an argument /// that could not be lifted at the boundary (a null pointer with a non-zero -/// length, a non-UTF-8 string, an out-of-range enum discriminant, or a -/// malformed value buffer). +/// length, a non-UTF-8 string, an out-of-range enum discriminant or integer, +/// a misaligned typed array, a malformed value buffer, or a value a custom +/// type's `lift` rejected). /// /// Both sides are generated from the same IDL, so a marshalling failure is a /// producer/consumer contract violation, not a domain error; wrappers surface @@ -38,13 +39,13 @@ pub const PANIC_ERROR_CODE: i32 = -2; pub const MARSHAL_ERROR_CODE: i32 = -3; /// The reserved error code reporting that a **consumer callback-interface -/// implementation failed**. +/// implementation failed**, or that a thread-affine callback was called off +/// its thread. /// /// A consumer's method implementation that raises reports it through the /// vtable entry's `out_err` slot (via `{prefix}_error_set`). The producer -/// sees every failure with this code, except a declared domain code from a -/// method that `throws` (see -/// [`ForeignError`](crate::abi::ForeignError)). +/// sees every failure with this code, except a declared code of the method's +/// own error domain (see [`ForeignError`](crate::abi::ForeignError)). pub const FOREIGN_ERROR_CODE: i32 = -4; /// The reserved error code reporting that an async call was **cancelled**: @@ -55,11 +56,12 @@ pub const CANCELLED_ERROR_CODE: i32 = -5; /// The error struct passed across the C ABI boundary (`{prefix}_error` in C). /// -/// `code == 0` means success and every pointer is null. Otherwise `message` -/// is a NUL-terminated UTF-8 string and `payload_ptr`/`payload_len` -/// optionally hold the matched error code's fields serialized in the -/// [`buffer`](crate::abi::buffer) format. The producer allocates both, and -/// [`error_clear`] (the body of `{prefix}_error_clear`) frees them. +/// `code == 0` means success and every pointer is null. Otherwise +/// `message_ptr`/`message_len` hold a UTF-8 message (not NUL-terminated) and +/// `payload_ptr`/`payload_len` optionally hold the matched error code's +/// fields serialized in the [`buffer`](crate::abi::buffer) format. The +/// producer allocates both as byte runs, and [`error_clear`] (the body of +/// `{prefix}_error_clear`) frees them. /// /// The struct owns its allocations, so it must not be copied bitwise while /// it holds any; a copy would double-free on clear. @@ -69,9 +71,11 @@ pub struct FfiError { /// Status code. `0` means success; a positive value is a domain error /// code and a negative value is one of the reserved runtime codes. pub code: i32, - /// Owned, NUL-terminated UTF-8 message describing the failure, or null - /// when [`code`](Self::code) is `0`. - pub message: *const c_char, + /// Owned UTF-8 message describing the failure (not NUL-terminated), or + /// null when it's empty. + pub message_ptr: *const u8, + /// Byte length of [`message_ptr`](Self::message_ptr); `0` when null. + pub message_len: usize, /// Owned value buffer holding the matched error code's payload fields, or /// null when the code declares no fields. pub payload_ptr: *const u8, @@ -83,7 +87,8 @@ impl Default for FfiError { fn default() -> Self { Self { code: 0, - message: ptr::null(), + message_ptr: ptr::null(), + message_len: 0, payload_ptr: ptr::null(), payload_len: 0, } @@ -97,8 +102,7 @@ impl Default for FfiError { unsafe impl Send for FfiError {} impl FfiError { - /// Build an owned error with `code` and `message`. Interior NUL bytes are - /// dropped from the message, since C reads it as a NUL-terminated string. + /// Build an owned error with `code` and `message`. #[must_use] pub fn new(code: i32, message: &str) -> Self { let mut err = Self::default(); @@ -106,15 +110,22 @@ impl FfiError { err } - /// Build the error an [`ErrorReport`] value describes, including its - /// serialized payload. + /// Build the error a declared domain's value describes: its code, its + /// `Display` output as the message, and its serialized payload. #[must_use] - pub fn from_report(e: &E) -> Self { - let mut err = Self::new(e.code(), &e.message()); - err.set_payload(e.payload()); + pub fn from_domain(e: &E) -> Self { + let mut err = Self::new(e.code(), &e.to_string()); + err.set_payload(&e.payload()); err } + /// Build the error an untyped failure (`throws any`) reports: the + /// [`GENERIC_ERROR_CODE`] with `e`'s `Display` output as the message. + #[must_use] + pub fn untyped(e: &E) -> Self { + Self::new(GENERIC_ERROR_CODE, &e.to_string()) + } + /// Build the error a caught unwind payload describes: a producer panic, /// reported with [`PANIC_ERROR_CODE`]. #[must_use] @@ -131,62 +142,52 @@ impl FfiError { Self::new(CANCELLED_ERROR_CODE, "cancelled") } - /// The message as a string slice, or `None` when it's null or not UTF-8. + /// The message as a string slice, or `None` when it's empty, malformed, + /// or not UTF-8. /// /// # Safety /// - /// `message` must be null or point to a NUL-terminated string that stays + /// `message_ptr` must be null or point to `message_len` bytes that stay /// valid for the borrow, which holds for any error this runtime filled. #[must_use] pub unsafe fn message_str(&self) -> Option<&str> { - if self.message.is_null() { - return None; + if self.message_ptr.is_null() { + return (self.code != 0).then_some(""); } - // SAFETY: the caller guarantees `message` is NUL-terminated and live. - unsafe { CStr::from_ptr(self.message) }.to_str().ok() + // SAFETY: the caller guarantees the run is live. + let bytes = unsafe { std::slice::from_raw_parts(self.message_ptr, self.message_len) }; + std::str::from_utf8(bytes).ok() } /// Release the message and payload, leaving the error in the success /// state. fn release(&mut self) { - if !self.message.is_null() { - // SAFETY: every non-null message was produced by - // `CString::into_raw` in `set`, and is released exactly once - // because the pointer is nulled right after. - unsafe { drop(CString::from_raw(self.message.cast_mut())) }; - self.message = ptr::null(); - } - if !self.payload_ptr.is_null() { - // SAFETY: every non-null payload was produced by - // `bytes_into_raw` in `set_payload` with this length. - unsafe { crate::abi::free_bytes(self.payload_ptr.cast_mut(), self.payload_len) }; - self.payload_ptr = ptr::null(); + // SAFETY: every non-null message and payload was allocated by + // `bytes_into_raw` with its recorded length, and each is released + // exactly once because the pointers are nulled right after. + unsafe { + crate::abi::free_bytes(self.message_ptr.cast_mut(), self.message_len); + crate::abi::free_bytes(self.payload_ptr.cast_mut(), self.payload_len); } + self.message_ptr = ptr::null(); + self.message_len = 0; + self.payload_ptr = ptr::null(); self.payload_len = 0; self.code = 0; } - // `CString::new` is infallible here: interior NUL bytes are removed first. - #[allow(clippy::missing_panics_doc)] fn set(&mut self, code: i32, message: &str) { self.release(); self.code = code; - let message = if message.as_bytes().contains(&0) { - message.replace('\0', "") - } else { - message.to_owned() - }; - self.message = CString::new(message) - .expect("interior NUL bytes were removed") - .into_raw(); - } - - fn set_payload(&mut self, payload: Vec) { - if !self.payload_ptr.is_null() { - // SAFETY: every non-null payload was produced by - // `bytes_into_raw` with this length, and is replaced right after. - unsafe { crate::abi::free_bytes(self.payload_ptr.cast_mut(), self.payload_len) }; - } + let (ptr, len) = crate::abi::convert::bytes_into_raw(message.as_bytes()); + self.message_ptr = ptr; + self.message_len = len; + } + + fn set_payload(&mut self, payload: &[u8]) { + // SAFETY: a non-null payload was allocated by `bytes_into_raw` with + // this length, and is replaced right after. + unsafe { crate::abi::free_bytes(self.payload_ptr.cast_mut(), self.payload_len) }; let (ptr, len) = crate::abi::convert::bytes_into_raw(payload); self.payload_ptr = ptr; self.payload_len = len; @@ -238,36 +239,32 @@ pub unsafe fn error_store(out_err: *mut FfiError, value: FfiError) { } } -/// The body of `{prefix}_error_set`: fill `out_err` from a borrowed C string -/// the consumer owns. +/// The body of `{prefix}_error_set`: fill `out_err` with `code` and a copy +/// of the `len` message bytes at `message`, which the consumer owns. /// /// Callback-interface implementations call it to report a failure so that /// the message is allocated by the producer, which is the side that frees -/// it. A null or non-UTF-8 `message` yields an empty message; the code is -/// always recorded. +/// it. A null `message` yields an empty message, and bytes that aren't +/// UTF-8 are replaced with U+FFFD; the code is always recorded. /// /// # Safety /// /// Same contract as [`error_set`] for `out_err`. `message` must be null or -/// point to a NUL-terminated string that stays valid for the call. -pub unsafe fn error_set_c(out_err: *mut FfiError, code: i32, message: *const c_char) { - let text = if message.is_null() { - "" - } else { - // SAFETY: the caller guarantees a NUL-terminated string. - unsafe { CStr::from_ptr(message) }.to_str().unwrap_or("") - }; +/// point to `len` readable bytes for the duration of the call. +pub unsafe fn error_set_c(out_err: *mut FfiError, code: i32, message: *const u8, len: usize) { + // SAFETY: forwarded from the caller. + let bytes = unsafe { crate::abi::lift_byte_slice(message, len) }.unwrap_or(&[]); // SAFETY: forwarded from the caller. - unsafe { error_set(out_err, code, text) }; + unsafe { error_set(out_err, code, &String::from_utf8_lossy(bytes)) }; } /// The body of `{prefix}_error_set_payload`: replace `out_err`'s payload /// with a producer-owned copy of the `len` bytes at `ptr`. A null `out_err` /// is a no-op; a null `ptr` (or a `len` of `0`) clears the payload. /// -/// A callback-interface implementation of a `throws` method calls it after -/// `{prefix}_error_set` to attach a domain code's fields, encoded as a value -/// buffer, so the producer can decode the typed error. +/// A callback-interface implementation of a method that throws a domain +/// calls it after `{prefix}_error_set` to attach a code's fields, encoded as +/// a value buffer, so the producer can decode the typed error. /// /// # Safety /// @@ -280,7 +277,7 @@ pub unsafe fn error_set_payload_c(out_err: *mut FfiError, ptr: *const u8, len: u }; // SAFETY: forwarded from the caller. let bytes = unsafe { crate::abi::lift_byte_slice(ptr, len) }.unwrap_or(&[]); - err.set_payload(bytes.to_vec()); + err.set_payload(bytes); } /// The body of `{prefix}_error_clear`: free any message and payload and set @@ -317,65 +314,27 @@ pub fn boxed_error(err: FfiError) -> *mut FfiError { Box::into_raw(Box::new(err)) } -/// Maps a producer error onto the ABI's `(code, message, payload)` triple. +/// A declared error domain: the trait the `#[weaveffi::error]` expansion +/// implements for the enum. /// -/// A fallible `#[weaveffi::export]` function returning `Result` reports -/// `Err(e)` through its trailing `out_err` slot using this trait. `String` and -/// `&str` errors are covered out of the box with the generic code `-1`, so -/// `Result` needs no extra code. The `#[weaveffi::error]` -/// expansion implements it for an error domain's enum: the code is the -/// variant's discriminant, the message is the enum's -/// [`Display`](std::fmt::Display) output, and the payload is the variant's -/// fields. -/// -/// To implement the trait by hand: -/// -/// ``` -/// use weaveffi::abi::ErrorReport; -/// -/// enum KvError { -/// KeyNotFound, -/// Io(String), -/// } -/// -/// impl ErrorReport for KvError { -/// fn code(&self) -> i32 { -/// match self { -/// KvError::KeyNotFound => 1001, -/// KvError::Io(_) => 1004, -/// } -/// } -/// fn message(&self) -> String { -/// match self { -/// KvError::KeyNotFound => "key not found".to_string(), -/// KvError::Io(detail) => format!("I/O error: {detail}"), -/// } -/// } -/// } -/// ``` -pub trait ErrorReport { - /// The non-zero status code written to [`FfiError::code`]. Defaults to - /// [`GENERIC_ERROR_CODE`]. - fn code(&self) -> i32 { - GENERIC_ERROR_CODE - } - - /// The human-readable message written to [`FfiError::message`]. - fn message(&self) -> String; - - /// The serialized payload fields written to [`FfiError::payload_ptr`], or - /// an empty vector for a code with no fields. +/// A callable whose `Result` error type is a domain declared in its module +/// tree reports `Err(e)` through its `out_err` slot with +/// [`FfiError::from_domain`]: the code is the variant's discriminant, the +/// message is the enum's `Display` output (generated from the variants' +/// message templates unless the enum opts out with `no_display`), and the +/// payload is the variant's fields. [`read_code`](Self::read_code) decodes a +/// code back into the enum, which is how a callback method that throws the +/// domain receives the consumer's typed error. +pub trait ErrorDomain: std::fmt::Display + Sized { + /// The variant's declared code (positive). + fn code(&self) -> i32; + + /// The variant's fields serialized as a value buffer, or an empty + /// vector for a code with no fields. fn payload(&self) -> Vec { Vec::new() } -} -/// An error type the `#[weaveffi::error]` expansion generates: a module's -/// error domain, decodable from a code and its payload. -/// -/// [`ForeignError::domain`](crate::abi::ForeignError::domain) uses it to turn -/// a domain error a consumer's callback reported into the typed error. -pub trait ErrorDomain: ErrorReport + Sized { /// Decode the error `code` names, reading its payload fields (if any) /// from `r`. Returns `Ok(None)` when `code` isn't one of the domain's /// declared codes. @@ -394,30 +353,6 @@ pub trait ErrorDomain: ErrorReport + Sized { ) -> Result, BufferDecodeError>; } -impl ErrorReport for String { - fn message(&self) -> String { - self.clone() - } -} - -impl ErrorReport for &str { - fn message(&self) -> String { - (*self).to_string() - } -} - -impl ErrorReport for Box { - fn message(&self) -> String { - self.to_string() - } -} - -impl ErrorReport for Box { - fn message(&self) -> String { - self.to_string() - } -} - /// Best-effort extraction of a panic payload's message (`&str` and `String` /// payloads; anything else yields a fixed placeholder). #[must_use] @@ -444,8 +379,9 @@ mod tests { fn default_is_ok() { let err = FfiError::default(); assert_eq!(err.code, 0); - assert!(err.message.is_null()); + assert!(err.message_ptr.is_null()); assert!(err.payload_ptr.is_null()); + assert_eq!(message(&err), None); } #[test] @@ -454,17 +390,25 @@ mod tests { unsafe { error_set(&mut err, 1, "first") }; unsafe { error_set(&mut err, 2, "second") }; assert_eq!(err.code, 2); + assert_eq!(err.message_len, 6); assert_eq!(message(&err).as_deref(), Some("second")); unsafe { error_clear(&mut err) }; assert_eq!(err.code, 0); - assert!(err.message.is_null()); + assert!(err.message_ptr.is_null()); + assert_eq!(err.message_len, 0); + } + + #[test] + fn messages_are_8_aligned_runs() { + let err = FfiError::new(1, "aligned"); + assert_eq!(err.message_ptr as usize % crate::abi::convert::RUN_ALIGN, 0); } #[test] fn null_slots_are_no_ops() { unsafe { error_set(ptr::null_mut(), 1, "x"); - error_set_c(ptr::null_mut(), 1, ptr::null()); + error_set_c(ptr::null_mut(), 1, ptr::null(), 0); error_clear(ptr::null_mut()); error_free(ptr::null_mut()); error_store(ptr::null_mut(), FfiError::new(3, "dropped")); @@ -472,40 +416,47 @@ mod tests { } #[test] - fn interior_nul_is_removed_from_messages() { + fn interior_nul_survives_in_messages() { let err = FfiError::new(1, "hel\0lo"); - assert_eq!(message(&err).as_deref(), Some("hello")); + assert_eq!(message(&err).as_deref(), Some("hel\0lo")); } #[test] fn set_c_copies_a_borrowed_message() { let mut err = FfiError::default(); - let msg = CString::new("from the consumer").unwrap(); - unsafe { error_set_c(&mut err, FOREIGN_ERROR_CODE, msg.as_ptr()) }; + let msg = b"from the consumer".to_vec(); + unsafe { error_set_c(&mut err, FOREIGN_ERROR_CODE, msg.as_ptr(), msg.len()) }; drop(msg); assert_eq!(err.code, FOREIGN_ERROR_CODE); assert_eq!(message(&err).as_deref(), Some("from the consumer")); - unsafe { error_set_c(&mut err, 3, ptr::null()) }; + unsafe { error_set_c(&mut err, 3, ptr::null(), 0) }; assert_eq!(err.code, 3); assert_eq!(message(&err).as_deref(), Some("")); + let bad = [b'o', 0xFF, b'k']; + unsafe { error_set_c(&mut err, 4, bad.as_ptr(), bad.len()) }; + assert_eq!(message(&err).as_deref(), Some("o\u{FFFD}k")); } + #[derive(Debug)] enum DomainError { NotFound, Io(String), } - impl ErrorReport for DomainError { - fn code(&self) -> i32 { + impl std::fmt::Display for DomainError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - DomainError::NotFound => 1001, - DomainError::Io(_) => 1004, + DomainError::NotFound => f.write_str("not found"), + DomainError::Io(detail) => write!(f, "io: {detail}"), } } - fn message(&self) -> String { + } + + impl ErrorDomain for DomainError { + fn code(&self) -> i32 { match self { - DomainError::NotFound => "not found".to_string(), - DomainError::Io(detail) => format!("io: {detail}"), + DomainError::NotFound => 1001, + DomainError::Io(_) => 1004, } } fn payload(&self) -> Vec { @@ -514,16 +465,26 @@ mod tests { DomainError::Io(detail) => crate::abi::encode_value(detail), } } + unsafe fn read_code( + code: i32, + r: &mut BufferReader<'_>, + ) -> Result, BufferDecodeError> { + Ok(match code { + 1001 => Some(DomainError::NotFound), + 1004 => Some(DomainError::Io(r.read_string()?)), + _ => None, + }) + } } #[test] - fn reports_carry_code_message_and_payload() { - let err = FfiError::from_report(&DomainError::NotFound); + fn domains_carry_code_message_and_payload() { + let err = FfiError::from_domain(&DomainError::NotFound); assert_eq!(err.code, 1001); assert_eq!(message(&err).as_deref(), Some("not found")); assert!(err.payload_ptr.is_null()); - let err = FfiError::from_report(&DomainError::Io("disk".into())); + let err = FfiError::from_domain(&DomainError::Io("disk".into())); assert_eq!(err.code, 1004); assert_eq!( unsafe { crate::abi::decode_value::(err.payload()) }.unwrap(), @@ -536,11 +497,15 @@ mod tests { } #[test] - fn string_errors_use_the_generic_code() { - let e = "boom".to_string(); - assert_eq!(ErrorReport::code(&e), GENERIC_ERROR_CODE); - assert_eq!(ErrorReport::message(&e), "boom"); - assert_eq!(ErrorReport::code(&"boom"), GENERIC_ERROR_CODE); + fn untyped_errors_use_the_generic_code_and_display() { + let err = FfiError::untyped("boom"); + assert_eq!(err.code, GENERIC_ERROR_CODE); + assert_eq!(message(&err).as_deref(), Some("boom")); + let io = std::io::Error::other("disk on fire"); + assert_eq!( + message(&FfiError::untyped(&io)).as_deref(), + Some("disk on fire") + ); } #[test] diff --git a/crates/weaveffi/src/abi/marshal.rs b/crates/weaveffi/src/abi/marshal.rs index 1e826b5b..86e0f39a 100644 --- a/crates/weaveffi/src/abi/marshal.rs +++ b/crates/weaveffi/src/abi/marshal.rs @@ -11,8 +11,10 @@ //! parameter. A thunk lifts **every** parameter before it looks at any //! result, so a callback context or the object tokens in a buffer are //! always adopted (and released again) even when another parameter fails. -//! * `lower_*` functions (and [`CEnum::to_i32`]) turn a returned value into -//! its C return and out slots. +//! * `lower_*` functions (and [`Scalar::to_abi`]) turn a returned value +//! into its C return and out slots, and the `*_run` and `*_slots` +//! functions into an async completion's result slots or a callback +//! method's argument slots. //! * [`call_sync`] runs a synchronous thunk's body under `catch_unwind` and //! reports its outcome through `out_err`. @@ -21,24 +23,13 @@ use std::sync::Arc; use crate::abi::callback::{lift_callback, lift_callback_opt, CallbackInterface}; use crate::abi::error::{error_clear, error_store, FfiError, MARSHAL_ERROR_CODE}; +use crate::abi::scalar::{Custom, Scalar, Text}; use crate::abi::BufferValue; -/// A C-style enum: an `i32` discriminant at the C ABI and inside value -/// buffers. The `#[weaveffi::module]` expansion implements it for every -/// `#[repr(i32)]` `#[weaveffi::enumeration]`. -pub trait CEnum: Sized { - /// The variant whose discriminant is `value`, or `None` when no variant - /// has it. - fn from_i32(value: i32) -> Option; - - /// This variant's discriminant. - fn to_i32(&self) -> i32; -} - /// Write a C-style enum into a value buffer (the body of its /// `BufferValue::write_value`). -pub fn write_enum(value: &E, w: &mut crate::abi::BufferWriter) { - w.write_i32(value.to_i32()); +pub fn write_enum>(value: &E, w: &mut crate::abi::BufferWriter) { + w.write_i32(value.to_abi()); } /// Read a C-style enum from a value buffer (the body of its @@ -48,10 +39,10 @@ pub fn write_enum(value: &E, w: &mut crate::abi::BufferWriter) { /// /// Returns an error when the buffer is exhausted or the discriminant isn't /// one of `E`'s. -pub fn read_enum( +pub fn read_enum>( r: &mut crate::abi::BufferReader<'_>, ) -> Result { - E::from_i32(r.read_i32()?).ok_or(crate::abi::BufferDecodeError { + E::from_abi(r.read_i32()?).ok_or(crate::abi::BufferDecodeError { context: "enum discriminant out of range", }) } @@ -140,21 +131,188 @@ fn invalid(name: &str) -> FfiError { FfiError::new(MARSHAL_ERROR_CODE, &format!("{name} is null or invalid")) } -/// Lift a C-style enum parameter. +fn short_type_name() -> &'static str { + let full = std::any::type_name::(); + full.rsplit("::").next().unwrap_or(full) +} + +fn out_of_range(name: &str, value: T::Abi) -> FfiError { + FfiError::new( + MARSHAL_ERROR_CODE, + &format!( + "{name}: {value:?} is not a valid {}", + short_type_name::() + ), + ) +} + +// ── Direct and OptDirect ──────────────────────────────────────────────── + +/// Lift a scalar parameter (an integer, float, `bool`, `usize`, `isize`, or +/// C-style enum) from its C slot. /// /// # Errors /// -/// Returns a marshalling error when `value` isn't one of `E`'s -/// discriminants. -pub fn lift_enum(value: i32, name: &str) -> Result { - E::from_i32(value).ok_or_else(|| { - FfiError::new( +/// Returns a marshalling error when the value has no `T` counterpart (an +/// undeclared enum value, a `u64` past `usize::MAX`). +pub fn lift_scalar_param(value: T::Abi, name: &str) -> Result { + T::from_abi(value).ok_or_else(|| out_of_range::(name, value)) +} + +/// Lift an optional scalar parameter (OptDirect) from its `has_{name}` and +/// `{name}` slots. `value` is ignored when `has` is false. +/// +/// # Errors +/// +/// Returns a marshalling error when a present value has no `T` counterpart. +pub fn lift_opt_param( + has: bool, + value: T::Abi, + name: &str, +) -> Result, FfiError> { + if has { + lift_scalar_param(value, name).map(Some) + } else { + Ok(None) + } +} + +/// Lower an optional scalar return (OptDirect): write the value to +/// `*out_value` (the zero value when absent) and return whether it's +/// present. +/// +/// # Safety +/// +/// `out_value` must be null or point to a writable `T::Abi`. +pub unsafe fn lower_opt_ret(value: Option, out_value: *mut T::Abi) -> bool { + let (present, abi) = opt_run(value); + if !out_value.is_null() { + // SAFETY: the caller guarantees `out_value` is writable when + // non-null. + unsafe { *out_value = abi }; + } + present +} + +/// An optional scalar as an async completion's `(has_result, result)` +/// slots (the zero value when absent). +#[must_use] +pub fn opt_run(value: Option) -> (bool, T::Abi) { + opt_slots(value.as_ref()) +} + +/// An optional scalar as a callback method argument's `(has_{name}, +/// {name})` slots (the zero value when absent). +#[must_use] +pub fn opt_slots(value: Option<&T>) -> (bool, T::Abi) { + match value { + Some(v) => (true, v.to_abi()), + None => (false, T::Abi::sentinel()), + } +} + +// ── Slice ─────────────────────────────────────────────────────────────── + +/// Check a typed array's `(ptr, len)` slots: null only with a count of `0`, +/// aligned for `A`, and a byte size that fits in memory. +fn check_slice(ptr: *const A, len: usize, name: &str) -> Result<(), FfiError> { + if ptr.is_null() { + return if len == 0 { Ok(()) } else { Err(invalid(name)) }; + } + if !ptr.is_aligned() { + return Err(FfiError::new( MARSHAL_ERROR_CODE, - &format!("{name}: {value} is not a valid enum value"), - ) - }) + &format!("{name}: the array isn't aligned for its element type"), + )); + } + if len + .checked_mul(std::mem::size_of::()) + .is_none_or(|n| n > isize::MAX as usize) + { + return Err(invalid(name)); + } + Ok(()) +} + +/// Borrow a typed-array parameter (Slice) as a slice of its own element +/// type, without copying. +/// +/// # Errors +/// +/// Returns a marshalling error for null with a non-zero count, a pointer +/// not aligned for `P`, or a count too large for memory. +/// +/// # Safety +/// +/// When `ptr` is non-null it must point to `len` initialized elements that +/// stay valid and unmodified for `'a` (generated thunks bound `'a` by the +/// call). +pub unsafe fn lift_slice_param<'a, P: Scalar>( + ptr: *const P, + len: usize, + name: &str, +) -> Result<&'a [P], FfiError> { + check_slice(ptr, len, name)?; + if ptr.is_null() { + return Ok(&[]); + } + // SAFETY: checked non-null and aligned above; the caller guarantees the + // elements are live for `'a`. + Ok(unsafe { std::slice::from_raw_parts(ptr, len) }) } +/// Copy a typed-array parameter (Slice) into a `Vec`, converting each +/// element (a `[u64]` into a `Vec`, say). +/// +/// # Errors +/// +/// Same as [`lift_slice_param`], plus a marshalling error when an element +/// has no `T` counterpart. +/// +/// # Safety +/// +/// Same contract as [`lift_slice_param`], for the duration of the call. +pub unsafe fn lift_slice_vec_param( + ptr: *const T::Abi, + len: usize, + name: &str, +) -> Result, FfiError> { + check_slice(ptr, len, name)?; + if ptr.is_null() { + return Ok(Vec::new()); + } + // SAFETY: checked non-null and aligned above; forwarded from the caller. + let items = unsafe { std::slice::from_raw_parts(ptr, len) }; + items.iter().map(|v| lift_scalar_param(*v, name)).collect() +} + +/// Lower a typed-array return (Slice) as a new 8-aligned run of +/// `T::Abi` elements, writing the **element count** to `*out_len`. The +/// consumer releases it with `{prefix}_free_bytes(ptr, count * +/// sizeof(T))`. +/// +/// # Safety +/// +/// `out_len` must be null or point to a writable `usize`. +pub unsafe fn lower_slice_ret(value: &[T], out_len: *mut usize) -> *mut T::Abi { + let (ptr, len) = crate::abi::convert::slice_into_raw(&T::abi_slice(value)); + if !out_len.is_null() { + // SAFETY: the caller guarantees `out_len` is writable when non-null. + unsafe { *out_len = len }; + } + ptr +} + +/// A typed array as an async completion's owned `(result_ptr, result_len)` +/// slots (`result_len` is the element count). +#[must_use] +pub fn slice_run(value: &[T]) -> (*const T::Abi, usize) { + let (ptr, len) = crate::abi::convert::slice_into_raw(&T::abi_slice(value)); + (ptr.cast_const(), len) +} + +// ── String, Bytes, and Buffer ─────────────────────────────────────────── + /// Borrow a string parameter's UTF-8 `(ptr, len)` run as a `&str`. /// /// # Errors @@ -174,22 +332,30 @@ pub unsafe fn lift_str_param<'a>( unsafe { crate::abi::lift_str(ptr, len) }.ok_or_else(|| invalid(name)) } -/// Copy a string parameter into an owned `String`. +/// Lift a string parameter into an owned [`Text`]: a `String`, or a `char` +/// (which must be exactly one Unicode scalar value). /// /// # Errors /// -/// Same as [`lift_str_param`]. +/// Same as [`lift_str_param`], plus a marshalling error when the text has no +/// `T` counterpart. /// /// # Safety /// /// Same contract as [`lift_str_param`]. -pub unsafe fn lift_string_param( +pub unsafe fn lift_text_param( ptr: *const u8, len: usize, name: &str, -) -> Result { +) -> Result { // SAFETY: forwarded from the caller. - unsafe { lift_str_param(ptr, len, name) }.map(str::to_owned) + let text = unsafe { lift_str_param(ptr, len, name) }?; + T::from_text(text).ok_or_else(|| { + FfiError::new( + MARSHAL_ERROR_CODE, + &format!("{name}: {text:?} is not a valid {}", short_type_name::()), + ) + }) } /// Borrow a bytes parameter's `(ptr, len)` run as a slice. @@ -201,7 +367,7 @@ pub unsafe fn lift_string_param( /// # Safety /// /// Same contract as [`lift_byte_slice`](crate::abi::lift_byte_slice). -pub unsafe fn lift_slice_param<'a>( +pub unsafe fn lift_byte_slice_param<'a>( ptr: *const u8, len: usize, name: &str, @@ -214,18 +380,18 @@ pub unsafe fn lift_slice_param<'a>( /// /// # Errors /// -/// Same as [`lift_slice_param`]. +/// Same as [`lift_byte_slice_param`]. /// /// # Safety /// -/// Same contract as [`lift_slice_param`]. +/// Same contract as [`lift_byte_slice_param`]. pub unsafe fn lift_bytes_param( ptr: *const u8, len: usize, name: &str, ) -> Result, FfiError> { // SAFETY: forwarded from the caller. - unsafe { lift_slice_param(ptr, len, name) }.map(<[u8]>::to_vec) + unsafe { lift_byte_slice_param(ptr, len, name) }.map(<[u8]>::to_vec) } /// Decode a value-buffer parameter (a record, rich enum, optional, list, or @@ -238,7 +404,7 @@ pub unsafe fn lift_bytes_param( /// /// # Safety /// -/// Same contract as [`lift_slice_param`], plus that of +/// Same contract as [`lift_byte_slice_param`], plus that of /// [`decode_value`](crate::abi::decode_value) for the buffer's object tokens. pub unsafe fn lift_buffer_param( ptr: *const u8, @@ -246,12 +412,76 @@ pub unsafe fn lift_buffer_param( name: &str, ) -> Result { // SAFETY: forwarded from the caller. - let bytes = unsafe { lift_slice_param(ptr, len, name) }?; + let bytes = unsafe { lift_byte_slice_param(ptr, len, name) }?; // SAFETY: forwarded from the caller; the thunk decodes each buffer once. unsafe { crate::abi::decode_value(bytes) } .map_err(|e| FfiError::new(MARSHAL_ERROR_CODE, &format!("{name}: {e}"))) } +/// Lower a returned string (or `char`) as a producer-allocated run plus +/// `*out_len`. +/// +/// # Safety +/// +/// `out_len` must be null or point to a writable `usize`. +pub unsafe fn lower_string_ret(value: &T, out_len: *mut usize) -> *const u8 { + // SAFETY: forwarded from the caller. + unsafe { crate::abi::lower_string(&value.as_text(), out_len) } +} + +/// Lower returned bytes as a producer-allocated run plus `*out_len`. +/// +/// # Safety +/// +/// Same contract as [`lower_string_ret`]. +pub unsafe fn lower_bytes_ret + ?Sized>( + value: &B, + out_len: *mut usize, +) -> *const u8 { + // SAFETY: forwarded from the caller. + unsafe { crate::abi::lower_bytes(value.as_ref(), out_len) } +} + +/// Encode a returned value buffer as a producer-allocated run plus +/// `*out_len`. +/// +/// # Safety +/// +/// Same contract as [`lower_string_ret`]. +pub unsafe fn lower_buffer_ret(value: &T, out_len: *mut usize) -> *const u8 { + // SAFETY: forwarded from the caller. + unsafe { crate::abi::lower_bytes(&crate::abi::encode_value(value), out_len) } +} + +/// A string (or `char`) as an async completion's owned `(result_ptr, +/// result_len)` run. +#[must_use] +pub fn string_run(value: &T) -> (*const u8, usize) { + crate::abi::bytes_into_raw(value.as_text().as_bytes()) +} + +/// Bytes as an async completion's owned `(result_ptr, result_len)` run. +#[must_use] +pub fn bytes_run + ?Sized>(value: &B) -> (*const u8, usize) { + crate::abi::bytes_into_raw(value.as_ref()) +} + +/// A value buffer as an async completion's owned `(result_ptr, result_len)` +/// run. +#[must_use] +pub fn buffer_run(value: &T) -> (*const u8, usize) { + crate::abi::bytes_into_raw(&crate::abi::encode_value(value)) +} + +/// Borrow a bytes-like argument as the `(ptr, len)` slots of a callback +/// method call. +pub fn byte_slots + ?Sized>(value: &B) -> (*const u8, usize) { + let b = value.as_ref(); + (b.as_ptr(), b.len()) +} + +// ── Objects, callbacks, and receivers ─────────────────────────────────── + /// Borrow an object parameter for the call. /// /// # Errors @@ -309,7 +539,8 @@ pub unsafe fn lift_object_arc_opt_param(ptr: *const T) -> Result(ptr: *const T) -> Result, FfiError> { unsafe { lift_object_arc_param(ptr, "self") } } -/// Lower a returned string as a producer-allocated run plus `*out_len`. -/// -/// # Safety -/// -/// `out_len` must be null or point to a writable `usize`. -pub unsafe fn lower_string_ret(value: impl Into, out_len: *mut usize) -> *const u8 { - // SAFETY: forwarded from the caller. - unsafe { crate::abi::lower_string(value.into(), out_len) } -} - -/// Lower returned bytes as a producer-allocated run plus `*out_len`. +/// Lift a custom type's value from its repr (already lifted from the +/// parameter's slots). /// -/// # Safety +/// # Errors /// -/// Same contract as [`lower_string_ret`]. -pub unsafe fn lower_bytes_ret(value: impl Into>, out_len: *mut usize) -> *const u8 { - // SAFETY: forwarded from the caller. - unsafe { crate::abi::lower_bytes(value.into(), out_len) } +/// Returns a marshalling error naming the parameter, with the `lift` +/// function's message, when it rejects the value. +pub fn lift_custom_param(repr: C::Repr, name: &str) -> Result { + C::lift(repr).map_err(|e| FfiError::new(MARSHAL_ERROR_CODE, &format!("{name}: {e}"))) } -/// Encode a returned value buffer as a producer-allocated run plus -/// `*out_len`. +/// Lift a custom type's value from a repr read out of a value buffer. /// -/// # Safety +/// # Errors /// -/// Same contract as [`lower_string_ret`]. -pub unsafe fn lower_buffer_ret(value: &T, out_len: *mut usize) -> *const u8 { - // SAFETY: forwarded from the caller. - unsafe { crate::abi::lower_bytes(crate::abi::encode_value(value), out_len) } -} - -/// A string as an async completion's owned `(result_ptr, result_len)` run. -#[must_use] -pub fn string_run(value: impl Into) -> (*const u8, usize) { - crate::abi::bytes_into_raw(value.into().into_bytes()) -} - -/// Bytes as an async completion's owned `(result_ptr, result_len)` run. -#[must_use] -pub fn bytes_run(value: impl Into>) -> (*const u8, usize) { - crate::abi::bytes_into_raw(value.into()) -} - -/// A value buffer as an async completion's owned `(result_ptr, result_len)` -/// run. -#[must_use] -pub fn buffer_run(value: &T) -> (*const u8, usize) { - crate::abi::bytes_into_raw(crate::abi::encode_value(value)) -} - -/// Borrow a string-like argument as the `(ptr, len)` slots of a callback -/// method call. -pub fn str_slots + ?Sized>(value: &S) -> (*const u8, usize) { - let s = value.as_ref(); - (s.as_ptr(), s.len()) -} - -/// Borrow a bytes-like argument as the `(ptr, len)` slots of a callback -/// method call. -pub fn byte_slots + ?Sized>(value: &B) -> (*const u8, usize) { - let b = value.as_ref(); - (b.as_ptr(), b.len()) +/// Returns a decode error when the `lift` function rejects the value. +pub fn lift_custom_buffered( + repr: C::Repr, +) -> Result { + C::lift(repr).map_err(|_| crate::abi::BufferDecodeError { + context: "a custom type's lift rejected the value", + }) } #[cfg(test)] @@ -450,15 +641,16 @@ mod tests { Blue, } - impl CEnum for Color { - fn from_i32(value: i32) -> Option { + impl Scalar for Color { + type Abi = i32; + fn from_abi(value: i32) -> Option { match value { 0 => Some(Self::Red), 2 => Some(Self::Blue), _ => None, } } - fn to_i32(&self) -> i32 { + fn to_abi(&self) -> i32 { match self { Self::Red => 0, Self::Blue => 2, @@ -468,9 +660,13 @@ mod tests { #[test] fn enums_lift_and_round_trip_through_buffers() { - assert_eq!(lift_enum::(2, "c").unwrap(), Color::Blue); - let err = lift_enum::(1, "c").unwrap_err(); + assert_eq!(lift_scalar_param::(2, "c").unwrap(), Color::Blue); + let err = lift_scalar_param::(1, "c").unwrap_err(); assert_eq!(err.code, MARSHAL_ERROR_CODE); + assert_eq!( + unsafe { err.message_str() }, + Some("c: 1 is not a valid Color") + ); let mut w = crate::abi::BufferWriter::new(); write_enum(&Color::Blue, &mut w); let bytes = w.finish(); @@ -478,6 +674,66 @@ mod tests { assert_eq!(read_enum::(&mut r).unwrap(), Color::Blue); } + #[test] + fn optionals_cross_as_a_flag_and_a_value() { + assert_eq!(lift_opt_param::(false, 99, "x").unwrap(), None); + assert_eq!(lift_opt_param::(true, 7, "x").unwrap(), Some(7)); + assert!(lift_opt_param::(true, 5, "x").is_err()); + assert_eq!(lift_opt_param::(false, 5, "x").unwrap(), None); + let mut out = 1.0f64; + assert!(!unsafe { lower_opt_ret::(None, &mut out) }); + assert_eq!(out, 0.0); + assert!(unsafe { lower_opt_ret(Some(2.5f64), &mut out) }); + assert_eq!(out, 2.5); + assert_eq!(opt_run(Some(3usize)), (true, 3u64)); + assert_eq!(opt_slots::(Some(&Color::Blue)), (true, 2)); + assert_eq!(opt_slots::(None), (false, false)); + } + + #[test] + fn slices_borrow_copy_convert_and_check() { + let xs = [1.5f64, -2.0, 3.25]; + let lent = unsafe { lift_slice_param(xs.as_ptr(), xs.len(), "xs") }.unwrap(); + assert!(std::ptr::eq(lent, &xs[..])); + assert!( + unsafe { lift_slice_param::(std::ptr::null(), 0, "xs") } + .unwrap() + .is_empty() + ); + assert!(unsafe { lift_slice_param::(std::ptr::null(), 2, "xs") }.is_err()); + let words = [0u64, 1, 2]; + let misaligned = unsafe { words.as_ptr().cast::().add(1) }.cast::(); + let err = unsafe { lift_slice_param(misaligned, 1, "ws") }.unwrap_err(); + assert_eq!(err.code, MARSHAL_ERROR_CODE); + let sizes: Vec = unsafe { lift_slice_vec_param(words.as_ptr(), 3, "ws") }.unwrap(); + assert_eq!(sizes, [0, 1, 2]); + + let mut len = 0usize; + let ptr = unsafe { lower_slice_ret(&[4usize, 5], &mut len) }; + assert_eq!(len, 2); + assert_eq!(ptr as usize % crate::abi::convert::RUN_ALIGN, 0); + assert_eq!(unsafe { std::slice::from_raw_parts(ptr, len) }, [4u64, 5]); + unsafe { crate::abi::free_bytes(ptr.cast(), len * 8) }; + let (ptr, len) = slice_run::(&[]); + assert!(ptr.is_null() && len == 0); + } + + #[test] + fn text_lifts_strings_and_chars() { + let s = "\u{1F980}"; + let c: char = unsafe { lift_text_param(s.as_ptr(), s.len(), "c") }.unwrap(); + assert_eq!(c, '\u{1F980}'); + let err = unsafe { lift_text_param::(b"ab".as_ptr(), 2, "c") }.unwrap_err(); + assert_eq!( + unsafe { err.message_str() }, + Some("c: \"ab\" is not a valid char") + ); + let mut len = 0usize; + let p = unsafe { lower_string_ret(&'x', &mut len) }; + assert_eq!(unsafe { crate::abi::lift_str(p, len) }, Some("x")); + unsafe { crate::abi::free_bytes(p.cast_mut(), len) }; + } + #[test] fn call_sync_reports_errors_and_panics() { let mut err = FfiError::new(1, "stale"); @@ -507,7 +763,7 @@ mod tests { Some("title is null or invalid") ); let bad = [0xffu8]; - assert!(unsafe { lift_string_param(bad.as_ptr(), 1, "s") }.is_err()); + assert!(unsafe { lift_text_param::(bad.as_ptr(), 1, "s") }.is_err()); let err = unsafe { lift_buffer_param::>(bad.as_ptr(), 1, "xs") }.unwrap_err(); assert!(unsafe { err.message_str() } .unwrap() @@ -516,5 +772,26 @@ mod tests { assert!(unsafe { lift_object_opt_param::(std::ptr::null()) } .unwrap() .is_none()); + struct Small; + impl Custom for Small { + type Repr = String; + type Value = u8; + fn lift(repr: String) -> Result { + repr.parse() + .map_err(|e: std::num::ParseIntError| e.to_string()) + } + fn lower(value: &u8) -> String { + value.to_string() + } + } + let err = lift_custom_param::("x".to_string(), "id").unwrap_err(); + assert!(unsafe { err.message_str() } + .unwrap() + .starts_with("id: invalid digit")); + assert_eq!( + lift_custom_param::("7".to_string(), "id").unwrap(), + 7 + ); + assert!(lift_custom_buffered::("300".to_string()).is_err()); } } diff --git a/crates/weaveffi/src/abi/mod.rs b/crates/weaveffi/src/abi/mod.rs index 4fe42192..7fd522f8 100644 --- a/crates/weaveffi/src/abi/mod.rs +++ b/crates/weaveffi/src/abi/mod.rs @@ -20,6 +20,7 @@ pub mod iter; pub mod leak; pub mod marshal; pub mod object; +pub mod scalar; pub mod spawn; pub use buffer::{ @@ -27,10 +28,11 @@ pub use buffer::{ ByValue, FixedWidth, }; pub use callback::{ - callback_ret_buffer, callback_ret_bytes, callback_ret_enum, callback_ret_object, - callback_ret_object_opt, callback_ret_string, callback_status, callback_status_in, - lift_callback, lift_callback_opt, CallbackInterface, ForeignCallback, ForeignError, Vtable, - VtableHeader, VtableTooSmall, + callback_ret_buffer, callback_ret_bytes, callback_ret_object, callback_ret_object_opt, + callback_ret_opt, callback_ret_scalar, callback_ret_slice, callback_ret_text, callback_status, + callback_status_in, convert_foreign, lift_callback, lift_callback_opt, lift_custom_returned, + CallbackInterface, ForeignCallback, ForeignError, Vtable, VtableHeader, VtableTooSmall, + OFF_THREAD_MESSAGE, VTABLE_THREAD_AFFINE, }; pub use cancel::{ cancel_token_cancel, cancel_token_create, cancel_token_destroy, cancel_token_is_cancelled, @@ -39,29 +41,32 @@ pub use cancel::{ pub use contract::{contract_compact, contract_len, contract_table, ContractEntry}; pub use convert::{ adopt_bytes, alloc, bytes_into_raw, free_bytes, lift_byte_slice, lift_bytes, lift_str, - lift_string, lower_bytes, lower_string, + lift_string, lower_bytes, lower_string, slice_into_raw, RUN_ALIGN, }; pub use error::{ boxed_error, error_clear, error_free, error_set, error_set_c, error_set_payload_c, error_store, - panic_message, ErrorDomain, ErrorReport, FfiError, CANCELLED_ERROR_CODE, FOREIGN_ERROR_CODE, + panic_message, ErrorDomain, FfiError, CANCELLED_ERROR_CODE, FOREIGN_ERROR_CODE, GENERIC_ERROR_CODE, MARSHAL_ERROR_CODE, PANIC_ERROR_CODE, }; pub use iter::{iter_destroy, iter_into_raw, iter_next, Iter, IterHandle}; pub use leak::debug_live; pub use marshal::{ - buffer_run, byte_slots, bytes_run, call_sync, lift_buffer_param, lift_bytes_param, - lift_callback_opt_param, lift_callback_param, lift_enum, lift_object_arc_opt_param, - lift_object_arc_param, lift_object_opt_param, lift_object_param, lift_self, lift_self_arc, - lift_slice_param, lift_str_param, lift_string_param, lower_buffer_ret, lower_bytes_ret, - lower_string_ret, read_enum, str_slots, string_run, write_enum, CEnum, Sentinel, + buffer_run, byte_slots, bytes_run, call_sync, lift_buffer_param, lift_byte_slice_param, + lift_bytes_param, lift_callback_opt_param, lift_callback_param, lift_custom_buffered, + lift_custom_param, lift_object_arc_opt_param, lift_object_arc_param, lift_object_opt_param, + lift_object_param, lift_opt_param, lift_scalar_param, lift_self, lift_self_arc, + lift_slice_param, lift_slice_vec_param, lift_str_param, lift_text_param, lower_buffer_ret, + lower_bytes_ret, lower_opt_ret, lower_slice_ret, lower_string_ret, opt_run, opt_slots, + read_enum, slice_run, string_run, write_enum, Sentinel, }; pub use object::{ lower_object, lower_object_opt, object_arc, object_clone, object_destroy, object_from_token, object_ref, object_to_token, }; +pub use scalar::{Custom, Scalar, Text}; pub use spawn::{ - block_on, default_pool_size, launch_async, run_async, set_spawner, spawn, BoxFuture, - CatchUnwind, SpawnError, Spawner, SpawnerAlreadySet, + block_on, launch_async, run_async, set_spawner, spawn, BoxFuture, CatchUnwind, SpawnError, + Spawner, SpawnerAlreadySet, }; /// The revision of the WeaveFFI C ABI this runtime implements. @@ -76,9 +81,11 @@ pub use spawn::{ /// the set and signatures of the runtime symbols) changes incompatibly. It's /// independent of the crate version and of the IDL schema version. /// -/// Revision 4 replaced the per-module checksums with per-declaration -/// contract tables, gave callback vtables a size-checked header and every -/// return family (with `throws` and optional callback parameters), exported -/// `{prefix}_alloc` on every target (removing `{prefix}_dealloc`), and added -/// `{prefix}_error_set_payload`. -pub const ABI_VERSION: u32 = 4; +/// Revision 5 gave the error struct a length-delimited message +/// (`message_ptr`, `message_len`, set with `{prefix}_error_set(err, code, +/// ptr, len)`), passes optional scalars (OptDirect) and numeric lists +/// (Slice) directly instead of through value buffers, allocates every byte +/// run with alignment 8, adds the thread-affine vtable flag, and splits the +/// contract tables' error-domain and callback-interface entries per code +/// and per method, so a revision-5 library can grow without a revision 6. +pub const ABI_VERSION: u32 = 5; diff --git a/crates/weaveffi/src/abi/scalar.rs b/crates/weaveffi/src/abi/scalar.rs new file mode 100644 index 00000000..b2e52e1e --- /dev/null +++ b/crates/weaveffi/src/abi/scalar.rs @@ -0,0 +1,191 @@ +//! The Rust types that cross the C ABI as one scalar slot ([`Scalar`]) or as +//! a UTF-8 run ([`Text`]), including the ones whose C spelling differs from +//! their Rust type. +//! +//! A [`Scalar`] has a C representation, its [`Abi`](Scalar::Abi) type, that +//! it converts to and from at the boundary. For the fixed-width integers, +//! the floats, and `bool` the two are the same type. `usize` and `isize` +//! cross as `u64` and `i64` (the same width on every platform), checked on +//! the way in, and a C-style enum crosses as its `i32` discriminant (the +//! `#[weaveffi::module]` expansion implements the trait for every +//! `#[repr(i32)]` `#[weaveffi::enumeration]`). The same conversions serve +//! every position: a parameter, a return, an optional (OptDirect) and a +//! typed array (Slice) of one, an async result, an iterator element, and a +//! callback method's parameters and return. +//! +//! A [`Text`] crosses as a UTF-8 run: `String`, `str`, and `char`, which is +//! a one-scalar string. +//! +//! A [`Custom`] type crosses as another type, its repr, through conversion +//! functions the producer supplies. + +use std::borrow::Cow; + +use crate::abi::marshal::Sentinel; + +/// A Rust type that crosses the C ABI as one scalar slot. +pub trait Scalar: Sized { + /// The C representation: the slot's Rust spelling (`u64` for `usize`, + /// `i32` for a C-style enum). + type Abi: Copy + Sentinel + std::fmt::Debug + 'static; + + /// Convert from the C representation, or `None` when the value has no + /// Rust counterpart (an out-of-range integer, an undeclared enum value). + fn from_abi(abi: Self::Abi) -> Option; + + /// Convert to the C representation. + fn to_abi(&self) -> Self::Abi; + + /// A typed array of `Self` as a typed array of [`Abi`](Self::Abi), + /// borrowed when the two are the same type. + fn abi_slice(items: &[Self]) -> Cow<'_, [Self::Abi]> { + Cow::Owned(items.iter().map(Self::to_abi).collect()) + } +} + +macro_rules! identity_scalar { + ($($t:ty),* $(,)?) => { + $(impl Scalar for $t { + type Abi = $t; + fn from_abi(abi: $t) -> Option { + Some(abi) + } + fn to_abi(&self) -> $t { + *self + } + fn abi_slice(items: &[Self]) -> Cow<'_, [$t]> { + Cow::Borrowed(items) + } + })* + }; +} + +identity_scalar!(bool, i8, u8, i16, u16, i32, u32, i64, u64, f32, f64); + +impl Scalar for usize { + type Abi = u64; + fn from_abi(abi: u64) -> Option { + usize::try_from(abi).ok() + } + fn to_abi(&self) -> u64 { + // `usize` is at most 64 bits wide on every supported target. + *self as u64 + } +} + +impl Scalar for isize { + type Abi = i64; + fn from_abi(abi: i64) -> Option { + isize::try_from(abi).ok() + } + fn to_abi(&self) -> i64 { + // `isize` is at most 64 bits wide on every supported target. + *self as i64 + } +} + +/// A Rust type that crosses the C ABI as UTF-8 text: `String`, `str`, and +/// `char` (a one-scalar string). +pub trait Text { + /// The value for `text`, or `None` when it has no counterpart (a + /// `char` from anything but exactly one Unicode scalar value, or any + /// text for a borrowed `str`, which can't be produced from one). + fn from_text(text: &str) -> Option + where + Self: Sized, + { + let _ = text; + None + } + + /// The value as text, borrowed when it already is. + fn as_text(&self) -> Cow<'_, str>; +} + +impl Text for String { + fn from_text(text: &str) -> Option { + Some(text.to_owned()) + } + fn as_text(&self) -> Cow<'_, str> { + Cow::Borrowed(self) + } +} + +impl Text for str { + fn as_text(&self) -> Cow<'_, str> { + Cow::Borrowed(self) + } +} + +impl Text for char { + fn from_text(text: &str) -> Option { + let mut chars = text.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => Some(c), + _ => None, + } + } + fn as_text(&self) -> Cow<'_, str> { + Cow::Owned(self.to_string()) + } +} + +impl Text for &str { + fn as_text(&self) -> Cow<'_, str> { + Cow::Borrowed(self) + } +} + +/// A custom type: a Rust type that crosses the ABI as its +/// [`Repr`](Self::Repr) (any type that crosses on its own, such as a +/// `String`), converted on the way in with a fallible `lift` and on the way +/// out with `lower`. +/// +/// The `#[weaveffi::module]` expansion implements it, on a hidden marker +/// type, for each `#[weaveffi::custom(repr = R, lift = f, lower = g)] pub +/// type Name = T;` in the module tree. The IR and every binding see `R`. +pub trait Custom { + /// The type the value crosses the ABI as. + type Repr; + /// The producer's type. + type Value; + + /// Convert from the repr. A failure fails the call with a marshalling + /// error carrying the message. + /// + /// # Errors + /// + /// Returns the producer's `lift` failure, rendered with `Display`. + fn lift(repr: Self::Repr) -> Result; + + /// Convert to the repr. + fn lower(value: &Self::Value) -> Self::Repr; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sizes_cross_as_64_bit_and_are_checked() { + assert_eq!(usize::from_abi(7), Some(7)); + assert_eq!(7usize.to_abi(), 7u64); + assert_eq!((-3isize).to_abi(), -3i64); + if usize::BITS < 64 { + assert_eq!(usize::from_abi(u64::MAX), None); + assert_eq!(isize::from_abi(i64::MIN), None); + } + assert_eq!(&*usize::abi_slice(&[1, 2]), &[1u64, 2]); + assert!(matches!(f64::abi_slice(&[1.0]), Cow::Borrowed(_))); + } + + #[test] + fn chars_are_one_scalar_strings() { + assert_eq!(char::from_text("\u{1F980}"), Some('\u{1F980}')); + assert_eq!(char::from_text(""), None); + assert_eq!(char::from_text("ab"), None); + assert_eq!('é'.as_text(), "é"); + assert_eq!(String::from_text("x").as_deref(), Some("x")); + assert_eq!("y".as_text(), "y"); + } +} diff --git a/crates/weaveffi/src/abi/spawn.rs b/crates/weaveffi/src/abi/spawn.rs index bbb43943..bbb6f4dc 100644 --- a/crates/weaveffi/src/abi/spawn.rs +++ b/crates/weaveffi/src/abi/spawn.rs @@ -5,16 +5,15 @@ //! completion callback that fires when the future resolves. Something has to //! drive the future in between: //! -//! * By default, a fixed pool of worker threads (one per available core, at -//! least two) started lazily on the first launch. Workers share one run -//! queue, and a future's waker puts it back on the queue, so a launch -//! never costs a thread. The pool has no reactor, and its workers are few: -//! a future that blocks a worker for a long time (synchronous I/O, a long -//! computation) holds up every other call, so such work belongs on a -//! runtime of its own (the `tokio` feature, or [`set_spawner`]). -//! * With the `tokio` feature, a Tokio runtime: the current one when the -//! launcher is called from inside a runtime, otherwise a multi-thread -//! runtime the library creates on first use. +//! * By default (the `tokio` feature, on by default), a Tokio runtime: the +//! current one when the launcher is called from inside a runtime, +//! otherwise a multi-thread runtime the library creates on first use +//! (its threads are named `weaveffi-async`). +//! * Without the `tokio` feature (`default-features = false`), each call +//! runs on a thread of its own, named `weaveffi-async`, which drives the +//! future with [`block_on`] and exits when it completes. There's no +//! reactor, so a future that awaits Tokio's I/O or timers never wakes; +//! a future woken from another thread resumes on its own thread. //! * On `wasm32`, which has no threads, the future is polled inline before //! the launcher returns. A future still pending once nothing wakes it //! completes with [`GENERIC_ERROR_CODE`] @@ -148,7 +147,11 @@ pub fn spawn(fut: impl Future + Send + 'static) -> Result<(), Spawn #[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))] fn default_spawn(fut: BoxFuture) -> Result<(), SpawnError> { - pool::spawn(fut) + std::thread::Builder::new() + .name("weaveffi-async".to_string()) + .spawn(move || block_on(fut)) + .map(drop) + .map_err(|e| SpawnError::Failed(format!("could not start a thread: {e}"))) } #[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] @@ -206,148 +209,6 @@ fn default_spawn(mut fut: BoxFuture) -> Result<(), SpawnError> { } } -/// The default native executor: a fixed pool of worker threads sharing one -/// run queue, started on the first launch. -#[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))] -mod pool { - use std::collections::VecDeque; - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError}; - use std::task::{Context, Wake, Waker}; - - use super::{BoxFuture, SpawnError}; - - struct Pool { - queue: Mutex>>, - ready: Condvar, - } - - struct Task { - future: Mutex>, - /// Set while the task sits on the queue, so a burst of wakes queues - /// it once. - queued: AtomicBool, - pool: &'static Pool, - } - - impl Task { - fn schedule(self: Arc) { - if !self.queued.swap(true, Ordering::AcqRel) { - let pool = self.pool; - pool.queue - .lock() - .unwrap_or_else(PoisonError::into_inner) - .push_back(self); - pool.ready.notify_one(); - } - } - } - - impl Wake for Task { - fn wake(self: Arc) { - self.schedule(); - } - fn wake_by_ref(self: &Arc) { - Arc::clone(self).schedule(); - } - } - - impl Pool { - fn work(&'static self) { - loop { - let task = { - let mut queue = self.queue.lock().unwrap_or_else(PoisonError::into_inner); - loop { - match queue.pop_front() { - Some(task) => break task, - None => { - queue = self - .ready - .wait(queue) - .unwrap_or_else(PoisonError::into_inner); - } - } - } - }; - // Clear the flag before polling, so a wake during the poll - // queues the task again. - task.queued.store(false, Ordering::Release); - let mut slot = task.future.lock().unwrap_or_else(PoisonError::into_inner); - if let Some(fut) = slot.as_mut() { - let waker = Waker::from(Arc::clone(&task)); - // The launcher wraps every future in `CatchUnwind`, so - // polling never unwinds into the worker. - if fut - .as_mut() - .poll(&mut Context::from_waker(&waker)) - .is_ready() - { - *slot = None; - } - } - } - } - } - - /// The number of workers the pool starts. - pub(super) fn size() -> usize { - std::thread::available_parallelism() - .map_or(2, std::num::NonZero::get) - .max(2) - } - - fn pool() -> Result<&'static Pool, SpawnError> { - static POOL: OnceLock> = OnceLock::new(); - POOL.get_or_init(|| { - let pool: &'static Pool = Box::leak(Box::new(Pool { - queue: Mutex::new(VecDeque::new()), - ready: Condvar::new(), - })); - let started = (0..size()) - .filter(|i| { - std::thread::Builder::new() - .name(format!("weaveffi-async-{i}")) - .spawn(move || pool.work()) - .is_ok() - }) - .count(); - if started == 0 { - Err("no executor thread could be started".to_string()) - } else { - Ok(pool) - } - }) - .clone() - .map_err(SpawnError::Failed) - } - - pub(super) fn spawn(fut: BoxFuture) -> Result<(), SpawnError> { - let pool = pool()?; - Arc::new(Task { - future: Mutex::new(Some(fut)), - queued: AtomicBool::new(false), - pool, - }) - .schedule(); - Ok(()) - } -} - -/// The number of worker threads the default executor runs: one per -/// available core, at least two. (With the `tokio` feature or on `wasm32` -/// the default executor is something else, and this is `0`.) -#[must_use] -pub fn default_pool_size() -> usize { - #[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))] - { - pool::size() - } - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] - { - 0 - } -} - /// Drive a future to completion on the current thread, blocking until it /// resolves. /// diff --git a/crates/weaveffi/src/lib.rs b/crates/weaveffi/src/lib.rs index 339327ca..42a9de20 100644 --- a/crates/weaveffi/src/lib.rs +++ b/crates/weaveffi/src/lib.rs @@ -11,7 +11,7 @@ //! ```ignore //! #[weaveffi::module] //! pub mod math { -//! /// The module's error domain; `Display` supplies the runtime message. +//! /// The module's error domain. Its `Display` comes from the messages. //! #[weaveffi::error] //! #[derive(Debug)] //! pub enum MathError { @@ -19,12 +19,6 @@ //! DivisionByZero = 1, //! } //! -//! impl std::fmt::Display for MathError { -//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -//! f.write_str("division by zero") -//! } -//! } -//! //! /// Add two integers. //! #[weaveffi::export] //! pub fn add(a: i32, b: i32) -> i32 { @@ -36,6 +30,12 @@ //! pub fn div(a: i32, b: i32) -> Result { //! a.checked_div(b).ok_or(MathError::DivisionByZero) //! } +//! +//! /// Parse an integer; any error type with `Display` works (`throws any`). +//! #[weaveffi::export] +//! pub fn parse(text: &str) -> Result { +//! text.parse() +//! } //! } //! //! // Export the runtime symbols (memory, errors, cancel tokens) once. @@ -51,34 +51,42 @@ //! //! * [`macro@module`] - the driver attribute on an exported `mod`. //! * [`macro@export`] - export a function (`async fn` is asynchronous; a -//! `Result`-returning fn is fallible). +//! `Result`-returning fn is fallible: `throws` its error type when that's +//! a [`macro@error`] enum of the module tree, else `throws any`). //! * [`macro@record`] - a by-value struct serialized across the ABI. //! * [`macro@enumeration`] - a `#[repr(i32)]` C-style enum, or a rich enum //! with data-carrying variants. //! * [`macro@interface`] - an opaque, reference-counted object type; pass one //! as `&T` or `Arc`, return one as `Self`, `T`, or `Arc`. -//! * [`macro@error`] - the module's error domain. +//! [`macro@skip`] leaves a `pub fn` of its `impl` unexported. +//! * [`macro@error`] - an error domain; the macro generates its `Display` +//! (from `#[weaveffi(message = "...")]` templates or the variants' docs) +//! and `std::error::Error`. //! * [`macro@callback_interface`] - a trait the consumer implements; accept //! one as `Arc` (or `Option>`). Its methods -//! return `Result`; [`macro@throws`] lets one report the -//! module's domain errors. +//! return `Result` with `E: From`; when `E` is a +//! declared domain, the consumer's typed errors arrive typed. +//! * [`macro@custom`] - a type alias that crosses as another type (its +//! repr), converted with your `lift` and `lower` functions. //! * [`macro@cancellable`] - mark an `async fn` as accepting a cancel token. //! * [`set_spawner`] - install the executor async exports run on; the default -//! is a small pool of worker threads, or Tokio with the `tokio` feature. +//! is Tokio (the `tokio` feature), else a thread per call. //! * [`export_runtime!`] - export the runtime symbols (memory, errors, cancel -//! tokens, ABI version) under the crate's prefix, once per library. +//! tokens, ABI version) under the crate's prefix, once per library. A +//! crate whose modules use the macros fails to compile without it. //! * [`abi`] - the C ABI runtime: the error struct, memory helpers, and the //! marshalling converters the expansion calls. //! //! # Features //! +//! * `tokio` (default) runs exported `async fn`s on Tokio: the current +//! runtime when a launcher is called from inside one, otherwise a +//! multi-thread runtime created on first use. Without it, each async call +//! runs on a thread of its own. [`set_spawner`] overrides either. On +//! `wasm32` the feature has no effect: calls are polled inline. //! * `leak-check` counts live objects, callbacks, iterators, cancel tokens, //! and returned allocations, reported by `{prefix}_debug_live` so a test //! harness can assert a consumer released everything. Off by default. -//! * `tokio` runs exported `async fn`s on Tokio: the current runtime when a -//! launcher is called from inside one, otherwise a multi-thread runtime -//! created on first use. [`set_spawner`] still overrides it. Off by -//! default. #![deny(missing_docs)] #![warn(clippy::missing_errors_doc)] @@ -102,30 +110,21 @@ pub use abi::Iter; /// cooperative cleanup (work on other threads, say). pub use abi::CancelToken; -/// A consumer's callback-interface implementation failed. Every callback -/// trait method returns `Result`, so the failure arrives as -/// an `Err`; a method marked `#[weaveffi::throws]` can decode a declared -/// domain error from it with [`ForeignError::domain`](abi::ForeignError::domain). +/// A consumer's callback-interface implementation failed. A callback trait +/// method's error type converts from it (`E: From`), and it's +/// a valid error type itself; [`ForeignError::domain`](abi::ForeignError::domain) +/// decodes a declared domain error from one. pub use abi::ForeignError; -/// A module's error domain: the trait the `#[weaveffi::error]` expansion -/// implements so [`ForeignError::domain`](abi::ForeignError::domain) can -/// decode a typed error a callback reported. +/// A declared error domain: the trait the `#[weaveffi::error]` expansion +/// implements, mapping each variant to its code, message, and payload. pub use abi::ErrorDomain; -/// Maps a producer error onto the ABI's `(code, message)` pair. A fallible -/// `#[weaveffi::export]` function reports `Err(e)` through its trailing -/// `out_err` slot using this trait: `String` and `&str` errors get the -/// generic code `-1` out of the box, while a `#[weaveffi::error]` enum (or a -/// manual [`ErrorReport`] impl) surfaces the named codes of an IDL error -/// domain, with its `Display` output as the message. -pub use abi::ErrorReport; - /// Install the process-wide executor that exported `async fn`s run on. Call it /// once at startup (before the first async export is launched) to hand futures /// to a runtime of your choice; until then, and if never called, futures run -/// on the default executor (a small worker pool, or Tokio with the `tokio` -/// feature). +/// on the default executor (Tokio with the default `tokio` feature, else a +/// thread per call). pub use abi::set_spawner; /// The executor hook [`set_spawner`] accepts: anything callable as @@ -136,6 +135,6 @@ pub use abi::Spawner; pub use abi::BoxFuture; pub use weaveffi_macros::{ - callback_interface, cancellable, enumeration, error, export, export_runtime, interface, module, - record, throws, + callback_interface, cancellable, custom, enumeration, error, export, export_runtime, interface, + module, record, skip, }; diff --git a/crates/weaveffi/tests/abi5.rs b/crates/weaveffi/tests/abi5.rs new file mode 100644 index 00000000..407d9e71 --- /dev/null +++ b/crates/weaveffi/tests/abi5.rs @@ -0,0 +1,653 @@ +//! The C ABI revision 5 families end to end: optional scalars (OptDirect) +//! and numeric lists (Slice) in every position, `usize`/`isize`/`char`, +//! custom types, typed callback errors, and thread-affine vtables. This +//! test crate is `abi5`, so that's the prefix of every symbol. + +#![allow(unsafe_code)] + +use std::os::raw::c_void; +use std::sync::mpsc; +use std::time::Duration; + +use weaveffi::abi::{self, FfiError}; + +const WAIT: Duration = Duration::from_secs(30); + +/// A hex-encoded `u32`: crosses as a `string`. +fn parse_hex(text: String) -> Result { + u32::from_str_radix(&text, 16) +} + +fn to_hex(value: &u32) -> String { + format!("{value:x}") +} + +#[weaveffi::module] +pub mod fam { + use std::sync::Arc; + + use weaveffi::ForeignError; + + /// A C-style enum, optional at the boundary. + #[weaveffi::enumeration] + #[repr(i32)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Mode { + /// Off. + Off = 0, + /// On. + On = 1, + } + + /// A number written in hex. + #[weaveffi::custom(repr = String, lift = super::parse_hex, lower = super::to_hex)] + pub type Hex = u32; + + /// A record with custom-typed fields. + #[weaveffi::record] + #[derive(Clone, Debug, PartialEq)] + pub struct Tagged { + /// The id. + pub id: Hex, + /// More ids. + pub others: Vec, + /// A plain field. + pub label: String, + } + + /// The probe's errors. + #[weaveffi::error] + #[derive(Debug, PartialEq)] + #[repr(i32)] + pub enum ProbeError { + /// a bad reading + #[weaveffi(message = "bad reading {n}")] + Bad { + /// The reading. + n: i32, + } = 1, + /// the probe failed + #[weaveffi(message = "probe failed ({code}): {message}")] + Failed { + /// The runtime code. + code: i32, + /// The message. + message: String, + } = 2, + } + + impl From for ProbeError { + fn from(e: ForeignError) -> Self { + Self::Failed { + code: e.code, + message: e.message, + } + } + } + + /// A consumer-implemented probe. + #[weaveffi::callback_interface] + pub trait Probe: Send + Sync { + /// The next reading after `last`, if any. + fn reading(&self, last: Option) -> Result, ProbeError>; + /// `n` samples, scaled by the elements of `scale`. + fn samples(&self, scale: &[f32]) -> Result, ForeignError>; + /// A one-character tag. + fn tag(&self) -> Result; + /// A notification with no return. + fn ping(&self, count: usize) -> Result<(), ForeignError>; + } + + /// Half of `x`, if any. + #[weaveffi::export] + pub fn half(x: Option) -> Option { + x.map(|v| f64::from(v) / 2.0) + } + + /// The other mode, if any. + #[weaveffi::export] + pub fn flip(mode: Option) -> Option { + mode.map(|m| match m { + Mode::Off => Mode::On, + Mode::On => Mode::Off, + }) + } + + /// Negate a flag. + #[weaveffi::export] + pub fn not(flag: Option) -> Option { + flag.map(|f| !f) + } + + /// Scale every element (a borrowed array in, an array out). + #[weaveffi::export] + pub fn scale(xs: &[f64], by: f64) -> Vec { + xs.iter().map(|x| x * by).collect() + } + + /// The address of a borrowed array, proving it wasn't copied. + #[weaveffi::export] + pub fn addr(xs: &[i64]) -> u64 { + xs.as_ptr() as u64 + } + + /// Sizes doubled (`usize` crosses as `u64`). + #[weaveffi::export] + pub fn doubled(sizes: Vec) -> Vec { + sizes.iter().map(|s| s * 2).collect() + } + + /// A count (`usize` in and out). + #[weaveffi::export] + pub fn count(n: usize) -> usize { + n + 1 + } + + /// An offset (`isize` in and out). + #[weaveffi::export] + pub fn offset(n: isize) -> isize { + n - 1 + } + + /// The first character of `name`. + #[weaveffi::export] + pub fn initial(name: &str) -> char { + name.chars().next().unwrap_or('?') + } + + /// A character, repeated. + #[weaveffi::export] + pub fn repeat(c: char, n: u8) -> String { + std::iter::repeat_n(c, usize::from(n)).collect() + } + + /// Add one to a hex number. + #[weaveffi::export] + pub fn bump(h: Hex) -> Hex { + h + 1 + } + + /// The largest of several hex numbers. + #[weaveffi::export] + pub fn largest(hs: Vec) -> Option { + hs.into_iter().max() + } + + /// Tag an id. + #[weaveffi::export] + pub fn tagged(id: Hex, label: String) -> Tagged { + Tagged { + id, + others: vec![id + 1, id + 2], + label, + } + } + + /// The sum of a tagged record's ids. + #[weaveffi::export] + pub fn total(t: &Tagged) -> u32 { + t.id + t.others.iter().sum::() + } + + /// Complete later with `x` doubled, if any. + #[weaveffi::export] + pub async fn double_later(x: Option) -> Option { + x.map(|v| v * 2) + } + + /// Complete later with `0..n`. + #[weaveffi::export] + pub async fn range_later(n: u32) -> Vec { + (0..i64::from(n)).collect() + } + + /// `n` arrays of `0..i`, pulled lazily. + #[weaveffi::export] + pub fn ramps(n: u16) -> weaveffi::Iter> { + weaveffi::Iter::new((0..n).map(|i| (0..i).collect())) + } + + /// Alternating flags with a gap, pulled lazily. + #[weaveffi::export] + pub fn flags(n: u8) -> weaveffi::Iter> { + weaveffi::Iter::new((0..n).map(|i| (i % 3 != 2).then_some(i % 2 == 0))) + } + + /// Exercise every method of `probe` and describe the results. + #[weaveffi::export] + pub fn survey(probe: Arc) -> Result { + let first = probe.reading(None)?; + let second = probe.reading(first)?; + let samples = probe.samples(&[1.0, 2.0]).map_err(ProbeError::from)?; + let tag = probe.tag().map_err(ProbeError::from)?; + probe.ping(3).map_err(ProbeError::from)?; + Ok(format!("{first:?} {second:?} {samples:?} {tag}")) + } + + /// Call `reading` from another thread and report the outcome. + #[weaveffi::export] + pub fn read_elsewhere(probe: Arc) -> String { + std::thread::spawn(move || { + probe + .ping(1) + .map_err(ProbeError::from) + .and(probe.reading(None)) + }) + .join() + .map_or_else(|_| "panicked".to_string(), |r| format!("{r:?}")) + } +} + +weaveffi::export_runtime!(); + +fn message(err: &FfiError) -> String { + unsafe { err.message_str() }.unwrap_or_default().to_string() +} + +fn take_string(ptr: *const u8, len: usize) -> String { + let s = unsafe { abi::lift_string(ptr, len) }.expect("valid UTF-8"); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; + s +} + +/// Copy a returned typed array and release it, as a binding does. +fn take_slice(ptr: *const T, len: usize) -> Vec { + if len == 0 { + assert!(ptr.is_null()); + return Vec::new(); + } + assert_eq!(ptr as usize % abi::RUN_ALIGN, 0, "every run is 8-aligned"); + let out = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec(); + unsafe { abi::free_bytes(ptr.cast_mut().cast(), len * std::mem::size_of::()) }; + out +} + +#[test] +fn optional_scalars_cross_directly() { + let mut err = FfiError::default(); + let mut out = 0.0f64; + assert!(unsafe { fam::abi5_fam_half(true, 7, &mut out, &mut err) }); + assert_eq!(out, 3.5); + assert!(!unsafe { fam::abi5_fam_half(false, 99, &mut out, &mut err) }); + assert_eq!(err.code, 0); + + let mut mode = -1i32; + assert!(unsafe { fam::abi5_fam_flip(true, 1, &mut mode, &mut err) }); + assert_eq!(mode, 0); + assert!(!unsafe { fam::abi5_fam_flip(false, 0, &mut mode, &mut err) }); + // A present value is still checked; an absent one is ignored. + assert!(!unsafe { fam::abi5_fam_flip(true, 7, &mut mode, &mut err) }); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + assert_eq!(message(&err), "mode: 7 is not a valid Mode"); + assert!(!unsafe { fam::abi5_fam_flip(false, 7, &mut mode, &mut err) }); + assert_eq!(err.code, 0); + + let mut flag = true; + assert!(unsafe { fam::abi5_fam_not(true, true, &mut flag, &mut err) }); + assert!(!flag); +} + +#[test] +fn numeric_lists_cross_as_typed_arrays() { + let mut err = FfiError::default(); + let xs = [1.0f64, -2.5, 4.0]; + let mut len = 0usize; + let ptr = unsafe { fam::abi5_fam_scale(xs.as_ptr(), xs.len(), 2.0, &mut len, &mut err) }; + assert_eq!(take_slice(ptr, len), [2.0, -5.0, 8.0]); + let ptr = unsafe { fam::abi5_fam_scale(std::ptr::null(), 0, 2.0, &mut len, &mut err) }; + assert_eq!(take_slice(ptr, len), Vec::::new()); + + let words = [1i64, 2, 3]; + assert_eq!( + unsafe { fam::abi5_fam_addr(words.as_ptr(), 3, &mut err) }, + words.as_ptr() as u64, + "a borrowed slice is lent, not copied" + ); + let misaligned = unsafe { words.as_ptr().cast::().add(4) }.cast::(); + assert_eq!(unsafe { fam::abi5_fam_addr(misaligned, 1, &mut err) }, 0); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + + let sizes = [1u64, 20, 300]; + let ptr = unsafe { fam::abi5_fam_doubled(sizes.as_ptr(), 3, &mut len, &mut err) }; + assert_eq!(err.code, 0); + assert_eq!(take_slice(ptr, len), [2u64, 40, 600]); +} + +#[test] +fn sizes_and_chars_have_idl_types() { + let mut err = FfiError::default(); + assert_eq!(unsafe { fam::abi5_fam_count(41, &mut err) }, 42u64); + assert_eq!(unsafe { fam::abi5_fam_offset(-41, &mut err) }, -42i64); + if usize::BITS < 64 { + assert_eq!(unsafe { fam::abi5_fam_count(u64::MAX, &mut err) }, 0); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + } + + let name = "\u{1F980}rab"; + let mut len = 0usize; + let ptr = unsafe { fam::abi5_fam_initial(name.as_ptr(), name.len(), &mut len, &mut err) }; + assert_eq!(take_string(ptr, len), "\u{1F980}"); + let c = "é"; + let ptr = unsafe { fam::abi5_fam_repeat(c.as_ptr(), c.len(), 3, &mut len, &mut err) }; + assert_eq!(take_string(ptr, len), "ééé"); + let two = "ab"; + let ptr = unsafe { fam::abi5_fam_repeat(two.as_ptr(), two.len(), 3, &mut len, &mut err) }; + assert!(ptr.is_null()); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + assert_eq!(message(&err), "c: \"ab\" is not a valid char"); +} + +#[test] +fn custom_types_cross_as_their_repr() { + let mut err = FfiError::default(); + let mut len = 0usize; + let h = "ff"; + let ptr = unsafe { fam::abi5_fam_bump(h.as_ptr(), h.len(), &mut len, &mut err) }; + assert_eq!(take_string(ptr, len), "100"); + let bad = "zz"; + let ptr = unsafe { fam::abi5_fam_bump(bad.as_ptr(), bad.len(), &mut len, &mut err) }; + assert!(ptr.is_null()); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + assert_eq!(message(&err), "h: invalid digit found in string"); + + let hs = abi::encode_value(&vec!["a".to_string(), "1f".to_string()]); + let ptr = unsafe { fam::abi5_fam_largest(hs.as_ptr(), hs.len(), &mut len, &mut err) }; + let bytes = take_slice(ptr, len); + assert_eq!( + unsafe { abi::decode_value::>(&bytes) }.unwrap(), + Some("1f".to_string()) + ); + + let label = "x"; + let ptr = unsafe { + fam::abi5_fam_tagged( + h.as_ptr(), + h.len(), + label.as_ptr(), + label.len(), + &mut len, + &mut err, + ) + }; + let bytes = take_slice(ptr, len); + // The record's custom fields encode as their repr... + let mut r = abi::BufferReader::new(&bytes); + assert_eq!(r.read_string().unwrap(), "ff"); + assert_eq!(r.read_count().unwrap(), 2); + assert_eq!(r.read_string().unwrap(), "100"); + // ...and decode back through `lift`. + let tagged: fam::Tagged = unsafe { abi::decode_value(&bytes) }.unwrap(); + assert_eq!(tagged.id, 255); + assert_eq!(tagged.others, [256, 257]); + assert_eq!( + unsafe { fam::abi5_fam_total(bytes.as_ptr(), bytes.len(), &mut err) }, + 255 + 256 + 257 + ); +} + +type Done = mpsc::Sender<(i32, T)>; + +#[test] +fn async_results_of_the_new_families() { + extern "C" fn on_opt(ctx: *mut c_void, err: *mut FfiError, has: bool, value: u32) { + assert!(err.is_null()); + let tx = unsafe { &*ctx.cast::>>() }.clone(); + tx.send((0, has.then_some(value))).unwrap(); + } + extern "C" fn on_slice(ctx: *mut c_void, err: *mut FfiError, ptr: *const i64, len: usize) { + assert!(err.is_null()); + let tx = unsafe { &*ctx.cast::>>() }.clone(); + tx.send((0, take_slice(ptr, len))).unwrap(); + } + + let (tx, rx) = mpsc::channel::<(i32, Option)>(); + let ctx: *mut c_void = Box::into_raw(Box::new(tx)).cast(); + unsafe { fam::abi5_fam_double_later(true, 21, on_opt, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, Some(42))); + unsafe { fam::abi5_fam_double_later(false, 21, on_opt, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, None)); + drop(unsafe { Box::from_raw(ctx.cast::>>()) }); + + let (tx, rx) = mpsc::channel::<(i32, Vec)>(); + let ctx: *mut c_void = Box::into_raw(Box::new(tx)).cast(); + unsafe { fam::abi5_fam_range_later(4, on_slice, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, vec![0, 1, 2, 3])); + unsafe { fam::abi5_fam_range_later(0, on_slice, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, vec![])); + drop(unsafe { Box::from_raw(ctx.cast::>>()) }); +} + +#[test] +fn iterator_items_of_the_new_families() { + let mut err = FfiError::default(); + let it = unsafe { fam::abi5_fam_ramps(3, &mut err) }; + let mut got = Vec::new(); + loop { + let mut item: *mut u16 = std::ptr::null_mut(); + let mut len = 0usize; + if unsafe { fam::abi5_fam_RampsIterator_next(it, &mut item, &mut len, &mut err) } == 0 { + break; + } + got.push(take_slice(item, len)); + } + unsafe { fam::abi5_fam_RampsIterator_destroy(it) }; + assert_eq!(got, vec![vec![], vec![0], vec![0, 1]]); + + let it = unsafe { fam::abi5_fam_flags(4, &mut err) }; + let mut got = Vec::new(); + loop { + let (mut has, mut item) = (false, false); + if unsafe { fam::abi5_fam_FlagsIterator_next(it, &mut has, &mut item, &mut err) } == 0 { + break; + } + got.push(has.then_some(item)); + } + // A null out slot fails before an element is pulled. + let mut item = false; + let r = + unsafe { fam::abi5_fam_FlagsIterator_next(it, std::ptr::null_mut(), &mut item, &mut err) }; + assert_eq!((r, err.code), (0, abi::MARSHAL_ERROR_CODE)); + unsafe { fam::abi5_fam_FlagsIterator_destroy(it) }; + assert_eq!(got, vec![Some(true), Some(false), None, Some(false)]); +} + +/// A consumer-side `Probe`. +mod consumer_probe { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + pub struct State { + pub fail_reading: bool, + pub pings: AtomicUsize, + } + + unsafe extern "C" fn reading( + ctx: *mut c_void, + has_last: bool, + last: f64, + out_value: *mut f64, + out_err: *mut FfiError, + ) -> bool { + let state = unsafe { &*ctx.cast::() }; + if state.fail_reading { + // A typed failure of the method's own domain, with its payload. + let msg = "bad"; + let fields = abi::encode_value(&7i32); + unsafe { + super::abi5_error_set(out_err, 1, msg.as_ptr(), msg.len()); + super::abi5_error_set_payload(out_err, fields.as_ptr(), fields.len()); + } + return false; + } + if has_last { + if last > 1.0 { + return false; + } + unsafe { *out_value = last + 1.0 }; + } else { + unsafe { *out_value = 0.5 }; + } + true + } + + unsafe extern "C" fn samples( + _ctx: *mut c_void, + scale_ptr: *const f32, + scale_len: usize, + out_ptr: *mut *mut f32, + out_len: *mut usize, + _err: *mut FfiError, + ) { + let scale = unsafe { std::slice::from_raw_parts(scale_ptr, scale_len) }; + let run = super::abi5_alloc(scale.len() * 4).cast::(); + for (i, s) in scale.iter().enumerate() { + unsafe { run.add(i).write(s * 10.0) }; + } + unsafe { + *out_ptr = run; + *out_len = scale.len(); + } + } + + unsafe extern "C" fn tag( + _ctx: *mut c_void, + out_ptr: *mut *mut u8, + out_len: *mut usize, + _err: *mut FfiError, + ) { + let text = "λ"; + let run = super::abi5_alloc(text.len()); + unsafe { + std::ptr::copy_nonoverlapping(text.as_ptr(), run, text.len()); + *out_ptr = run; + *out_len = text.len(); + } + } + + unsafe extern "C" fn ping(ctx: *mut c_void, count: u64, _err: *mut FfiError) { + let state = unsafe { &*ctx.cast::() }; + state.pings.fetch_add(count as usize, Ordering::SeqCst); + } + + unsafe extern "C" fn free(ctx: *mut c_void) { + drop(unsafe { Box::from_raw(ctx.cast::()) }); + } + + pub fn vtable(flags: u32) -> fam::abi5_fam_Probe_vtable { + fam::abi5_fam_Probe_vtable { + header: abi::VtableHeader { + size: std::mem::size_of::() as u32, + flags, + free, + }, + reading, + samples, + tag, + ping, + } + } + + pub fn ctx(fail_reading: bool) -> *mut c_void { + Box::into_raw(Box::new(State { + fail_reading, + pings: AtomicUsize::new(0), + })) + .cast() + } +} + +#[test] +fn callback_methods_with_the_new_families() { + let vtable = Box::leak(Box::new(consumer_probe::vtable(0))); + let mut err = FfiError::default(); + let mut len = 0usize; + let ptr = + unsafe { fam::abi5_fam_survey(consumer_probe::ctx(false), vtable, &mut len, &mut err) }; + assert_eq!(err.code, 0, "{}", message(&err)); + assert_eq!(take_string(ptr, len), "Some(0.5) Some(1.5) [10.0, 20.0] λ"); + + // The consumer's typed failure arrives typed and propagates with its + // code, message, and payload. + let ptr = + unsafe { fam::abi5_fam_survey(consumer_probe::ctx(true), vtable, &mut len, &mut err) }; + assert!(ptr.is_null()); + assert_eq!(err.code, 1); + assert_eq!(message(&err), "bad reading 7"); + assert_eq!( + unsafe { abi::decode_value::(err.payload()) }.unwrap(), + 7 + ); + unsafe { abi::error_clear(&mut err) }; +} + +#[test] +fn thread_affine_vtables_reject_value_methods_off_thread() { + let mut err = FfiError::default(); + let mut len = 0usize; + let affine = Box::leak(Box::new(consumer_probe::vtable(abi::VTABLE_THREAD_AFFINE))); + let ptr = unsafe { + fam::abi5_fam_read_elsewhere(consumer_probe::ctx(false), affine, &mut len, &mut err) + }; + // `ping` (no return value) still ran on the other thread; `reading` + // failed without being called, reaching the producer as `-4`. + assert_eq!( + take_string(ptr, len), + "Err(Failed { code: -4, message: \"callback called off its thread\" })" + ); + let free_threaded = Box::leak(Box::new(consumer_probe::vtable(0))); + let ptr = unsafe { + fam::abi5_fam_read_elsewhere( + consumer_probe::ctx(false), + free_threaded, + &mut len, + &mut err, + ) + }; + assert_eq!(take_string(ptr, len), "Ok(Some(0.5))"); + // On the adopting thread, an affine vtable works normally. + let ptr = + unsafe { fam::abi5_fam_survey(consumer_probe::ctx(false), affine, &mut len, &mut err) }; + assert_eq!(err.code, 0); + assert_eq!(take_string(ptr, len), "Some(0.5) Some(1.5) [10.0, 20.0] λ"); +} + +#[test] +fn contract_tables_carry_per_method_and_per_code_entries() { + fn fnv(data: &str) -> u64 { + data.bytes().fold(0xcbf2_9ce4_8422_2325, |h, b| { + (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3) + }) + } + let mut len = 0usize; + let ptr = unsafe { fam::abi5_fam_contract(&mut len) }; + let table = unsafe { std::slice::from_raw_parts(ptr, len) }; + let hash = |path: &str| table.iter().find(|e| e.id == fnv(path)).map(|e| e.hash); + assert_eq!(hash("fam.Probe"), Some(fnv("callback Probe"))); + assert_eq!( + hash("fam.Probe.reading"), + Some(fnv( + "callback_method #0 reading(f64?) -> f64? throws ProbeError" + )) + ); + assert_eq!( + hash("fam.Probe.samples"), + Some(fnv("callback_method #1 samples([f32]) -> [f32] throws any")) + ); + assert_eq!( + hash("fam.Probe.ping"), + Some(fnv("callback_method #3 ping(u64) -> void throws any")) + ); + assert_eq!(hash("fam.ProbeError"), Some(fnv("errors ProbeError"))); + assert_eq!( + hash("fam.ProbeError.Failed"), + Some(fnv("code Failed = 2 {i32, string}")) + ); + assert_eq!( + hash("fam.tagged"), + Some(fnv("function tagged(string, string) -> Tagged")) + ); + assert_eq!( + hash("fam.Tagged"), + Some(fnv("record Tagged {string, [string], string}")) + ); +} diff --git a/crates/weaveffi/tests/dropped_future.rs b/crates/weaveffi/tests/dropped_future.rs index ad811b06..a288eea2 100644 --- a/crates/weaveffi/tests/dropped_future.rs +++ b/crates/weaveffi/tests/dropped_future.rs @@ -19,6 +19,8 @@ pub mod work { } } +weaveffi::export_runtime!(); + extern "C" fn done(ctx: *mut c_void, err: *mut FfiError, result: i32) { // Clone the sender before sending: the test may free the context as soon // as the value arrives, which can be before `send` returns. diff --git a/crates/weaveffi/tests/export_runtime.rs b/crates/weaveffi/tests/export_runtime.rs index d6d77443..52ac1f79 100644 --- a/crates/weaveffi/tests/export_runtime.rs +++ b/crates/weaveffi/tests/export_runtime.rs @@ -4,8 +4,6 @@ #![allow(unsafe_code)] -use std::ffi::CString; -use std::os::raw::c_char; use std::ptr; use weaveffi::abi::{self, FfiCancelToken, FfiError}; @@ -15,7 +13,7 @@ weaveffi::export_runtime!(); #[test] fn every_runtime_symbol_has_its_c_signature() { let _: extern "C" fn() -> u32 = export_runtime_abi_version; - let _: unsafe extern "C" fn(*mut FfiError, i32, *const c_char) = export_runtime_error_set; + let _: unsafe extern "C" fn(*mut FfiError, i32, *const u8, usize) = export_runtime_error_set; let _: unsafe extern "C" fn(*mut FfiError, *const u8, usize) = export_runtime_error_set_payload; let _: extern "C" fn(usize) -> *mut u8 = export_runtime_alloc; let _: unsafe extern "C" fn(*mut FfiError) = export_runtime_error_clear; @@ -27,22 +25,31 @@ fn every_runtime_symbol_has_its_c_signature() { export_runtime_cancel_token_is_cancelled; let _: unsafe extern "C" fn(*mut FfiCancelToken) = export_runtime_cancel_token_destroy; let _: extern "C" fn(i32) -> u64 = export_runtime_debug_live; - assert_eq!(export_runtime_abi_version(), 4); + assert_eq!(export_runtime_abi_version(), 5); } #[test] fn error_set_copies_and_clear_frees() { let mut err = FfiError::default(); - let msg = CString::new("consumer failure").unwrap(); - unsafe { export_runtime_error_set(&mut err, abi::FOREIGN_ERROR_CODE, msg.as_ptr()) }; - drop(msg); + // The message is a length-delimited run, not NUL-terminated: a slice of + // a larger buffer works, and an interior NUL survives. + let backing = b"consumer\0failure, and more".to_vec(); + unsafe { export_runtime_error_set(&mut err, abi::FOREIGN_ERROR_CODE, backing.as_ptr(), 16) }; + drop(backing); assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); - assert_eq!(unsafe { err.message_str() }, Some("consumer failure")); + assert_eq!(err.message_len, 16); + assert_eq!( + err.message_ptr as usize % abi::RUN_ALIGN, + 0, + "an 8-aligned run" + ); + assert_eq!(unsafe { err.message_str() }, Some("consumer\0failure")); unsafe { export_runtime_error_clear(&mut err) }; assert_eq!(err.code, 0); - assert!(err.message.is_null()); + assert!(err.message_ptr.is_null()); + assert_eq!(err.message_len, 0); unsafe { - export_runtime_error_set(ptr::null_mut(), 1, ptr::null()); + export_runtime_error_set(ptr::null_mut(), 1, ptr::null(), 0); export_runtime_error_clear(ptr::null_mut()); export_runtime_error_free(ptr::null_mut()); } @@ -53,7 +60,7 @@ fn error_set_payload_copies_the_run() { let mut err = FfiError::default(); let fields = abi::encode_value(&"key".to_string()); unsafe { - export_runtime_error_set(&mut err, 1, ptr::null()); + export_runtime_error_set(&mut err, 1, ptr::null(), 0); export_runtime_error_set_payload(&mut err, fields.as_ptr(), fields.len()); } drop(fields); @@ -67,9 +74,10 @@ fn error_set_payload_copies_the_run() { #[test] fn free_bytes_releases_returned_and_allocated_runs() { - let (p, len) = abi::bytes_into_raw(vec![1, 2, 3]); + let (p, len) = abi::bytes_into_raw(&[1, 2, 3]); let staged = export_runtime_alloc(16); assert!(!staged.is_null()); + assert_eq!(staged as usize % abi::RUN_ALIGN, 0, "an 8-aligned run"); assert!(export_runtime_alloc(0).is_null()); unsafe { export_runtime_free_bytes(p.cast_mut(), len); diff --git a/crates/weaveffi/tests/leak.rs b/crates/weaveffi/tests/leak.rs index cf666719..0ff90147 100644 --- a/crates/weaveffi/tests/leak.rs +++ b/crates/weaveffi/tests/leak.rs @@ -76,6 +76,18 @@ pub mod pool { slot.n } + /// Squares as a typed array (a returned run). + #[weaveffi::export] + pub fn squares(xs: &[u32]) -> Vec { + xs.iter().map(|x| u64::from(*x) * u64::from(*x)).collect() + } + + /// Fail with an untyped error (its message is a run). + #[weaveffi::export] + pub fn fail(why: String) -> Result<(), String> { + Err(why) + } + /// Count up lazily. #[weaveffi::export] pub fn count(n: i32) -> weaveffi::Iter { @@ -149,10 +161,20 @@ fn every_counter_returns_to_zero() { let back = pool::leak_pool_release(bytes.as_ptr(), bytes.len(), &mut err); assert_eq!(live()[0], 3, "the token was adopted, the return handed out"); - // Allocations: a returned string. + // Allocations: a returned string, a typed array, and an error's + // message, each a run until it's released. let text = pool::leak_pool_describe(back, &mut len, &mut err); assert_eq!(live()[4], 1); leak_free_bytes(text.cast_mut(), len); + let xs = [2u32, 3]; + let sq = pool::leak_pool_squares(xs.as_ptr(), 2, &mut len, &mut err); + assert_eq!((live()[4], len), (1, 2)); + leak_free_bytes(sq.cast(), len * 8); + let why = "no"; + pool::leak_pool_fail(why.as_ptr(), why.len(), &mut err); + assert_eq!((err.code, live()[4]), (abi::GENERIC_ERROR_CODE, 1)); + abi::error_clear(&mut err); + assert_eq!(live()[4], 0); pool::leak_pool_Slot_destroy(back); pool::leak_pool_Slot_destroy(twin); diff --git a/crates/weaveffi/tests/runtime.rs b/crates/weaveffi/tests/runtime.rs index 171e5f87..1c9bf0bf 100644 --- a/crates/weaveffi/tests/runtime.rs +++ b/crates/weaveffi/tests/runtime.rs @@ -28,17 +28,10 @@ pub mod demo { #[derive(Debug)] pub enum DemoError { /// division by zero + #[weaveffi(message = "cannot divide by zero")] DivisionByZero = 100, } - impl std::fmt::Display for DemoError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::DivisionByZero => f.write_str("cannot divide by zero"), - } - } - } - /// A C-style enum that crosses the ABI as its `i32` discriminant. #[weaveffi::enumeration] #[repr(i32)] @@ -112,7 +105,7 @@ pub mod demo { present.then(|| "present".to_string()) } - /// Sum a list of scalars. + /// Sum a list of scalars (a typed array). #[weaveffi::export] pub fn sum(xs: Vec) -> i32 { xs.iter().sum() @@ -208,19 +201,29 @@ pub mod rich { #[repr(i32)] pub enum LookupError { /// no such key + #[weaveffi(message = "missing {key}")] Missing { /// The key that wasn't found. key: String, } = 1, /// the source is busy + #[weaveffi(message = "busy")] Busy = 2, - } - - impl std::fmt::Display for LookupError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Missing { key } => write!(f, "missing {key}"), - Self::Busy => f.write_str("busy"), + /// the source failed some other way + #[weaveffi(message = "foreign {code}: {message}")] + Foreign { + /// The failure's runtime code. + code: i32, + /// The consumer's message. + message: String, + } = 3, + } + + impl From for LookupError { + fn from(e: ForeignError) -> Self { + Self::Foreign { + code: e.code, + message: e.message, } } } @@ -278,8 +281,7 @@ pub mod rich { /// An optional object. fn maybe_token(&self) -> Result>, ForeignError>; /// Look up a key, failing with a `LookupError`. - #[weaveffi::throws] - fn lookup(&self, key: &str) -> Result; + fn lookup(&self, key: &str) -> Result; } /// Call every value-returning method and describe the results. @@ -303,11 +305,7 @@ pub mod rich { pub fn lookup_via(source: Arc, key: Key) -> String { match source.lookup(&key) { Ok(v) => format!("ok {v}"), - Err(e) => match e.domain::() { - Some(LookupError::Missing { key }) => format!("missing {key}"), - Some(LookupError::Busy) => "busy".to_string(), - None => format!("foreign {}: {}", e.code, e.message), - }, + Err(e) => e.to_string(), } } @@ -463,12 +461,6 @@ pub mod bus { Closed = 1, } - impl std::fmt::Display for BusError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("the bus is closed") - } - } - /// A message priority (a C-style enum crossing a callback boundary). #[weaveffi::enumeration] #[repr(i32)] @@ -629,12 +621,6 @@ pub mod tasks { Overflow = 1, } - impl std::fmt::Display for TaskError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("arithmetic overflow") - } - } - /// The by-value result an async task completes with. #[weaveffi::record] #[derive(Clone)] @@ -722,7 +708,7 @@ fn scalar_call_sets_ok() { let r = unsafe { demo::runtime_demo_add(2, 40, &mut err) }; assert_eq!(r, 42); assert_eq!(err.code, 0); - assert!(err.message.is_null()); + assert!(err.message_ptr.is_null()); } #[test] @@ -745,7 +731,7 @@ fn fallible_ok_and_err_paths_use_display_for_the_message() { // The next successful call resets the slot. assert_eq!(unsafe { demo::runtime_demo_add(1, 1, &mut err) }, 2); assert_eq!(err.code, 0); - assert!(err.message.is_null()); + assert!(err.message_ptr.is_null()); } #[test] @@ -827,11 +813,21 @@ fn optional_string_return_is_buffered() { } #[test] -fn scalar_list_param_is_buffered() { +fn scalar_list_param_is_a_typed_array() { let mut err = ok_err(); - let xs = abi::encode_value(&vec![3i32, 4, 5]); + let xs = [3i32, 4, 5]; let total = unsafe { demo::runtime_demo_sum(xs.as_ptr(), xs.len(), &mut err) }; assert_eq!(total, 12); + assert_eq!( + unsafe { demo::runtime_demo_sum(std::ptr::null(), 0, &mut err) }, + 0 + ); + assert_eq!(err.code, 0); + assert_eq!( + unsafe { demo::runtime_demo_sum(std::ptr::null(), 2, &mut err) }, + 0 + ); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); } #[test] @@ -850,8 +846,10 @@ fn malformed_buffer_param_reports_error() { // A truncated encoding (count with no elements) must be rejected through // `out_err`, never decoded partially. let bad = [9u8, 0, 0, 0]; - let total = unsafe { demo::runtime_demo_sum(bad.as_ptr(), bad.len(), &mut err) }; - assert_eq!(total, 0, "error path returns the zero sentinel"); + let mut out_len = 0usize; + let joined = + unsafe { demo::runtime_demo_join(bad.as_ptr(), bad.len(), &mut out_len, &mut err) }; + assert!(joined.is_null(), "error path returns the zero sentinel"); assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); } @@ -986,14 +984,15 @@ fn contract( fn contract_tables_hold_one_sorted_entry_per_declaration() { let table = contract(demo::runtime_demo_contract); assert!(table.windows(2).all(|w| w[0].id < w[1].id), "sorted by id"); - // demo: the error domain, Color, Point, and thirteen functions. - assert_eq!(table.len(), 16); + // demo: the error domain and its code, Color, Point, and thirteen + // functions. + assert_eq!(table.len(), 17); let add = table.iter().find(|e| e.id == fnv("demo.add")).unwrap(); - assert_eq!(add.hash, fnv("function add(a: i32, b: i32) -> i32")); + assert_eq!(add.hash, fnv("function add(i32, i32) -> i32")); let point = table.iter().find(|e| e.id == fnv("demo.Point")).unwrap(); assert_eq!( point.hash, - fnv("record Point {x: i32, label: string, nickname: string?, color: Color}") + fnv("record Point {i32, string, string?, Color}") ); let div = table .iter() @@ -1001,8 +1000,13 @@ fn contract_tables_hold_one_sorted_entry_per_declaration() { .unwrap(); assert_eq!( div.hash, - fnv("function checked_div(a: i32, b: i32) -> i32 throws") + fnv("function checked_div(i32, i32) -> i32 throws DemoError") ); + let code = table + .iter() + .find(|e| e.id == fnv("demo.DemoError.DivisionByZero")) + .unwrap(); + assert_eq!(code.hash, fnv("code DivisionByZero = 100")); // Nested modules and interface members are in their root's table. let outer = contract(outer::runtime_outer_contract); @@ -1036,9 +1040,17 @@ fn contract_tables_follow_cfg() { .unwrap(); assert_eq!( lookup.hash, - fnv("function lookup_via(source: Source, key: string) -> string"), + fnv("function lookup_via(Source, string) -> string"), "the alias resolved to its target" ); + let method = table + .iter() + .find(|e| e.id == fnv("rich.Source.lookup")) + .unwrap(); + assert_eq!( + method.hash, + fnv("callback_method #5 lookup(string) -> i64 throws LookupError") + ); } #[test] @@ -1267,9 +1279,9 @@ mod consumer_subscriber { .unwrap(); *state.last_topic.lock().unwrap() = env.topic.clone(); if weight == state.fail_at { - let msg = std::ffi::CString::new(format!("subscriber rejected {text}")).unwrap(); + let msg = format!("subscriber rejected {text}"); // Consumers report through the exported `{prefix}_error_set`. - unsafe { super::runtime_error_set(out_err, abi::FOREIGN_ERROR_CODE, msg.as_ptr()) }; + unsafe { super::runtime_error_set(out_err, -1, msg.as_ptr(), msg.len()) }; return 0; } state.total.fetch_add(weight as i64, Ordering::Relaxed) + weight as i64 @@ -1454,9 +1466,10 @@ fn callback_interface_retained_and_foreign_error() { assert_eq!(publish(5, &mut err), 16); // The first subscriber fails on weight 7: the producer call is aborted and - // the consumer's own message comes back with FOREIGN_ERROR_CODE. + // the consumer's own message comes back. `publish` throws `any` + // (`ForeignError` isn't a domain), so the code is the generic one. assert_eq!(publish(7, &mut err), 0); - assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); + assert_eq!(err.code, abi::GENERIC_ERROR_CODE); assert_eq!(message(&err), "subscriber rejected hi"); // The bus is still usable afterwards. @@ -1523,7 +1536,7 @@ fn callback_interface_from_async_method() { // A foreign failure inside the future is delivered through the callback. unsafe { bus::runtime_bus_Bus_publish_later(b, text.as_ptr(), text.len(), 4, on_i64, ctx) }; let (code, msg, result) = rx.recv_timeout(WAIT).unwrap(); - assert_eq!(code, abi::FOREIGN_ERROR_CODE); + assert_eq!(code, abi::GENERIC_ERROR_CODE); assert_eq!(msg, "subscriber rejected async"); assert_eq!(result, 0); @@ -1843,64 +1856,61 @@ fn objects_inside_value_buffers_carry_a_reference() { } } -/// A producer module whose fallible function reports through a hand-written -/// [`weaveffi::ErrorReport`] type rather than the domain enum itself. +/// A producer module with a declared domain whose message interpolates a +/// field, and a function whose error type isn't a domain (`throws any`). #[weaveffi::module] pub mod vault { - use weaveffi::ErrorReport; - /// The vault's declared error domain: the codes consumers can match on. #[weaveffi::error] #[derive(Debug)] + #[repr(i32)] pub enum VaultError { /// entry not found NotFound = 2001, /// vault sealed - Sealed = 2002, + #[weaveffi(message = "vault sealed: {reason}")] + Sealed { + /// Why the vault is sealed. + reason: String, + } = 2002, } - impl std::fmt::Display for VaultError { + /// The producer's internal failure type: not a domain, so a function + /// returning it throws `any` and reports its `Display` with code -1. + #[derive(Debug)] + pub struct VaultFailure(String); + + impl std::fmt::Display for VaultFailure { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::NotFound => "entry not found", - Self::Sealed => "vault sealed", - }) + write!(f, "vault failure: {}", self.0) } } - /// The producer's internal failure type: it carries data the declared - /// domain doesn't, so it maps itself onto the domain's codes with a - /// hand-written `ErrorReport` and dynamic messages. - pub enum VaultFailure { - /// No entry exists for the key. - NotFound, - /// The vault is sealed for the given reason. - Sealed(String), + /// Fetch a doubled value, failing with a domain code for invalid keys. + #[weaveffi::export] + pub fn fetch(key: i64) -> Result { + match key { + 0 => Err(VaultError::NotFound), + n if n < 0 => Err(VaultError::Sealed { + reason: "negative key".to_string(), + }), + n => Ok(n * 2), + } } - impl ErrorReport for VaultFailure { - fn code(&self) -> i32 { - match self { - VaultFailure::NotFound => 2001, - VaultFailure::Sealed(_) => 2002, - } - } - fn message(&self) -> String { - match self { - VaultFailure::NotFound => "entry not found".to_string(), - VaultFailure::Sealed(reason) => format!("vault sealed: {reason}"), - } + /// Fetch, failing with an untyped error. + #[weaveffi::export] + pub fn fetch_any(key: i64) -> Result { + if key == 0 { + return Err(VaultFailure("no key".to_string())); } + Ok(key) } - /// Fetch a doubled value, failing with a domain code for invalid keys. + /// Parse an integer: a std error type works as `throws any` too. #[weaveffi::export] - pub fn fetch(key: i64) -> Result { - match key { - 0 => Err(VaultFailure::NotFound), - n if n < 0 => Err(VaultFailure::Sealed("negative key".to_string())), - n => Ok(n * 2), - } + pub fn parse(text: &str) -> Result { + text.parse() } } @@ -1921,6 +1931,23 @@ fn fallible_with_domain_error_codes() { assert_eq!(message(&err), "vault sealed: negative key"); } +#[test] +fn untyped_errors_report_the_generic_code_and_display() { + let mut err = ok_err(); + assert_eq!(unsafe { vault::runtime_vault_fetch_any(5, &mut err) }, 5); + assert_eq!(unsafe { vault::runtime_vault_fetch_any(0, &mut err) }, 0); + assert_eq!(err.code, abi::GENERIC_ERROR_CODE); + assert_eq!(message(&err), "vault failure: no key"); + assert!(err.payload_ptr.is_null()); + let text = "x1"; + assert_eq!( + unsafe { vault::runtime_vault_parse(text.as_ptr(), text.len(), &mut err) }, + 0 + ); + assert_eq!(err.code, abi::GENERIC_ERROR_CODE); + assert_eq!(message(&err), "invalid digit found in string"); +} + /// A producer module that exports a `#[deprecated]` function. The generated /// thunk must still *call* the deprecated function, so it carries an /// `#[allow(deprecated)]` of its own; this module compiling under the @@ -1953,15 +1980,10 @@ pub mod counters { #[derive(Debug)] pub enum CounterError { /// start value out of range + #[weaveffi(message = "step must be positive")] OutOfRange = 1, } - impl std::fmt::Display for CounterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("step must be positive") - } - } - /// A monotonic counter, exported as an interface. #[weaveffi::interface] pub struct Counter { @@ -2303,6 +2325,7 @@ pub mod quota { #[repr(i32)] pub enum QuotaError { /// quota exceeded + #[weaveffi(message = "used {used} of {limit}")] Exceeded { /// The configured limit. limit: i64, @@ -2313,15 +2336,6 @@ pub mod quota { Unavailable = 3002, } - impl std::fmt::Display for QuotaError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Exceeded { limit, used } => write!(f, "used {used} of {limit}"), - Self::Unavailable => f.write_str("quota service unavailable"), - } - } - } - /// Consume `amount` units against a limit of 100. #[weaveffi::export] pub fn consume(amount: i64) -> Result { @@ -2434,8 +2448,8 @@ mod consumer_source { ) -> i64 { let key = unsafe { abi::lift_string(key_ptr, key_len) }.unwrap(); let fail = |code: i32, payload: Option<&str>| unsafe { - let msg = std::ffi::CString::new(format!("failed {key}")).unwrap(); - super::runtime_error_set(out_err, code, msg.as_ptr()); + let msg = format!("failed {key}"); + super::runtime_error_set(out_err, code, msg.as_ptr(), msg.len()); if let Some(p) = payload { let fields = abi::encode_value(&p.to_string()); super::runtime_error_set_payload(out_err, fields.as_ptr(), fields.len()); @@ -2548,6 +2562,7 @@ fn optional_callback_parameters_accept_null() { assert_eq!(unsafe { rich::runtime_rich_always(&mut err) }, 1); } +#[cfg(feature = "tokio")] #[test] fn many_concurrent_launches_share_a_few_executor_threads() { extern "C" fn on_tag(ctx: *mut c_void, err: *mut FfiError, ptr: *const u8, len: usize) { @@ -2576,3 +2591,28 @@ fn many_concurrent_launches_share_a_few_executor_threads() { threads.len() ); } + +/// Without Tokio (`default-features = false`), each call runs on a thread of +/// its own. +#[cfg(not(feature = "tokio"))] +#[test] +fn without_tokio_each_launch_gets_its_own_thread() { + extern "C" fn on_tag(ctx: *mut c_void, err: *mut FfiError, ptr: *const u8, len: usize) { + on_string(ctx, err, ptr, len); + } + const CALLS: usize = 16; + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); + for _ in 0..CALLS { + unsafe { tasks::runtime_tasks_thread_tag(on_tag, ctx) }; + } + let mut threads = std::collections::BTreeSet::new(); + for _ in 0..CALLS { + let (code, _, tag) = rx.recv_timeout(WAIT).unwrap(); + assert_eq!(code, 0); + assert!(tag.ends_with(" weaveffi-async"), "{tag}"); + threads.insert(tag); + } + drop_ctx::(ctx); + assert_eq!(threads.len(), CALLS, "one thread per call"); +} diff --git a/crates/weaveffi/tests/spawn_failure.rs b/crates/weaveffi/tests/spawn_failure.rs index 58594edf..2efc4eb3 100644 --- a/crates/weaveffi/tests/spawn_failure.rs +++ b/crates/weaveffi/tests/spawn_failure.rs @@ -19,6 +19,8 @@ pub mod work { } } +weaveffi::export_runtime!(); + extern "C" fn done(ctx: *mut c_void, err: *mut FfiError, result: i32) { // Clone the sender before sending: the test may free the context as soon // as the value arrives, which can be before `send` returns. diff --git a/crates/weaveffi/tests/tokio.rs b/crates/weaveffi/tests/tokio.rs index 75a8fe87..068aa327 100644 --- a/crates/weaveffi/tests/tokio.rs +++ b/crates/weaveffi/tests/tokio.rs @@ -22,6 +22,8 @@ pub mod timers { } } +weaveffi::export_runtime!(); + extern "C" fn done(ctx: *mut c_void, err: *mut FfiError, ptr: *const u8, len: usize) { // Clone the sender before sending: the test may free the context as soon // as the value arrives, which can be before `send` returns. diff --git a/docs/src/api/README.md b/docs/src/api/README.md index f30d2a7c..93b6e228 100644 --- a/docs/src/api/README.md +++ b/docs/src/api/README.md @@ -6,7 +6,7 @@ WeaveFFI is published as four crates that share one version: |-------|-------------| | [`weaveffi`](https://docs.rs/weaveffi) | You're writing a Rust producer. It's the only dependency a producer needs, and its `abi` module is the runtime. | | [`weaveffi-macros`](https://docs.rs/weaveffi-macros) | Never directly; `weaveffi` re-exports its macros. | -| [`weaveffi-model`](https://docs.rs/weaveffi-model) | You're building a tool on the IR: parsing, validating, or extracting an API, reading library metadata, or reading the validated `Model`. Its default `idl` feature adds the YAML, JSON, and TOML formats, the JSON Schema, and rich diagnostics. | +| [`weaveffi-model`](https://docs.rs/weaveffi-model) | You're building a tool on the IR: parsing, validating, or extracting an API, reading library metadata, or reading the validated `Model`. Its default `idl` feature adds the YAML and JSON formats, the JSON Schema, and rich diagnostics. | | [`weaveffi-cli`](https://docs.rs/weaveffi-cli) | You want the `weaveffi` command (`cargo install weaveffi-cli`), or you're driving or extending the generators from Rust (`project::Project` generates a project, from a `build.rs` too). | - [Rust API Map](rust.md) lists the items a producer uses from `weaveffi` diff --git a/docs/src/api/doc-style.md b/docs/src/api/doc-style.md index 4bde63cc..05dcd62f 100644 --- a/docs/src/api/doc-style.md +++ b/docs/src/api/doc-style.md @@ -128,8 +128,10 @@ pub struct FfiError { /// Status code. `0` means success; a positive value is a domain error /// code and a negative value is one of the reserved runtime codes. pub code: i32, - /// Owned, NUL-terminated UTF-8 message, or null when `code` is `0`. - pub message: *const c_char, + /// Owned UTF-8 message (not NUL-terminated), or null when it's empty. + pub message_ptr: *const u8, + /// Byte length of `message_ptr`; `0` when null. + pub message_len: usize, /// Owned value buffer holding the error code's payload fields, or null /// when the code declares no fields. pub payload_ptr: *const u8, @@ -190,7 +192,7 @@ is the Rust analog of the docs site's autorefs: /// Renders from the shared [`Model`], never re-deriving lowering. /// /// See [`Api`](weaveffi_model::ir::Api) for the input model and -/// [`LanguageBackend`](crate::backend::LanguageBackend) for the trait every +/// [`Target`](crate::targets::Target) for the trait every /// generator implements. ``` diff --git a/docs/src/api/rust.md b/docs/src/api/rust.md index c2f775fe..42a8259c 100644 --- a/docs/src/api/rust.md +++ b/docs/src/api/rust.md @@ -6,7 +6,7 @@ contracts; this page says which items you'll reach for. ```toml [dependencies] -weaveffi = "0.24" +weaveffi = "0.25" ``` ## The `weaveffi` facade @@ -14,25 +14,26 @@ weaveffi = "0.24" | Item | Purpose | |------|---------| | `#[weaveffi::module]` | Marks an exported module, validates its tree, and emits its thunks, contract table, and the library metadata the CLI reads the API from. | -| `#[weaveffi::export]` | Exports a function (`async fn` is async; a `Result` return throws). | +| `#[weaveffi::export]` | Exports a function (`async fn` is async; a `Result` return throws its error type when that's a declared domain, else `any`). | | `#[weaveffi::record]` | A by-value struct. | | `#[weaveffi::enumeration]` | A `#[repr(i32)]` C-style enum or a rich enum. | | `#[weaveffi::interface]` | A reference-counted object type, with its inherent `impl`. | -| `#[weaveffi::callback_interface]` | A trait the consumer implements, each method returning `Result`; accepted as `Arc` or `Option>`. | -| `#[weaveffi::throws]` | A callback method whose consumer may fail with the error domain in scope. | -| `#[weaveffi::error]` | The module's error domain; requires `Display`. | +| `#[weaveffi::skip]` | Leaves a `pub fn` of an interface's `impl` unexported. | +| `#[weaveffi::callback_interface]` | A trait the consumer implements, each method returning `Result` with `E: From` (a domain `E` receives the consumer's typed errors); accepted as `Arc` or `Option>`. | +| `#[weaveffi::error]` | An error domain (a module may declare several); generates `Display` and `std::error::Error` unless written `#[weaveffi::error(no_display)]`. | +| `#[weaveffi::custom]` | A type alias that crosses as another type, with `lift` and `lower` functions. | | `#[weaveffi::cancellable]` | An async function taking a `CancelToken`. | | `weaveffi::export_runtime!()` | Exports the runtime symbols under the crate's prefix; call once. | | `Iter` | The return type for `iter`; build with `Iter::new(iterator)`. | | `CancelToken` | A cancellable function's last parameter; `is_cancelled()` for cooperative cleanup. | -| `ForeignError` | A consumer callback's failure (`code`, `message`, `payload`), the `Err` of every callback method; `domain::()` decodes a declared domain error. Return it from an exported function to propagate a failure with `?`. | -| `ErrorDomain` | Implemented by every `#[weaveffi::error]` enum, so `ForeignError::domain` can decode it. | -| `ErrorReport` | Maps an error type to `(code, message, payload)`; implemented for `String`, `&str`, boxed errors, `ForeignError`, and every `#[weaveffi::error]` enum. | -| `set_spawner`, `Spawner`, `BoxFuture` | Install the executor async functions run on, overriding the default worker pool (or Tokio). | +| `ForeignError` | A consumer callback's failure (`code`, `message`, `payload`), which every callback method's error type converts from; `domain::()` decodes a declared domain error by hand. As an exported function's error type it's `throws: any`. | +| `ErrorDomain` | Implemented by every `#[weaveffi::error]` enum (with `Display` as a supertrait): each variant's code and payload. | +| `set_spawner`, `Spawner`, `BoxFuture` | Install the executor async functions run on, overriding the default (Tokio, or a thread per call without the `tokio` feature). | | `abi` | The C ABI runtime (below). | The `leak-check` feature enables the runtime's leak counters, and the -`tokio` feature runs async functions on Tokio by default. The +default `tokio` feature runs async functions on Tokio +(`default-features = false` runs each on a thread of its own). The [producer macro guide](../guides/producer-macro.md) covers every attribute. ## The `weaveffi::abi` runtime @@ -43,17 +44,18 @@ but the `leak` constants is re-exported at the root of `weaveffi::abi`. | Module | Items | |--------|-------| -| root | `ABI_VERSION` (`4`). | -| `error` | `FfiError`, the `#[repr(C)]` error struct (the C `{prefix}_error`); the codes `GENERIC_ERROR_CODE` through `CANCELLED_ERROR_CODE` (`-1` to `-5`); `ErrorReport`; `ErrorDomain`; `panic_message`. **(macro)** `error_set`, `error_set_c`, `error_set_payload_c`, `error_store`, `error_clear`, `error_free`, `boxed_error`. | -| `convert` | `(ptr, len)` conversions: `lift_str`, `lift_string`, `lift_byte_slice`, `lift_bytes` read borrowed parameters; `lower_string`, `lower_bytes`, `bytes_into_raw` hand out allocations; `alloc` allocates a run for the producer to adopt; `adopt_bytes` adopts one; `free_bytes` releases either kind. Useful in tests that call thunks directly. | -| `marshal` | `CEnum`, the trait every C-style enum implements (`from_i32`, `to_i32`). **(macro)** the per-family `lift_*_param` and `lower_*_ret` functions, `lift_self`, `lift_self_arc`, `lift_enum`, `read_enum`, `write_enum`, the `*_slots` and `*_run` helpers, `call_sync`, and `Sentinel`. | +| root | `ABI_VERSION` (`5`). | +| `error` | `FfiError`, the `#[repr(C)]` error struct (the C `{prefix}_error`, with `message_ptr` and `message_len`); the codes `GENERIC_ERROR_CODE` through `CANCELLED_ERROR_CODE` (`-1` to `-5`); `ErrorDomain`; `panic_message`. **(macro)** `error_set`, `error_set_c`, `error_set_payload_c`, `error_store`, `error_clear`, `error_free`, `boxed_error`. | +| `convert` | `(ptr, len)` conversions: `lift_str`, `lift_string`, `lift_byte_slice`, `lift_bytes` read borrowed parameters; `lower_string`, `lower_bytes`, `bytes_into_raw`, `slice_into_raw` hand out 8-aligned runs (`RUN_ALIGN`); `alloc` allocates a run for the producer to adopt; `adopt_bytes` adopts one; `free_bytes` releases either kind. Useful in tests that call thunks directly. | +| `scalar` | `Scalar`, the trait for values that cross as one C scalar (every fixed-width integer, float, and `bool`, `usize` and `isize` as `u64` and `i64`, and every C-style enum as an `i32`); `Text` (`String`, `str`, `char`); `Custom`, which a `#[weaveffi::custom]` declaration implements. | +| `marshal` | **(macro)** the per-family `lift_*_param` and `lower_*_ret` functions (including the OptDirect `lift_opt_param`/`lower_opt_ret` and Slice `lift_slice_param`/`lower_slice_ret` pairs), `lift_custom_param`, `lift_self`, `lift_self_arc`, `read_enum`, `write_enum`, the `*_slots` and `*_run` helpers, `call_sync`, and `Sentinel`. | | `object` | **(macro)** `lower_object`, `lower_object_opt`, `object_ref`, `object_arc`, `object_clone`, `object_destroy`, and the buffer token pair `object_to_token` / `object_from_token`. | -| `callback` | `ForeignError`; `VtableHeader`, the fixed start of every vtable. **(macro)** `ForeignCallback`, `Vtable`, `VtableTooSmall`, `CallbackInterface`, `lift_callback`, `lift_callback_opt`, `callback_status`, `callback_status_in`, and the `callback_ret_*` adopters. | +| `callback` | `ForeignError`; `VtableHeader`, the fixed start of every vtable; `VTABLE_THREAD_AFFINE` and `OFF_THREAD_MESSAGE`. **(macro)** `ForeignCallback` (with its `check_thread`), `Vtable`, `VtableTooSmall`, `CallbackInterface`, `lift_callback`, `lift_callback_opt`, `callback_status`, `callback_status_in`, `convert_foreign`, and the `callback_ret_*` adopters. | | `cancel` | `CancelToken`. **(macro)** `Cancellable`, `FfiCancelToken`, `AtomicWaker`, and the `cancel_token_*` bodies. | | `iter` | `Iter`. **(macro)** `IterHandle`, `iter_into_raw`, `iter_next`, `iter_destroy`. | | `buffer` | The value-buffer codec: `BufferValue`, `BufferWriter`, `BufferReader`, `BufferDecodeError`, `encode_value`, `decode_value`, the `ByValue` marker that records and rich enums implement, and `FixedWidth`, the token behind single-copy lists of numbers. `decode_value` and `BufferValue::read_value` are `unsafe`, because decoding an object token adopts a reference; a `BufferReader::token_free` reader refuses tokens and is safe for any input. | | `contract` | `ContractEntry`. **(macro)** `contract_table`, `contract_len`, `contract_compact`. | -| `spawn` | `set_spawner`, `Spawner`, `SpawnerAlreadySet`, `BoxFuture`, `SpawnError`, `default_pool_size`, and `block_on` (a blocking executor for tests). **(macro)** `launch_async`, `run_async`, `spawn`, `CatchUnwind`. | +| `spawn` | `set_spawner`, `Spawner`, `SpawnerAlreadySet`, `BoxFuture`, `SpawnError`, and `block_on` (a blocking executor, also the one the thread-per-call default uses). **(macro)** `launch_async`, `run_async`, `spawn`, `CatchUnwind`. | | `leak` | `debug_live`, the `ENABLED` flag, and the kind constants `COUNTING` (`-1`), `OBJECTS`, `CALLBACKS`, `ITERATORS`, `TOKENS`, and `ALLOCATIONS` (`0` to `4`); counters are live only with `leak-check`. | The [C ABI contract](../reference/abi.md) is the specification these items diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 9a350bc8..f2bf5538 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -7,61 +7,67 @@ everything here implements. ## The pipeline -Every generating command (`generate`, `diff`, `dev`, `package`) runs the -same stages: +Every generating command (`generate`, including `--check`, `--diff`, and +`--dry-run`, `dev`, and `package`) runs the same stages: ```text -Rust producer crate IDL (.yml / .json / .toml) - cargo build, then the library's weaveffi_model::parse - metadata frames (weaveffi_cli::library, | - weaveffi_model::meta::assemble) | - \ / - +--------> Api (the IR) <------+ - | - v - validate weaveffi_model::validate::validate(&api, &identity): every - rule, including the C symbol table under the library's - real Identity (from weaveffi.toml / cargo metadata); reports - all violations at once and builds the Model exactly once - | - v - Model the identity, the schema version, per-module bindings with - every C symbol and lowered ABI signature, and the type - index; the plan (ArgPass, RetPass, ErrorStrategy, and the - iterator, async, and callback protocols) is computed from it - | - v - render Target::render(&Model): each generator returns its files - in memory (pure; no I/O) and never sees the Api - | - v - orchestrate Orchestrator: writes changed files, removes stale ones, - keeps generation records +Rust producer crate IDL (.yml / .yaml / .json) + cargo rustc --crate-type cdylib, then weaveffi_model::parse + the library's metadata frames | + (weaveffi_cli::library, | + weaveffi_model::meta::assemble) | + \ / + +------------> Api (the IR) <--------+ + | + v + validate weaveffi_model::validate::validate(&api, &identity, source): + every rule, including the C symbol and slot tables under + the library's real Identity (from weaveffi.toml / cargo + metadata); reports all violations at once and builds the + Model exactly once + | + v + Model the identity, the schema version, the type index, and one + binding per declaration with every C symbol, its lowered + AbiFn, and its passing contracts (ArgPass, RetPass, + ResultPass, ItemPass, CallbackRetPass, ErrorStrategy) + already resolved + | + v + render Target::render(&Model): each target returns its files in + memory (pure; no I/O), through the shared emitters in + codegen/, and never sees the Api + | + v + orchestrate Orchestrator::plan builds a Changeset against the output + directory; apply writes changed files, removes stale ones, + and keeps generation records ``` -`validate` stops once the model is built; `diff` renders and compares -without writing; `package` renders installable artifacts (wheels, npm +`validate` stops once the model is built; `generate --check` and `--diff` +plan without applying; `package` renders installable artifacts (wheels, npm tarballs, gems, ...) around the per-platform libraries `build` compiled. The **producer macro** runs the front half of the same pipeline inside the -producer's build. `#[weaveffi::module]` lowers its module tree with -`weaveffi_model::rust::extract_module`, validates it into a `Model` with the -same validator the CLI uses -(`validate_scoped`, which accepts names declared in another tree as records -or rich enums and leaves the macro to assert that at compile time), with an -identity named after `CARGO_CRATE_NAME`, and reports each validation error -on the offending item through the extractor's `SourceMap`. It then computes -the module's contract table with `weaveffi_model::contract::entries` and -emits `extern "C"` thunks instead of rendering a binding, wrapping each -item's output in that item's `#[cfg]`. Finally it embeds the tree's IR in -the library: one `weaveffi_model::meta::Frame` per declaration, in an -exported `{PREFIX}_META_{HASH16}` static (the `weaveffi_meta` custom section -on `wasm32`) carrying the same `#[cfg]`. The CLI reads those frames back out -of the built library (see [Library Mode](guides/extract.md)), so it -generates from exactly the declarations the build compiled, with the same -validator, model, and contract function, and the symbols and contract -entries a producer exports are exactly those its generated bindings -expect. +producer's build. `#[weaveffi::module]` lowers its module tree with its own +extractor (`crates/weaveffi-macros/src/extract.rs`, which reads `syn` +tokens and keeps a `SourceMap` of spans), validates it into a `Model` with +the same validator the CLI uses (`validate_scoped`, whose `Options::foreign` +lists the names declared outside the tree, `Api::undeclared_type_names`, +which resolve as records or rich enums that the macro then asserts at compile +time), with an identity named after `CARGO_CRATE_NAME`, and reports each +validation error on the offending item through the `SourceMap`. It then +computes the module's contract table with `weaveffi_model::contract::entries` +and emits `extern "C"` thunks instead of rendering a binding, reading the +same stored passing contracts a generator reads and wrapping each item's +output in that item's `#[cfg]`. Finally it embeds the tree's IR in the +library: one `weaveffi_model::meta::Frame` per declaration, in an exported +`{PREFIX}_META_{HASH16}` static (the `weaveffi_meta` custom section on +`wasm32`) carrying the same `#[cfg]`. The CLI reads those frames back out of +the built library (see [Library Mode](guides/extract.md)), so it generates +from exactly the declarations the build compiled, with the same validator, +model, and contract function, and the symbols and contract entries a +producer exports are exactly those its generated bindings expect. ## Crates @@ -70,23 +76,23 @@ weaveffi-cli ─────────────────► weaveffi-mod ▲ weaveffi ──► weaveffi-macros ────────┘ (proc macro, build time only) -weaveffi-fuzz ──► weaveffi-model (unpublished) +weaveffi-fuzz ──► weaveffi, weaveffi-model (unpublished) ``` The runtime (`weaveffi::abi`) depends on no other workspace crate at run time, so a producer never links the generators, and the generators never link the runtime. The ABI revision is therefore declared twice, in -`weaveffi::abi::ABI_VERSION` and in the model (re-exported as -`weaveffi_cli::cabi::ABI_VERSION`), and +`weaveffi::abi::ABI_VERSION` and in the model +(`weaveffi_model::model::ABI_VERSION`), and `crates/weaveffi-cli/tests/abi_version.rs` keeps them equal. | Crate | Owns | |-------|------| -| `weaveffi-model` | Everything between a definition and code generation: the IR (`ir`), IDL parsing (`parse`, behind the default `idl` feature), Rust extraction for the macro (`rust`), library metadata frames (`meta`), validation and diagnostics (`validate`), resolved types and the type index (`ty`), package identity (`pkg`), the model with the symbol table (`model`), C ABI lowering (`abi`), the marshalling plan (`plan`), error naming (`errors`), and contract tables (`contract`). The macro uses it without the `idl` feature. | -| `weaveffi-cli` | The library (`src/lib.rs`): the backend framework and all eleven generators: `backend` (the `LanguageBackend` trait), `codegen` (the object-safe `Target`, `ConfiguredBackend`, the `Orchestrator`, and the `CodeWriter` toolkit), `cabi` (the shared C declaration renderer), `cache` (generation records), `lang` (keyword tables and escaping), `manifest` (JSON and XML escaping for package manifests), `cargo` (resolving a producer crate with `cargo metadata`), `build` (cross-compiling a producer per platform and prebuilding the Node.js and JNI glue), `package` (artifacts and the wheel, npm, gem, tarball, and zip writers) and `platform` (the platform matrix), `config` (`weaveffi.toml` and the target registry), `project` (locating a project, identity resolution, and loading its API), `library` (reading a built library's metadata), `utils` (generated-file banners), and `targets::{c, cpp, swift, kotlin, node, wasm, python, dotnet, dart, go, ruby}`. The `weaveffi` binary (`src/main.rs`): argument parsing and one module per subcommand under `commands/`. | -| `weaveffi-macros` | `#[weaveffi::module]`, the marker attributes, and `export_runtime!`. Emission is split by concern under `src/codegen/` (`sync`, `async_fns`, `iterators`, `records`, `enums`, `interfaces`, `callbacks`, `contract`, `meta`, `foreign`, `diagnostics`, `helpers`, `lift`). | -| `weaveffi` | The producer facade: re-exports the macros and the few runtime types a producer names. Its `abi` module is the runtime: the error struct and codes, `(ptr, len)` conversions, reference-counted objects, cancel tokens, callback vtables and foreign errors, iterators, the value-buffer codec, the async spawner (a worker pool by default, Tokio with the `tokio` feature) and `run_async`, contract tables, and leak counters (the `leak-check` feature). | -| `weaveffi-fuzz` | `cargo-fuzz` targets for the parsers, `parse_type_ref`, the validator, and the value-buffer decoder. | +| `weaveffi-model` | Everything between a definition and code generation: the IR (`ir`), IDL parsing for YAML and JSON (`parse`, behind the default `idl` feature), library metadata frames (`meta`), validation and diagnostics (`validate`), types split by position and their families (`ty`), package identity (`pkg`), the model with the symbol table (`model`), C slot types and lowering (`abi`), the passing contracts (`plan`), error-type naming (`errors`), and contract tables (`contract`). It doesn't depend on `syn`; the macro uses it without the `idl` feature. | +| `weaveffi-cli` | The library (`src/lib.rs`): the target framework and all eleven generators. Public: `targets` (the `Target` trait, its hook types, and the `REGISTRY`), `codegen` (the `Orchestrator`, its `Changeset`, `OutputFile`, and the `CodeWriter`), `config` (`weaveffi.toml`), `project` (locating a project, identity resolution, and loading its API), and `package` (the artifact types). Internal: the shared emitters (`codegen::{codecs, contract, errors, docs, common}`), `cabi` (the shared C declaration renderer), `record` (generation records), `lang` (keyword tables and escaping), `manifest` (JSON and XML escaping for package manifests), `cargo` (resolving a producer crate with `cargo metadata` and building its library), `build` (cross-compiling a producer per platform and prebuilding the Node.js and JNI glue), `platform` (the platform matrix and the glibc reader for manylinux tags), `library` (reading a built library's metadata), `utils` (generated-file banners), the target modules `targets::{c, cpp, swift, kotlin, node, wasm, python, dotnet, dart, go, ruby}` (plus `targets::js`, the JavaScript layer `node` and `wasm` share), and the hidden `commands` (one module per subcommand). The `weaveffi` binary (`src/main.rs`): argument parsing, dispatch, and error rendering. | +| `weaveffi-macros` | `#[weaveffi::module]`, the marker attributes, and `export_runtime!`. `src/extract.rs` lowers a module tree to the IR; emission is split by concern under `src/codegen/` (`sync`, `async_fns`, `iterators`, `records`, `enums`, `errors`, `custom`, `interfaces`, `callbacks`, `contract`, `meta`, `foreign`, `diagnostics`, `helpers`, `lift`). | +| `weaveffi` | The producer facade: re-exports the macros and the few runtime types a producer names. Its `abi` module is the runtime: the error struct and codes, run allocation (8-aligned) and `(ptr, len)` conversions, the `Scalar`, `Text`, and `Custom` lift and lower traits, the OptDirect and Slice helpers, reference-counted objects, cancel tokens, callback vtables (with the thread-affinity check) and foreign errors, iterators, the value-buffer codec, the async spawner (Tokio with the default `tokio` feature, else a thread per call) and `run_async`, contract tables, and leak counters (the `leak-check` feature). | +| `weaveffi-fuzz` | `cargo-fuzz` targets for the YAML and JSON parsers, `parse_type_ref`, the validator, and the value-buffer decoder. | The workspace denies `unsafe_code`; `weaveffi::abi` opts in, and the thunks the macro emits carry a scoped allowance, so a macro-based producer needs no @@ -94,18 +100,26 @@ the macro emits carry a scoped allowance, so a macro-based producer needs no ## Key invariants -- **Generators consume the model only.** Symbol names, ABI signatures, type - families, and wire shapes come from `Model`, `Ty::family()`, and - `Ty::wire()`; a generator never re-derives a symbol or reads the raw IR. - Names come from the `Identity` on the `Model`. -- **Global names.** Type names, free-function names, and error-code names +- **Generators consume the model only, and never re-derive it.** Symbol + names, slot lists, and passing contracts come from the bindings on the + `Model`; a target never matches on `Family`, never computes a slot name, + and never looks up a callable's error domain to decide how it fails + (`grep -rn "Family::" crates/weaveffi-cli/src/targets` is empty). Names + come from the `Identity` on the `Model`. +- **Global names.** Type names (records, enums, interfaces, callback + interfaces, and error domains), free-function names, and error-code names are unique across the API, so a `Ty` carries the bare name (`Store`) and equality works across modules. Where a generator needs the declaring - module (for a C type name or a namespace path), it asks the model's type - index (`Model::owner`, `Model::interface`, and so on); nothing splits a - dotted string. -- **One symbol table.** `Model::c_symbols` lists every C identifier with the - declaration that owns it, and validation rejects duplicates. + module (for a C type name or a namespace path), it asks the model + (`Model::owner`, `Model::interface`, `Model::record`, + `Model::error_domain`, and so on); nothing splits a dotted string, and + every name a validated model's bindings mention resolves. +- **One symbol table.** `Model::c_symbols` lists every C identifier with + its `SymbolOwner`, and validation rejects duplicates (`SymbolCollision`) + and two slots of one function with the same name (`SlotCollision`). +- **One naming rule for error types.** `weaveffi_model::errors::type_name` + turns a domain or code name into a target's type name (`KitchenErrors` + plus `Error` is `KitchenError`), and every target uses it. - **Fixed runtime code is real source.** A target's codec, error base types, and loader live as source files under `targets//runtime/`, included with `include_str!` and filled in with `{{PLACEHOLDER}}` @@ -119,107 +133,183 @@ the macro emits carry a scoped allowance, so a macro-based producer needs no `validate` indexes every type declaration, checks every rule, and then walks the document once to produce the `Model`: the identity, the schema version, -the type index (each name's kind and declaring module), and one -`ModuleBinding` per module (nested modules flattened, each with its segments, -its underscore `path`, its dotted path, its own error domain if it declares -one). A top-level module's contract table is computed from the model on -demand by `weaveffi_model::contract::entries`. A module without a domain -inherits the nearest ancestor's, found with `Model::error_domain`. Every -function, interface member, and callback method carries: - -- its resolved parameter and return `Ty`s, the idiomatic shape a generator - renders; -- its lowered `AbiFn`, the native shape: the C symbol and the ordered slots - from `weaveffi_model::abi` (`lower_param`, `lower_return`, the async and - callback signature helpers), whose C rendering is canonical; -- its `CallShape`: `Sync`, `Async` (launcher, completion type, result - slots, cancellability), or `Iterator` (launcher, iterator type, `_next`, - `_destroy`). - -Two classifications on `Ty` drive every generator's dispatch, so no target -re-derives them. `Ty::family()` is the slot family from the -[ABI contract](reference/abi.md#families-and-slots) (`Direct`, `String`, -`Bytes`, `Buffer`, or `Object` with nullability). `Ty::wire()` is the shape -inside a value buffer (`Prim`, `Object`, `Enum`, `User`, `Optional`, `List`, -`Map`), which codec emitters match on. - -`weaveffi_model::plan` states the calling contracts once, derived from the -family: how each argument is passed (`ParamBinding::arg_pass`), what a -wrapper does with a result (`RetPass::of`; an adopted object owes its -interface binding's `destroy_symbol`), whether a non-zero error code is a -typed domain error or a trap (`FnBinding::error_strategy`), and the -iterator, async, and callback protocols (`protocol()` on each binding). A -generator renders these in its own syntax; it doesn't decide them. - -## Backends and the orchestrator - -A generator implements `weaveffi_cli::backend::LanguageBackend`: +the type index (each name's kind and declaring module, plus the foreign +names a scoped validation accepted), and one `ModuleBinding` per module. +Modules are flattened into `Model::modules`, each with its segments, its +underscore `path`, its dotted path, and `parent` and `children` indices +(`Model::roots` and `Model::children` walk them), and each carries its own +error domains (`errors: Vec`). A top-level module's contract +table is computed from the model on demand by +`weaveffi_model::contract::entries`. + +Types are split by position, so nothing downstream handles a type where it +can't occur: + +- `Ty` is a value type, legal everywhere including inside buffers: a + primitive, a record, a C-style or rich enum, an interface, or an optional, + list, or map of value types. `Ty::family()` is its slot family + (`Direct`, `OptDirect`, `Slice(Prim)`, `String`, `Bytes`, `Buffer`, or + `Object` with nullability), and `Ty::wire()` its shape inside a value + buffer; both are total. +- `ParamTy` is a value or a callback interface (`ParamTy::Callback { name, + nullable }`), legal only as a parameter of a function or interface member. +- `RetTy` is a value or an iterator (`RetTy::Iterator(Ty)`), legal only as + the outermost return of a synchronous callable. + +Every callable binding stores its lowering, resolved once by the model +(`weaveffi_model::plan`): + +- **`FnBinding`** (a function or interface member) carries its parameters + (`ParamBinding { name, ty: ParamTy, doc, pass: ArgPass }`), its `ret: + Option` and `ret_pass: RetPass`, its `error: ErrorStrategy`, its + `abi: AbiFn` (the C symbol to call, with its ordered slots: the sync + entry, the async launcher, or the iterator launcher), and its `shape: + CallShape` (`Sync` or `Async(AsyncBinding)`). An iterator function is a + `Sync` call whose `ret_pass` is `RetPass::Iterator(IteratorBinding)`. +- **`ArgPass`** says how an argument crosses and names its slots: `Direct`, + `OptDirect { has, value, inner }`, `Slice { ptr, len, elem }`, `String`, + `Bytes`, `Buffer` (each `{ ptr, len }`), `Object { slot, nullable, + interface }`, or `Callback { ctx, vtable, nullable, interface }`. +- **`RetPass`** says what the C return and its out slots mean: `Void`, + `Direct`, `OptDirect { out_value }`, `Slice { out_len, elem }`, `String`, + `Bytes`, `Buffer` (each `{ out_len }`), `Object { nullable, interface, + destroy_symbol }`, or `Iterator`. +- **`ResultPass`** (on `AsyncBinding`, with the completion's + `callback_type`, its full `callback_params`, and the optional + `cancel_token` slot), **`ItemPass`** (on `IteratorBinding`, with its + `next` and `destroy_symbol`), and **`CallbackRetPass`** (on + `CallbackMethodBinding`) do the same for async results, iterator items, + and callback-method returns, each variant naming its own slots, so no + generator looks a slot up by position. +- **`ErrorStrategy`** is `Trap` (no `throws`: a non-zero code is a bug), + `Domain(name)` (positive codes of that domain, open to codes added later; + `Model::error_domain(name)` gives its `ErrorBinding`), or `Untyped` + (`throws: any`: `-1` and a message). + +Callback methods are callables too: `CallbackMethodBinding` has value-typed +parameters with their `ArgPass`, a `ret: Option` with its +`CallbackRetPass`, an `ErrorStrategy`, and an `AbiFn` whose symbol is the +vtable field name (`ctx` first, `out_err` last). A generator renders these +in its own syntax; it doesn't decide them. + +## Targets and the orchestrator + +A generator implements `weaveffi_cli::targets::Target`, an object-safe trait +whose implementing struct owns its configuration (its `[generators.]` +table, deserialized with `serde`): ```rust,ignore -pub trait LanguageBackend: Send + Sync { - type Config: Default + Clone + Send + Sync; +pub trait Target: Send + Sync { fn name(&self) -> &'static str; - fn files(&self, model: &Model, out_dir: &Utf8Path, - config: &Self::Config) -> Vec; - fn package(/* ... */) -> Option> { None } + fn render(&self, model: &Model) -> Vec; // paths relative to {out}/{name}/ + fn package(&self, model: &Model, ctx: &PackageContext<'_>) -> Result> { .. } + fn finish_package(&self, dist: &Utf8Path, artifacts: &[Artifact], ctx: &PackageContext<'_>) + -> Result> { .. } + fn glue(&self, model: &Model) -> Option { None } + fn dev_bundle_dir(&self, model: &Model) -> Option { None } + fn linkage(&self) -> Linkage { Linkage::Runtime } + fn fixed_files(&self) -> &'static [&'static str] { &[] } } ``` -`ConfiguredBackend::new(backend, config)` erases the backend to -`dyn Target`, whose `render(&Model, out_dir)` returns `OutputFile`s. -Each backend walks the model in the order its language needs and builds its -own file layout in `files`. +`targets::REGISTRY` lists every target once: its name, description, whether +it's generated by default, and how to build it from its config table. It +drives `weaveffi.toml` parsing, `--target` validation, help, and +completions, the snapshot and determinism tests, the benchmarks, and a test +that the CI matrices, `scripts/check-fixtures.sh`, and `conformance/run.sh` +list exactly the registry's targets. The hooks carry everything a command +needs to know about a target (what `package` builds, which C glue `build` +prebuilds, where `dev` copies the library, how `dev` tells the user to link +it), so no command matches on a target's name. + +Targets render through the **shared emitters** in `codegen/` instead of +keeping their own copies: + +- `codecs::composites(model)` lists every optional, list, and map that + crosses inside a value buffer, deduplicated and in dependency order, and + `codecs::stem(&ty)` is its one canonical mangled name (`list_i32`, + `opt_Item`, `map_string_list_i64`). OptDirect and Slice values at a call + boundary aren't composites; the same types nested in a record are. +- `contract::tables(model)` and `contract::rows(model, root)` give each + top-level module's contract rows (id, hash, path, and canonical + signature), including one per error code and per callback method. +- `errors::tables(model, suffix)` gives every domain with its codes and + their type names under the shared naming rule. +- `docs::Doc` and `docs::ApiNames` render doc and deprecation text with + backticked API identifiers (`` `new_op` ``) rewritten to the target's + spelling. +- `common` holds doc-comment emission, prose wrapping, and `PascalCase`; + `CodeWriter` is the indentation-aware writer. The `Orchestrator` runs the selected targets: 1. **Render.** Every target renders in parallel (rayon), in memory. -2. **Compare.** Each file is compared with the output directory, and each +2. **Plan.** Each file is compared with the output directory, and each target's file list with its record from the previous run, - `{out}/.weaveffi-cache/{target}.json`. When nothing differs, nothing is - written. -3. **Write.** Each file is written only if its contents differ, files + `{out}/.weaveffi-cache/{target}.json`, giving a `Changeset`: the files + that would be added, modified, or removed. +3. **Apply.** Each file is written only if its contents differ, files recorded by the previous run but no longer produced are deleted, and the record is updated. -`weaveffi diff` uses `render` directly and compares in memory. +`weaveffi generate --check` reports the plan and exits `1` if it isn't +empty, `--diff` prints it as a unified diff, and `--dry-run` lists every +rendered file; none of them applies it. ## The CLI The library's `config` module holds `ProjectConfig` (the `[project]`, `[package]`, `[build]`, and `[generators.*]` tables, with discovery and path -resolution) and the `cli_targets!` registry: one line per target that -expands to the typed `[generators.]` field, the `--target` name, and the -orchestrator registration. Its `project` module is the front half every -command runs: `Project` locates the project and its input (an IDL, a crate, -or a library alone), builds a crate's library with `cargo` and reads its -frames with `library`, resolves the identity, and validates the API into -the `Model` once. A `build.rs` can use `Project` too. The binary's -`commands/` has one module per subcommand: `init`, `generate`, `dev`, -`validate`, `diff`, `extract`, `build`, `package`. +resolution). Its `project` module is the front half every command runs: +`Project` locates the project and its input (an IDL, a crate, or a library +alone), builds a crate's library with `cargo rustc --lib --crate-type +cdylib --profile

` and reads its frames, resolves the identity, and +validates the API into the `Model` once. A `build.rs` can use `Project` and +the `Orchestrator` too. The commands live in the library's hidden +`commands` module, one per subcommand: `init`, `generate`, `dev`, +`validate`, `extract`, `build`, `package` (`schema` and `completions` are +handled in the binary); every command that takes an input shares one +argument group (the input, `--config`, and, where it reads a library, +`--library` and `--profile`). Commands return an exit code; the library +reports errors with `miette`, and the binary renders them. ## Adding a generator 1. Add `crates/weaveffi-cli/src/targets//` following an existing - target's layout: `mod.rs` (config, generator, `LanguageBackend` impl), - `types.rs`, `codec.rs`, `calls.rs`, `entities.rs`, `package.rs`, a - `runtime/` directory of fixed source, and `tests.rs`. -2. Implement `LanguageBackend`. Take every name from the model and the - identity; bind to `{library}` (and, if the language loads it at run time, - accept a full path in `{PREFIX}_LIBRARY`); check the ABI revision and - every top-level module's contract table at load; follow the + target's layout: `mod.rs` (the config struct, the generator struct that + owns it, `From`, and the `Target` impl), `types.rs`, `codec.rs`, + `calls.rs`, `entities.rs`, `package.rs`, a `runtime/` directory of fixed + source, and `tests.rs`. +2. Implement `Target`. Take every name from the model and the identity, and + every transport decision from the stored contracts (`ArgPass`, + `RetPass`, `ResultPass`, `ItemPass`, `CallbackRetPass`, + `ErrorStrategy`); render composites, contract rows, error tables, and + doc text through the shared emitters. Bind to `{library}` (and, if the + language loads it at run time, accept a full path in + `{PREFIX}_LIBRARY`); check the ABI revision and every top-level module's + contract table at load, reporting a failure as a catchable error; read + the error message as `(ptr, len)`; map an unknown positive code of a + domain to the domain's base error; follow the [trap policy](guides/errors-and-memory.md#the-trap-policy); surface - cancellation idiomatically; keep objects alive across calls. Implement - `package` if the ecosystem has an installable artifact. -3. Register it with one line in `cli_targets!` in - `crates/weaveffi-cli/src/config.rs`. -4. Add it to `snapshot_tests!` in `crates/weaveffi-cli/tests/snapshots.rs` - and accept the new snapshots. -5. Add `scripts/fixtures/.sh`, which compiles or type-checks a - generated tree with the language's toolchain, and a CI matrix entry. -6. Add consumers under `conformance//` with a `run.sh` that declares - one lane per sample, and add the language to `conformance/run.sh`. -7. Write `docs/src/generators/.md`, add it to `SUMMARY.md`, and add a - row to the [capability matrix](generators/README.md). + cancellation idiomatically; keep objects alive across calls; allocate + callback returns with `{prefix}_alloc`; and set the vtable's + thread-affine flag only if the language needs it. Implement `package` if + the ecosystem has an installable artifact, and the other hooks the + target needs. +3. Register it with one entry in `REGISTRY` in + `crates/weaveffi-cli/src/targets/mod.rs`, run the snapshot tests, and + review and accept the new `kitchen_sink` snapshots (list runtimes and + manifests in `fixed_files` so they aren't snapshotted). +4. Add `scripts/fixtures/.sh`, which compiles or type-checks a + generated tree with the language's toolchain, and add the target to the + CI fixture matrix and `scripts/check-fixtures.sh`. +5. Add consumers under `conformance//` with a `run.sh` that declares + one lane per sample, and add the language to `conformance/run.sh` and the + CI conformance matrix. (The registry test fails until every list + matches.) +6. Write `docs/src/generators/.md` (including its tier), add it to + `SUMMARY.md`, and add a row to the + [capability matrix](generators/README.md). ## Testing @@ -228,22 +318,25 @@ usually has to pass all of them. | Layer | Where | What it pins | |-------|-------|--------------| -| Unit tests | `#[cfg(test)]` modules; `tests.rs` in each target | Parsing, validation rules, `Ty` classification, lowering, naming helpers, metadata frames (`meta`) | +| Unit tests | `#[cfg(test)]` modules; `tests.rs` in each target | Parsing, validation rules, `Ty` classification, lowering and the stored contracts, contract strings, naming helpers, metadata frames (`meta`); per target, only target-specific naming, escaping, and configuration | | Validation tests | `crates/weaveffi-model/src/validate/tests.rs` | Every `ValidationError` with its source span | | Property tests | `crates/weaveffi/tests/buffer_proptest.rs` | Codec laws: round trips, self-delimiting encodings, rejected trailing bytes | -| Runtime tests | `crates/weaveffi/tests/` | Exported runtime symbols, cancellation and dropped futures, foreign-error routing, leak counters | +| Runtime tests | `crates/weaveffi/tests/` | Exported runtime symbols, run alignment, OptDirect and Slice lifting, thread-affine vtables, cancellation and dropped futures, foreign-error routing, leak counters | | Macro tests | `crates/weaveffi-macros/tests/ui/` (`trybuild`) | `pass_*.rs` compile; `fail_*.rs` fail with the pinned `.stderr` | -| Snapshots | `crates/weaveffi-cli/tests/snapshots.rs` (`insta`) | Byte-exact output of every target: fixture-dependent files for every fixture in `tests/fixtures/`, fixed runtimes and manifests once, and copies of the C header asserted equal to the C target's | -| CLI tests | `crates/weaveffi-cli/tests/cli/`, `crates/weaveffi-cli/tests/abi_version.rs` | Every subcommand's behavior and exit codes, library mode (the `tests/fixtures/producer` crate extracts to its `expected.yml`, and generating from its library equals generating from that IDL), determinism, no stub markers in output, the ABI revision lockstep | +| Snapshots | `crates/weaveffi-cli/tests/snapshots.rs` (`insta`) | Every registered target renders all five fixtures; the `kitchen_sink` output is snapshotted file by file (except each target's `fixed_files`), and any copy of the C header must equal the C target's own | +| CLI tests | `crates/weaveffi-cli/tests/cli/`, `crates/weaveffi-cli/tests/abi_version.rs`, `crates/weaveffi-cli/tests/c_buffer.rs` | Every subcommand's behavior and exit codes, library mode (the `tests/fixtures/producer` crate extracts to its `expected.yml`, and generating from its library equals generating from that IDL), determinism, the registry against CI and scripts, no stub markers in output, the ABI revision lockstep, every fixture's C headers compiling as C11 and C++17 | | Fixture compile checks | `scripts/check-fixtures.sh ` | Every fixture's generated tree compiles or type-checks with the target's toolchain | | Conformance | `conformance/run.sh` | Real consumers in every language against every sample, with leak counters at zero | | Fuzzing | `crates/weaveffi-fuzz` | Parsers and the validator never panic; value-buffer decoders reject malformed bytes cleanly | -The fixtures are `kitchen_sink` (every feature), `edge_cases` (reserved -words, deep nesting, objects in every legal position), `nested_modules` -(cross-module references and name clashes), `docs_everywhere` (doc-comment -emission), and `shapes` (rich enums). Snapshots redact the CLI version, so a -version bump doesn't touch them. +The fixtures are `kitchen_sink` (every feature, including OptDirect and +Slice values in every position, several error domains, and `throws: any`), +`edge_cases` (reserved words, deep nesting, objects in every legal +position), `nested_modules` (cross-module references and name clashes), +`docs_everywhere` (doc-comment emission), and `shapes` (rich enums). Only +`kitchen_sink` is snapshotted; the other four are covered by the fixture +compile checks, the determinism test, and targeted assertions. Snapshots +redact the CLI version, so a version bump doesn't touch them. Day to day: diff --git a/docs/src/comparison.md b/docs/src/comparison.md index f2c01fed..12ed1c93 100644 --- a/docs/src/comparison.md +++ b/docs/src/comparison.md @@ -6,9 +6,11 @@ evaluate against it: **UniFFI** (Mozilla's multi-language generator), **Diplomat** (the Rust-to-many-languages tool behind ICU4X), **cbindgen** plus hand-written glue, the single-language bridges **swift-bridge**, **napi-rs**, and **wasm-bindgen**, and the C/C++-first generators **SWIG** and **autocxx**. +A [shorter section](#other-tools) covers **interoptopus**, **flapigen**, +**PyO3** with **maturin**, and **cxx**. > All comparisons reflect the public state of each project at the time of -> writing (WeaveFFI ABI revision 4, schema `0.11.0`). If something here is out +> writing (WeaveFFI ABI revision 5, schema `0.12.0`). If something here is out > of date, please open a PR. ## The short version @@ -29,7 +31,8 @@ WeaveFFI-built libraries coexist in one process. The honest flip side is that WeaveFFI is pre-1.0 and its type system is deliberately smaller than UniFFI's: no user-defined generics or trait -objects, no custom types, no async callback methods, and no multi-file IDL +objects, custom types only on the producer side (bindings see the type's +builtin representation), no async callback methods, and no multi-file IDL imports. The [roadmap](roadmap.md) lists what's planned. ## At a glance @@ -37,14 +40,14 @@ imports. The [roadmap](roadmap.md) lists what's planned. | | **WeaveFFI** | **UniFFI** | **Diplomat** | **cbindgen + glue** | **swift-bridge** | **napi-rs** | **wasm-bindgen** | **SWIG** | **autocxx** | |------------------------------------|:------------:|:----------:|:------------:|:-------------------:|:----------------:|:-----------:|:----------------:|:--------:|:-----------:| | Producer language | Rust, C, C++, Zig (anything with a C ABI) | Rust | Rust | Rust | Rust | Rust | Rust | C / C++ | C++ (consumed from Rust) | -| Input | YAML / JSON / TOML IDL or annotated Rust (read from the built library) | UDL or proc-macros (library mode reads the built library) | annotated Rust bridge crate | Rust source | annotated Rust bridge module | annotated Rust | annotated Rust | C/C++ headers + `.i` file | C++ headers | +| Input | YAML / JSON IDL or annotated Rust (read from the built library) | UDL or proc-macros (library mode reads the built library) | annotated Rust bridge crate | Rust source | annotated Rust bridge module | annotated Rust | annotated Rust | C/C++ headers + `.i` file | C++ headers | | Consumer languages | C, C++, Swift, Kotlin, Node.js, Wasm/JS, Python, .NET, Dart, Go, Ruby | Kotlin, Swift, Python, Ruby first-party; Go, C#, Dart, Kotlin Multiplatform, React Native (`uniffi-bindgen-react-native`) as external bindgens | C, C++, JS/TS (Wasm), Dart, Kotlin (JNA), Python (nanobind) | C (you write the rest) | Swift | Node.js | JS/TS (Wasm) | many (Python, Java, C#, Ruby, Lua, Perl, PHP, R, ...) | Rust | | Standalone C header | ✓ (the contract every target shares) | ✗ (private scaffolding ABI) | ✓ (C backend) | ✓ (its purpose) | ✓ (generated) | ✗ | ✗ | ✗ | n/a | | **Type system** | | | | | | | | | | | Records / enums / optionals / lists / maps | ✓ (value buffers) | ✓ | ✓ (structs, enums, `Option`, slices; no maps) | manual | ✓ (transparent structs and enums, `Option`, `Vec`) | ✓ (serde objects) | ✓ (`serde-wasm-bindgen` or classes) | ✓ | ✓ | | Objects with methods | ✓ reference counted, shareable, nestable | ✓ (`Arc`) | ✓ (opaques, borrowed or owned) | manual | ✓ (opaque types) | ✓ (`#[napi]` classes) | ✓ (classes) | ✓ (classes) | ✓ (C++ classes) | | Callback interfaces | ✓ (vtable; synchronous, any return, `throws`, optional, failures as `Result`) | ✓ (foreign traits; any return, `throws`, async) | partial (Kotlin, C, C++; input-only) | manual fn pointers | partial (closures Rust to Swift) | ✓ (`ThreadsafeFunction`) | ✓ (closures) | partial (directors) | partial | -| Typed error domains | ✓ (per-module codes, opt-in `throws`, payload fields) | ✓ (error enums) | ✓ (`Result`) | manual | ✓ (`Result`) | ✓ (JS `Error`) | ✓ (`Result`) | ✗ | ✗ | +| Typed error domains | ✓ (several per module, open to new codes, payload fields; `throws` a domain or an untyped `any`) | ✓ (error enums) | ✓ (`Result`) | manual | ✓ (`Result`) | ✓ (JS `Error`) | ✓ (`Result`) | ✗ | ✗ | | Async functions | ✓ (callback ABI, pluggable spawner, native cancellation on every target) | ✓ (poll-based, foreign executors) | ✗ | manual | ✓ (both directions) | ✓ (Tokio) | ✓ (`Promise`) | ✗ | ✗ | | Iterators | ✓ (`iter`, lazy on every target) | ✗ (materialize or use a trait) | partial (`DiplomatWrite`) | manual | ✗ | ✗ | ✓ (JS iterators) | partial | ✗ | | Generics / trait objects | ✗ (fixed set of built-in shapes) | partial (traits, no generics) | partial (traits on some backends) | ✗ | partial | ✗ | ✗ | ✓ (templates via `%template`) | ✓ | @@ -54,7 +57,7 @@ imports. The [roadmap](roadmap.md) lists what's planned. | Publishable per-ecosystem packages | ✓ (`weaveffi package`: wheels, npm tarballs, gems, an XCFramework SwiftPM package, a `.nupkg`; Gradle, pub, and Go as ready-to-publish directories) | via companion tools (`cargo-swift`, `maturin`, `uniffi-bindgen-react-native`) | partial | n/a | ✓ (SwiftPM) | ✓ (npm) | ✓ (npm via wasm-pack) | ✗ | n/a | | Schema-checked IDL with JSON Schema | ✓ | ✗ | n/a | n/a | n/a | n/a | n/a | ✗ | n/a | | Load-time ABI check | ✓ (ABI revision plus per-declaration contract tables) | ✓ (checksums) | ✗ | ✗ | ✗ | ✓ (N-API version) | ✗ | ✗ | n/a | -| Generated-output drift check in CI | ✓ (`weaveffi diff --check`) | build-time | build-time | ✓ | build-time | build-time | build-time | ✗ | build-time | +| Generated-output drift check in CI | ✓ (`weaveffi generate --check`) | build-time | build-time | ✓ | build-time | build-time | build-time | ✗ | build-time | | Maturity | pre-1.0 | shipping in Firefox and Mozilla products since 2020 | shipping in ICU4X | 1.0+, widely deployed | 0.1.x, active | 2.x+, widely deployed | 0.2.x, ubiquitous | 30+ years | pre-1.0 | | License | MIT OR Apache-2.0 | MPL-2.0 | MIT OR Apache-2.0 | MPL-2.0 | MIT OR Apache-2.0 | MIT | MIT OR Apache-2.0 | GPL-3.0 (generated code exempt) | MIT OR Apache-2.0 | @@ -71,9 +74,10 @@ is ahead of WeaveFFI today. ships in Firefox, Firefox Sync, Glean, and Nimbus and has years of battle-testing across iOS, Android, and desktop. Its foreign traits can be `async`; WeaveFFI's callback-interface methods can return any type and - `throw`, but are synchronous. UniFFI also supports custom types and - external types across crates, whereas a WeaveFFI API is one IDL document - (or one Rust producer crate). Its ecosystem of companion tools is larger + `throw`, but are synchronous. UniFFI's custom types can map to a + consumer-side type in each language (WeaveFFI's are converted only on + the producer side), and it supports external types across crates, whereas a + WeaveFFI API is one IDL document (or one Rust producer crate). Its ecosystem of companion tools is larger too: `cargo-swift` builds the XCFramework and SwiftPM package, `maturin` builds Python wheels, and `uniffi-bindgen-react-native` targets React Native, where WeaveFFI's `weaveffi package` covers the first two itself @@ -112,20 +116,23 @@ is ahead of WeaveFFI today. the call fails with `-1`, and there's little for an `AbortSignal` to cancel), and callback-interface methods fire only while a call into the module is on the stack. wasm-bindgen's `wasm-bindgen-futures` integrates - with the JS event loop natively, and Emscripten-based toolchains can use - `pthread`s. In WeaveFFI's Emscripten compatibility mode, async functions - and callback interfaces are not available at all. + with the JS event loop natively; WeaveFFI targets only + `wasm32-unknown-unknown`, which has no threads. - **The single-language tools package better for their one ecosystem.** `maturin` (with PyO3) and napi-rs's CLI build wheels and npm packages for every platform from one CI matrix with years of edge cases handled; `weaveffi package` is newer, and it doesn't prebuild the Node.js addon or the JNI shim for Windows. - **No formal stability guarantee yet.** WeaveFFI is pre-1.0; schema - `0.11.0` and ABI revision 4 changed names, the load-time contract check, - and the callback vtable layout without compatibility shims (see the - [migration guide](stability.md#migrating-to-schema-011-and-abi-4)). - UniFFI, cbindgen, napi-rs, wasm-bindgen, and SWIG offer stronger - compatibility commitments today. + `0.12.0` and ABI revision 5 changed the error struct, enum types, and the + contract tables without compatibility shims (see the + [migration guide](stability.md#migrating-from-025-to-the-next-release-schema-012-abi-5)). + Revision 5 is designed to grow additively, but UniFFI, cbindgen, napi-rs, + wasm-bindgen, and SWIG offer stronger compatibility commitments today. +- **Not every target is equally mature.** C, C++, Swift, Kotlin, Python, + Node.js, and .NET are Tier 1; Go, Ruby, Dart, and WebAssembly are Tier 2 + and may lag new ABI work (see + [Target tiers](stability.md#target-tiers)). ## When to choose WeaveFFI @@ -149,9 +156,9 @@ WeaveFFI is the right pick when you want: methods once and each target gets a protocol, interface, or abstract class to implement; the producer receives an `Arc` it can retain and call from any thread. Methods can return strings, records, and objects, - and a consumer-side exception reaches the producer as a - `weaveffi::ForeignError` (carrying a typed domain error when the method - is `throws`) that it can propagate to the original caller with `?`. + and a consumer-side exception reaches the producer as a typed domain + error (when the method throws one) or through `From`, which + it can propagate to the original caller with `?`. 5. **Standalone, publishable consumer packages.** Generated packages are self-contained; `weaveffi build` cross-compiles the producer per platform and `weaveffi package` bundles it into wheels, npm tarballs, gems, an @@ -164,20 +171,44 @@ WeaveFFI is the right pick when you want: `Future` in Dart, all from the same `async: true` flag; Rust producers can plug their own executor with `weaveffi::set_spawner`. Cancelling the awaiting task, coroutine, or promise cancels the native call. -7. **A CI-first CLI.** `validate`, `diff --check`, `extract`, `schema`, - `build`, and `package` are designed to drop into pipelines, every generator's output is +7. **A CI-first CLI.** `validate`, `generate --check`, `extract`, `schema`, + `build`, and `package` are designed to drop into pipelines, every + generator's output is byte-for-byte deterministic, and generated consumers refuse to load a producer built for a different ABI revision or from a different API definition (per-declaration contract tables, so adding a declaration never breaks a deployed binding). +## Other tools + +- **interoptopus** generates C#, C, and Python bindings from annotated Rust + (`#[ffi_function]`, `#[ffi_type]`), with patterns for services, slices, + options, and callbacks. It's Rust-only on the producer side and covers + fewer languages; its C# backend is especially polished. +- **flapigen** (formerly rust_swig) generates Java (JNI) and C++ bindings + from a declarative `foreign_class!` description in the producer's build + script. It's a good fit when exactly those two consumers matter. +- **PyO3 with maturin** builds a native CPython extension module from Rust: + Python classes, exceptions, iterators, and async integration with direct + access to the Python object model, and wheels for every platform. For a + Python-only library it's deeper and better integrated than any + multi-language tool, WeaveFFI's `ctypes` binding included. +- **cxx** is a safe, statically checked bridge between Rust and C++ in both + directions (`#[cxx::bridge]`), sharing types like `String`, `Vec`, and + `unique_ptr` without a C layer in between. If both sides are your own Rust + and C++, it gives stronger guarantees than WeaveFFI's C++ target, which + wraps a C ABI. + ## When to choose something else - **You only need Kotlin, Swift, and Python and want maximum stability, or you need async callbacks**: use UniFFI. - **You only need a C header for a Rust crate**: use cbindgen. -- **You ship to exactly one of Swift, Node.js, or the browser**: use - swift-bridge, napi-rs, or wasm-bindgen respectively. +- **You ship to exactly one of Swift, Node.js, Python, or the browser**: use + swift-bridge, napi-rs, PyO3 with maturin, or wasm-bindgen respectively. +- **You need Rust and C++ calling each other in one codebase**: use cxx. +- **You need only C#, or only Java and C++**: interoptopus or flapigen may + be enough. - **You're wrapping a large existing C++ codebase from Rust**: use autocxx (or cxx plus bindgen directly). - **Your target language is Lua, Tcl, R, Octave, Perl, or PHP**: use SWIG. diff --git a/docs/src/faq.md b/docs/src/faq.md index 3c957243..5e864e34 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -8,8 +8,8 @@ maximum maturity, it's the safer pick. WeaveFFI exists for a different set of needs: eleven first-party targets from one definition, a producer that can be written in any language with a C ABI (the generated header is a public contract, not private scaffolding), a standalone CLI built for CI, and a -schema-checked YAML, JSON, or TOML IDL. UniFFI is still ahead on async -callback methods, custom types, and cross-crate type imports. See the +schema-checked YAML or JSON IDL. UniFFI is still ahead on async callback +methods, consumer-side custom types, and cross-crate type imports. See the [comparison](comparison.md) for the full table. ## Can I use it with a C++ codebase? @@ -35,9 +35,10 @@ trait-object interfaces are on the [roadmap](roadmap.md). A call costs the marshalling of its arguments, one `extern "C"` call, and the marshalling of its result. Scalars pass by value. Strings and bytes pass as borrowed `(ptr, len)` views, so the producer copies only what it keeps. -Objects pass as one pointer. Records and collections are encoded into one -value buffer per value, with single-copy fast paths for byte and numeric -lists. Async calls add a completion callback and whatever executor drives +Objects pass as one pointer. Optional scalars pass as a flag plus the +value, and numeric lists as typed arrays the producer reads in place (a +`&[f64]` parameter borrows the caller's array without copying). Records and +other collections are encoded into one value buffer per value. Async calls add a completion callback and whatever executor drives the future. The runtime itself is the small `weaveffi::abi` module; the `leak-check` counters cost an atomic update per counted operation and are off by default. @@ -45,12 +46,16 @@ by default. ## How are errors propagated? Every fallible C call takes a `{prefix}_error*` out-parameter; async calls -receive one in their completion. Positive codes are your declared error -domain, negative codes are the runtime's (generic, panic, marshalling, -callback failure, cancelled). A function marked `throws: true` (a `Result` in -Rust) surfaces domain codes as typed errors in every language; a function -without it traps on failure, since a failure there is a bug. See -[Errors and Memory](guides/errors-and-memory.md#the-trap-policy). +receive one in their completion. Positive codes belong to the error domain +the callable throws, negative codes are the runtime's (untyped, panic, +marshalling, callback failure, cancelled). A function declared `throws: +KvError` (a `Result` in Rust) surfaces the domain's codes as +typed errors in every language, and an unknown code added later as the +domain's base error. A function declared `throws: any` (a `Result` with any +other error type, such as `anyhow::Error`) surfaces its failure as the +library's base error with a message. A function without `throws` traps on +failure, since a failure there is a bug. See +[Errors and Memory](guides/errors-and-memory.md#domain-errors-and-the-trap-channel). ## Can two WeaveFFI libraries live in one process? @@ -65,12 +70,14 @@ own copy of the small runtime. They refuse to load. Every generated consumer compares the library's ABI revision with the one it was generated against, and checks that every declaration it uses is in the library's contract table with the same -signature hash. On a mismatch it raises the language's load error naming the -declaration that's missing or changed. The hashes cover names, types, and -flags, but not documentation or declaration order, so editing doc comments -never breaks a deployed binding, and neither does adding a function, method, -or type. -In CI, `weaveffi diff --check` catches stale committed bindings before they +signature hash. On a mismatch it raises the language's load error (a +catchable one, in every language) naming the declaration that's missing or +changed. The hashes cover declaration names, types, and flags, but not +parameter or field names, documentation, or declaration order, so editing doc +comments or renaming a parameter never breaks a deployed binding, and neither +does adding a function, method, type, or error code, or appending a callback +method to its interface. +In CI, `weaveffi generate --check` catches stale committed bindings before they ship. ## Does the CLI parse my Rust source? @@ -129,11 +136,11 @@ objects passed to the producer are borrowed. See ## Which executor runs my async functions, and how does cancellation work? -By default, a small pool of worker threads (one per core, at least two) -started on the first call. With the `weaveffi` crate's `tokio` feature it's -Tokio: the current runtime when the call is made from inside one, otherwise -one the library creates. `weaveffi::set_spawner` installs any other executor -and overrides both. Consumers see their native async idiom, and cancelling it (a Swift `Task`, a Kotlin coroutine, an +By default, Tokio (the `weaveffi` crate's default `tokio` feature): the +current runtime when the call is made from inside one, otherwise one the +library creates. With `default-features = false`, each call runs on a thread +of its own. `weaveffi::set_spawner` installs any other executor and +overrides both. Consumers see their native async idiom, and cancelling it (a Swift `Task`, a Kotlin coroutine, an `AbortSignal`, a `CancellationToken`, a Go `context`) cancels the native call: the runtime drops the future and completes with the cancelled code, which surfaces as the language's cancellation error. See diff --git a/docs/src/generators/README.md b/docs/src/generators/README.md index 0eac540b..d5e6f076 100644 --- a/docs/src/generators/README.md +++ b/docs/src/generators/README.md @@ -1,14 +1,18 @@ # Generators WeaveFFI ships eleven generators, one per target. Every generator implements -the whole IDL surface of schema 0.11.0 on ABI revision 4: records, C-style +the whole IDL surface of schema 0.12.0 on ABI revision 5: records, C-style and rich enums, reference-counted objects in every position, callback -interfaces, optionals, lists, maps, typed error domains, nested modules, -async functions with cancellation, and lazy iterators. Each one names its -output from the library's [identity](../reference/naming.md), binds to the -native library `{library}`, checks the ABI revision and every top-level -module's contract table at load time, and produces a tree under -`{out}/{target}/` that builds with the language's normal tooling. +interfaces, optionals, lists, maps, typed and open error domains (several per +module, plus `throws: any`), nested modules, async functions with +cancellation, and lazy iterators. Optional scalars and numeric lists cross +the ABI directly (the OptDirect and Slice families) rather than through a +value buffer, in every position. Each generator names its output from the +library's [identity](../reference/naming.md), binds to the native library +`{library}`, checks the ABI revision and every top-level module's contract +table at load time (reporting a mismatch as a catchable error), and produces +a tree under `{out}/{target}/` that builds with the language's normal +tooling. The targets that load the library at run time (Python, Ruby, .NET, Dart, Kotlin on the JVM, and WebAssembly on Node.js) accept a full path to it in @@ -18,21 +22,29 @@ through its CMake target (which reads `{PREFIX}_LIBRARY` at configure time), and the Node.js addon when `npm install` compiles it. Each language page says where its package looks. +## Tiers + +Every target passes the same conformance lanes. **Tier 1** targets (C, C++, +Swift, Kotlin, Python, Node.js, and .NET) carry the stability promise and +get ABI work first. **Tier 2** targets (Go, Ruby, Dart, and WebAssembly) may +lag a release behind on new ABI features. See +[Stability](../stability.md#target-tiers). + ## Capability matrix -| Target | Package / module | Object release | Async | Cancellation | Callback interface | `iter` | Minimum toolchain | -|--------|------------------|----------------|-------|--------------|--------------------|-----------|-------------------| -| [C](c.md) | header `{library}.h` | `_destroy` | launcher plus callback | `{prefix}_cancel_token*` | `ctx` plus vtable struct | `_next`/`_destroy` | C11 | -| [C++](cpp.md) | header `{library}.hpp`, namespace `{prefix}` | RAII destructor; copies clone | `std::future` | RAII `CancelToken` | abstract class | input range | C++17, CMake 3.14 | -| [Swift](swift.md) | module `PascalCase(name)` | `deinit` | `async` / `async throws` | `Task` cancellation | protocol | `Sequence` | Swift 5.9 | -| [Kotlin](kotlin.md) | package `{prefix}` | `close()`, cleaner backstop | `suspend fun` | coroutine cancellation | interface | `NativeIterator` (an `Iterator`) | Kotlin 2.0; Android API 21 or a JVM | -| [Node.js](node.md) | npm `{name}` | `close()`, `Symbol.dispose`, finalizer | `Promise` | `AbortSignal` | object implementing an interface | `IterableIterator` | Node.js 18 | -| [WebAssembly](wasm.md) | npm `{name}` | `close()`, `Symbol.dispose`, finalizer | `Promise` | `AbortSignal` | object implementing an interface | `IterableIterator` | Node.js 18 or a current browser | -| [Python](python.md) | dist `{name}`, import `{prefix}` | `close()`, `with`, finalizer | awaitable | task cancellation | abstract base class | iterator | Python 3.9 | -| [.NET](dotnet.md) | namespace `PascalCase(name)` | `Dispose()` (`SafeHandle`) | `Task` | `CancellationToken` | interface | `IEnumerable` | .NET 8 | -| [Dart](dart.md) | package `{prefix}` | `dispose()`, `NativeFinalizer` | `Future` | cancel-token object | abstract class | `Iterable` | Dart 3.10 | -| [Go](go.md) | module `{name}`, package `{prefix}` | `Close()`, finalizer | blocking call | `context.Context` | interface | `iter.Seq` / `iter.Seq2` | Go 1.23 | -| [Ruby](ruby.md) | gem `{name}`, module `PascalCase(name)` | `close`, `FFI::AutoPointer` | blocking call | `cancel:` keyword | duck-typed object | `Enumerator` | Ruby 2.7 with the `ffi` gem 1.15 | +| Target | Tier | Package / module | Object release | Async | Cancellation | Callback interface | `iter` | Minimum toolchain | +|--------|------|------------------|----------------|-------|--------------|--------------------|-----------|-------------------| +| [C](c.md) | 1 | header `{library}.h` | `_destroy` | launcher plus callback | `{prefix}_cancel_token*` | `ctx` plus vtable struct | `_next`/`_destroy` | C11 | +| [C++](cpp.md) | 1 | header `{library}.hpp`, namespace `{prefix}` | Rule-of-Zero handle; copies clone; `==` is identity | `std::future` | RAII `CancelToken` | abstract class (`shared_ptr`) | `Range` (an input range) | C++17 (clean on C++20), CMake 3.14 | +| [Swift](swift.md) | 1 | module `PascalCase(name)` | `deinit` | `async` / `async throws` | `Task` cancellation | protocol | `NativeSequence` (a `Sequence`) | Swift 5.9 (Swift 5 and 6 language modes) | +| [Kotlin](kotlin.md) | 1 | package `{prefix}` | `close()`, cleaner backstop | `suspend fun` | coroutine cancellation | interface | `NativeIterator` (an `Iterator`) | Kotlin 2.0; Android API 21 or a JVM | +| [Node.js](node.md) | 1 | npm `{name}` (plus `./debug`) | `close()`, `Symbol.dispose`, finalizer | `Promise` | `AbortSignal` | object implementing an interface | `NativeIterator` (`close()`, `Symbol.dispose`) | Node.js 18 | +| [Python](python.md) | 1 | dist `{name}`, import `{prefix}` | `close()`, `with`, finalizer | awaitable | task cancellation | abstract base class | `NativeIterator[T]` (`close()`, `with`) | Python 3.10 | +| [.NET](dotnet.md) | 1 | namespace `PascalCase(name)` | `Dispose()` (`SafeHandle`) | `Task` | `CancellationToken` on every async method | interface `I{Name}` | re-enumerable `IEnumerable` | .NET 8 | +| [Go](go.md) | 2 | module `{name}`, package `{prefix}` without underscores | `Close()`, `runtime.AddCleanup` backstop | blocking call returning `error` | `context.Context` | interface | `iter.Seq` / `iter.Seq2` | Go 1.24 | +| [Ruby](ruby.md) | 2 | gem `{name}`, module `PascalCase(name)` | `close`, `FFI::AutoPointer` | blocking call (only the calling Fiber under a scheduler) | `cancel:` keyword | duck-typed object | `Enumerator` | Ruby 3.2 with the `ffi` gem 1.16 | +| [Dart](dart.md) | 2 | package `{prefix}` (parts under `lib/src/`) | `dispose()`, `NativeFinalizer` | `Future` | cancel-token object | abstract class (thread-affine vtable) | `Iterable` | Dart 3.10 | +| [WebAssembly](wasm.md) | 2 | npm `{name}` (plus `./debug`) | `close()`, `Symbol.dispose`, finalizer | `Promise` | `AbortSignal` | object implementing an interface | `NativeIterator` (`close()`, `Symbol.dispose`) | Node.js 18 or a current browser | The language pages are authoritative for exact type names, signatures, and toolchain versions. Notes that apply across targets: @@ -41,16 +53,26 @@ toolchain versions. Notes that apply across targets: alive for the duration of each native call, so it can't be released mid-call. Disposing twice is a no-op; using a disposed wrapper raises the target's usage error. -- **Callbacks.** Callback methods may be invoked from any producer thread. - An implementation that throws reaches the original caller as runtime code - `-4`; an object passed to a callback is adopted by the implementation. +- **Errors.** A callable that throws a domain raises that domain's typed + error; a code the bindings don't know (one a newer library added) raises + the domain's base error with its code and message. A `throws: any` + callable raises the target's root error type with code `-1`. A non-zero + code from a callable that declares no errors is a trap. +- **Callbacks.** Callback methods may be invoked from any producer thread + (Dart's value-returning methods excepted; see its page). An + implementation that raises a code of the method's own domain reports it + with its fields; any other failure is reported as `-1` with its message, + or as `-4` from a method that declares no errors. An object passed to a + callback is adopted by the implementation. - **Async.** A cancelled call raises the language's cancellation error. Go and Ruby block the calling goroutine or thread while the native work runs elsewhere. See [Async and Cancellation](../guides/async.md). -- **64-bit integers.** Node.js and WebAssembly use `bigint`; Kotlin uses - `ULong` for `u64`; Dart carries `u64` in its signed 64-bit `int` (the bits - are preserved, so values above `2^63 - 1` read as negative). `NaN`, infinities, and `-0` cross - unchanged everywhere. +- **Integers.** Every target range-checks integer arguments rather than + wrapping or truncating them. Node.js and WebAssembly use `bigint` for + 64-bit values; Kotlin uses `ULong` for `u64`; Dart carries `u64` in its + signed 64-bit `int` (the bits are preserved, so values above `2^63 - 1` + read as negative). `NaN`, infinities, and `-0` cross unchanged + everywhere. ## Fixture compile checks @@ -61,7 +83,7 @@ code. `scripts/check-fixtures.sh ` generates every fixture in then runs `scripts/fixtures/.sh`, which compiles or type-checks the output with the target's own toolchain: `cc` and `c++` with `-Werror` for C, `clang++ -fsyntax-only` for C++, `swiftc -typecheck`, `kotlinc`, `tsc ---strict` and `node --check`, `py_compile` plus a type checker on the stubs, +--strict` and `node --check`, `py_compile` plus `mypy --strict`, `dotnet build -warnaserror`, `dart analyze --fatal-infos`, `go vet` and `go build`, and `ruby -wc`. CI runs one job per target. A missing tool is a printed skip locally and a failure when `CI=true`. diff --git a/docs/src/generators/c.md b/docs/src/generators/c.md index ecea0546..f977b911 100644 --- a/docs/src/generators/c.md +++ b/docs/src/generators/c.md @@ -1,11 +1,16 @@ # C The C target emits the canonical header for a library's -[C ABI](../reference/abi.md) (revision 4). Every other target binds to the -symbols it declares, and a C or C++ producer implementing an IDL by hand -implements exactly those symbols. Alongside it, the target emits a helper -header with C structs and codecs for the API's value buffers, so a C -consumer never encodes the wire format by hand. +[C ABI](../reference/abi.md) (revision 5). Every other target binds to the +symbols it declares (the C++, Swift, and Go targets include it, and the +Kotlin JNI shim and the Node.js addon compile against it), and a C or C++ +producer implementing an IDL by hand implements exactly those symbols. +Alongside it, the target emits a helper header with C structs and codecs for +the API's value buffers, so a C consumer never encodes the wire format by +hand. + +C is a [Tier 1](../stability.md#target-tiers) target: it tracks every ABI +revision as it lands and runs the full conformance suite in CI. ## What gets generated @@ -66,43 +71,94 @@ if (kvstore_kv_contract_check() != 0) { Each top-level module has a contract table: the library exports `{p}_{m}_contract(size_t* out_len)`, and the header defines the entries the bindings were generated with as `{P}_{M}_CONTRACT` (each commented with its -declaration's dotted path), their count as `{P}_{M}_CONTRACT_LEN`, and a +declaration's dotted path and canonical signature), their count as +`{P}_{M}_CONTRACT_LEN`, and a `static inline uint64_t {p}_{m}_contract_check(void)`. The checker returns `0` when every expected entry is in the library's table with an equal hash, and otherwise the id of the first one that's missing or changed, which you can look up in the macro to name the declaration. A library with extra -entries passes, so adding a declaration doesn't break existing consumers. +entries passes, so adding a declaration or an error code, or appending a +callback method, doesn't break existing consumers. See [load-time checks](../reference/abi.md#load-time-checks). ## Type mapping -| IDL type | Parameter | Return | In `{library}_buffer.h` | +| IDL type | Parameter `x` | Return | In `{library}_buffer.h` | |---|---|---|---| | `i8` to `u64`, `f32`, `f64` | `int8_t` to `uint64_t`, `float`, `double` | same | same | | `bool` | `bool` | `bool` | `bool` | -| C-style enum | `{p}_{path}_{Enum}` | same | same | -| `string` | `const uint8_t* s_ptr, size_t s_len` | `const uint8_t*` + `size_t* out_len` | `{p}_str` | -| `bytes` | `const uint8_t* b_ptr, size_t b_len` | `const uint8_t*` + `size_t* out_len` | `{p}_bytes` | +| C-style enum | `{p}_{path}_{Enum}` (`typedef int32_t`) | same | same | +| `T?` of one of those | `bool has_x, T x` | `bool` (present) + `T* out_value` | `T*` (`NULL` is absent) | +| `[i8]` `[i16]` `[i32]` `[i64]` `[u16]` `[u32]` `[u64]` `[f32]` `[f64]` | `const T* x_ptr, size_t x_len` (element count) | `T*` + `size_t* out_len` (element count) | struct `{p}_list_{T}` | +| `string` | `const uint8_t* x_ptr, size_t x_len` | `const uint8_t*` + `size_t* out_len` | `{p}_str` | +| `bytes` | `const uint8_t* x_ptr, size_t x_len` | `const uint8_t*` + `size_t* out_len` | `{p}_bytes` | | record, rich enum | value buffer `(ptr, len)` | value buffer + `out_len` | struct `{p}_{path}_{Name}` | -| `[T]`, `{K:V}` | value buffer | value buffer | structs `{p}_list_{T}`, `{p}_map_{K}_{V}` | -| `T?` | value buffer | value buffer | `T*` (`NULL` is absent) | +| any other `[T]`, `{K:V}` | value buffer | value buffer | structs `{p}_list_{T}`, `{p}_map_{K}_{V}` | +| any other `T?` | value buffer | value buffer | `T*` (`NULL` is absent) | | interface `I` | `const {p}_{path}_I*` (borrowed) | `{p}_{path}_I*` (owned) | `{p}_{path}_I*` | | `I?` | `const {p}_{path}_I*` (`NULL` is absent) | `{p}_{path}_I*` | `{p}_{path}_I*` | | callback interface, `Cb?` | `void* x_ctx, const {p}_{path}_{Cb}_vtable* x_vtable` (`NULL` vtable is absent for `Cb?`) | not allowed | not allowed | | `iter` | not allowed | iterator handle | not applicable | +Enums, rich-enum tags, and error-code types are `typedef int32_t` with their +constants in an anonymous `enum`, so their size never depends on the +compiler: + +```c +/** Task priority levels */ +typedef int32_t kitchen_sink_kitchen_Priority; +enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +}; +``` + +An optional scalar or enum (OptDirect) crosses directly: a parameter is a +presence flag plus the value (pass `0` when absent), and a return is a +`bool` C return with the value in a trailing out slot: + +```c +KITCHEN_SINK_API bool kitchen_sink_kitchen_opt_i32_id(bool has_x, int32_t x, int32_t* out_value, kitchen_sink_error* out_err); + +int32_t value = 0; +if (kitchen_sink_kitchen_opt_i32_id(true, 42, &value, &err)) { + /* present: value == 42 */ +} +``` + +A list of fixed-width numbers (Slice) crosses as a typed array: pass your +array and its element count, borrowed for the call and aligned for its +element type. A returned array is the producer's run, released with +`{p}_free_bytes` and its size in bytes: + +```c +KITCHEN_SINK_API int32_t* kitchen_sink_kitchen_scale_i32(const int32_t* xs_ptr, size_t xs_len, int32_t factor, size_t* out_len, kitchen_sink_error* out_err); + +int32_t xs[3] = {1, 2, 3}; +size_t n = 0; +int32_t* scaled = kitchen_sink_kitchen_scale_i32(xs, 3, 10, &n, &err); +/* ... use scaled[0 .. n) ... */ +kitchen_sink_free_bytes((uint8_t*)scaled, n * sizeof(int32_t)); +``` + Strings are UTF-8 without a NUL terminator and may contain NUL bytes. A parameter's pointer may be `NULL` when its length is `0`; a `NULL` pointer -with a nonzero length, or invalid UTF-8, fails the call with code `-3`. -Every returned string, byte string, and value buffer is owned by the -consumer and released with `{p}_free_bytes(ptr, len)`; `NULL` with length -`0` is the empty value. +with a nonzero length, invalid UTF-8, or a misaligned typed array fails the +call with code `-3`. Every returned string, byte string, value buffer, and +typed array is owned by the consumer and released with +`{p}_free_bytes(ptr, byte_len)`; `NULL` with length `0` is the empty value. +Every run the producer returns is 8-aligned. ## Value buffers and `{library}_buffer.h` -Records, rich enums, lists, maps, and optionals cross as one serialized -[value buffer](../reference/value-buffers.md). The helper header renders -each record as a struct (from the `kitchen_sink` fixture): +Records, rich enums, maps, and the lists and optionals that don't cross +directly are one serialized [value buffer](../reference/value-buffers.md). +The helper header renders each record as a struct (from the `kitchen_sink` +fixture): ```c struct kitchen_sink_kitchen_Item { @@ -130,8 +186,11 @@ struct kitchen_sink_kitchen_Item { ``` A rich enum is a `tag` (its `{p}_{path}_{Enum}_Tag` constant) plus a union -`as` with one struct per variant that has fields. Each of these types, and -each optional shape, gets four `static inline` functions: +`as` with one struct per variant that has fields. Inside a record, an +optional scalar is a pointer (`int64_t* parent`) and a numeric list is a +`{p}_list_{T}` struct: the direct OptDirect and Slice slots apply only at a +call boundary. Each of these types, and each optional shape, gets four +`static inline` functions: ```c static inline void kitchen_sink_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v); @@ -148,17 +207,20 @@ the decoded value when you're done: size_t len = 0; const uint8_t* buf = kitchen_sink_kitchen_maybe_item(7, &len, &err); kitchen_sink_kitchen_Item* item = NULL; -if (kitchen_sink_opt_kitchen_Item_decode(buf, len, &item)) { +if (kitchen_sink_opt_Item_decode(buf, len, &item)) { /* item is NULL when the producer returned none */ } kitchen_sink_free_bytes((uint8_t*)buf, len); -kitchen_sink_opt_kitchen_Item_free(&item); +kitchen_sink_opt_Item_free(&item); ``` -An optional's `_write` takes the nullable pointer itself. A value you build -for writing may borrow your own memory (`{p}_str_of("text")` borrows a C -string); only values produced by `_read` or `_decode` are passed to `_free`. -`_decode` rejects malformed or trailing bytes, frees what it built, and +A list, map, or optional codec is named by the shape's canonical stem, the +same in every target: `{p}_list_string`, `{p}_opt_Item`, +`{p}_map_string_list_i64`. An optional's `_write` takes the nullable pointer +itself. A value you build for writing may borrow your own memory +(`{p}_str_of("text")` borrows a C string); only values produced by `_read` +or `_decode` are passed to `_free`. `_decode` rejects malformed or trailing +bytes and strings that aren't well-formed UTF-8, frees what it built, and returns `false`. ## Objects and lifetime @@ -173,14 +235,43 @@ helper's `_write` stores a fresh `_clone`, `_read` adopts the token, and ## Errors -Every synchronous function takes a trailing `{p}_error* out_err`. On -failure `code` is nonzero and `message` is NUL-terminated UTF-8; release -both with `{p}_error_clear`. Positive codes are the module's error domain -(`kvstore_kv_KvError_KeyNotFound`); negative codes are runtime traps: `-1` -generic (or an async call that couldn't start), `-2` panic, `-3` -marshalling failure, `-4` a callback implementation failed, and `-5` -cancelled. An error code with fields carries -them in `payload_ptr`/`payload_len`, which the helper decodes with +Every synchronous function takes a trailing `{p}_error* out_err`: + +```c +typedef struct kitchen_sink_error { + int32_t code; + const uint8_t* message_ptr; + size_t message_len; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; +``` + +On failure `code` is nonzero and the message is `message_len` bytes of +UTF-8 at `message_ptr`, **not** NUL-terminated (print it with +`printf("%.*s", (int)err.message_len, (const char*)err.message_ptr)`); +release the message and payload with `{p}_error_clear`. Negative codes are +runtime failures: `-1` an untyped error (a `throws: any` function, or an +async call that couldn't start), `-2` panic, `-3` marshalling failure, `-4` a +callback implementation failed, and `-5` cancelled. Positive codes belong to +the error domain the function throws, which its doc comment names: + +```c +/** + * Fails with the second domain + * + * Fails with a `kitchen_sink_kitchen_PantryError` code (and its fields as a + * payload when it declares any). + */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_take_from_pantry(const uint8_t* item_ptr, size_t item_len, kitchen_sink_error* out_err); +``` + +Each domain is an `int32_t` type with its code constants +(`kitchen_sink_kitchen_PantryError_OutOfStock`). Domains are open: a newer +library may report a positive code your header doesn't list, so treat an +unknown positive code as a failure of the domain with that code and +message. An error code with fields carries them in +`payload_ptr`/`payload_len`, which the helper decodes with `{code constant}_payload_decode`. ## Async and cancellation @@ -194,10 +285,12 @@ KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_p ``` `err` is `NULL` on success; otherwise it's heap-allocated and the consumer -releases it with `{p}_error_free`. A string, bytes, or buffer result is -owned by the consumer (release it with `{p}_free_bytes`), and an object -result carries one reference. Inputs are copied before the launcher -returns. +releases it with `{p}_error_free`. Result slots follow the result's family: +`bool has_result, T result` for an optional scalar, `const T* result_ptr, +size_t result_len` for a typed array, `const uint8_t* result_ptr, size_t +result_len` for a string, bytes, or buffer. A run is owned by the consumer +(release it with `{p}_free_bytes`), and an object result carries one +reference. Inputs are copied before the launcher returns. A `cancellable` function takes a `{p}_cancel_token*` (or `NULL`). Create one with `{p}_cancel_token_create`, cancel it with `{p}_cancel_token_cancel`, @@ -220,7 +313,12 @@ typedef struct kitchen_sink_kitchen_ReadyListener_vtable { void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); /** Receives the item itself and says whether to keep listening */ bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); - /** A display label, failing with a kitchen error */ + /** + * A display label, failing with a kitchen error + * + * May fail with a `kitchen_sink_kitchen_KitchenErrors` code (attaching its + * fields with `kitchen_sink_error_set_payload` when it declares any). + */ void (*label)(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); /** The most recent item the listener kept, if any */ void (*latest)(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err); @@ -230,9 +328,10 @@ typedef struct kitchen_sink_kitchen_ReadyListener_vtable { ``` Define one static vtable per interface, with `size` set to `sizeof` the -vtable and `flags` to `0`. The producer rejects a vtable smaller than its -own with `-3` (calling `free(ctx)` first), so a consumer built from an older -header can't make it call a missing entry: +vtable and `flags` to `0` (or `{P}_VTABLE_THREAD_AFFINE`; see below). The +producer rejects a vtable smaller than its own with `-3` (calling +`free(ctx)` first), so a consumer built from an older header can't make it +call a missing entry: ```c static const kitchen_sink_kitchen_ReadyListener_vtable LISTENER = { @@ -249,32 +348,43 @@ static const kitchen_sink_kitchen_ReadyListener_vtable LISTENER = { The producer may call any entry from any thread, so the context must be thread-safe, and it calls `free(ctx)` exactly once, from any thread, when it -drops its last reference. For an optional parameter (`ReadyListener?`), pass -a `NULL` vtable for none. +drops its last reference. A consumer whose methods can only run on one +thread sets `flags` to `{P}_VTABLE_THREAD_AFFINE`: the producer then calls +the methods that return a value only on the thread that passed the vtable, +failing a call from any other thread with `-4` instead. For an optional +parameter (`ReadyListener?`), pass a `NULL` vtable for none. String and buffer arguments are borrowed for the call; an object argument carries one reference the method adopts. Returns go the other way: - A direct value is the C return. +- An optional scalar is a `bool` C return (present) with the value written + to the trailing `out_value` slot. - An object (`favorite`) is the C return, one strong reference the producer adopts: return a fresh `_clone`, never `NULL` for a non-optional type. - A string, bytes, or buffer (`label`, `latest`) is written to the trailing `out_ptr` and `out_len` slots as a run allocated with `{p}_alloc`, which the producer adopts and frees. Leave them `NULL` and `0` for an empty value. +- A typed array is the same, with `out_len` counting elements and the run + allocated with `{p}_alloc(len * sizeof(T))`. -A method reports failure with `{p}_error_set(out_err, code, "message")`, -which copies the message. A method declared `throws` (`label`) may use a -code of its module's error domain and attach the code's fields, encoded as a -value buffer, with `{p}_error_set_payload(out_err, ptr, len)`; any other -failure reaches the producer as `-4`: +A method reports failure with `{p}_error_set(out_err, code, message_ptr, +message_len)`, which copies the message. A method declared `throws: Domain` +(`label`) may use a code of that domain and attach the code's fields, +encoded as a value buffer, with `{p}_error_set_payload(out_err, ptr, len)`, +and the producer receives the typed error. Any other failure is reported as +`-1` with a message (`-4` from a method that declares no errors) and reaches +the producer as a callback failure (`-4`): ```c static void listener_label(void* ctx, uint8_t** out_ptr, size_t* out_len, kitchen_sink_error* out_err) { const char* label = ((listener*)ctx)->label; if (label == NULL) { - kitchen_sink_error_set(out_err, kitchen_sink_kitchen_KitchenErrors_NotFound, "no label"); + static const char msg[] = "no label"; + kitchen_sink_error_set(out_err, kitchen_sink_kitchen_KitchenErrors_NotFound, + (const uint8_t*)msg, sizeof msg - 1); return; } size_t n = strlen(label); @@ -299,8 +409,10 @@ KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_s KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); ``` -String, bytes, and buffer items are owned (release each with -`{p}_free_bytes`); object items carry one reference. +An optional scalar item is written to `bool* out_has_item, T* out_item`, a +typed-array item to `T** out_item, size_t* out_len`. String, bytes, buffer, +and typed-array items are owned (release each with `{p}_free_bytes`); +object items carry one reference. ## Implementing an IDL in C @@ -317,7 +429,7 @@ const calculator_contract_entry* calculator_calculator_contract(size_t* out_len) return table; } -void calculator_error_set(calculator_error* err, int32_t code, const char* message); +void calculator_error_set(calculator_error* err, int32_t code, const uint8_t* message_ptr, size_t message_len); void calculator_error_set_payload(calculator_error* err, const uint8_t* ptr, size_t len); void calculator_error_clear(calculator_error* err); void calculator_error_free(calculator_error* err); @@ -330,9 +442,10 @@ void calculator_cancel_token_destroy(calculator_cancel_token* token); uint64_t calculator_debug_live(int32_t kind); /* may return 0 for every kind */ ``` -`{p}_alloc` returns a zero-filled run (`NULL` for `0`), and `{p}_free_bytes` -must release both the runs the producer returns and the runs `{p}_alloc` -hands out, so both use one allocator. `{p}_debug_live(-1)` returns `1` when +`{p}_alloc` returns a zero-filled, 8-aligned run (`NULL` for `0`), and +`{p}_free_bytes` must release both the runs the producer returns and the +runs `{p}_alloc` hands out, so both use one allocator. A producer that +accepts callback interfaces honors `{P}_VTABLE_THREAD_AFFINE`. `{p}_debug_live(-1)` returns `1` when the producer counts live resources and `0` when it doesn't. Every prototype carries `{PREFIX}_API`, so the definitions stay exported under `-fvisibility=hidden`; on Windows, define `{PREFIX}_BUILD` while building @@ -341,6 +454,9 @@ the producer. `conformance/c/producer.c` is a complete example. ## Known limitations - The C target emits declarations and helpers only; there's no runtime - loader, so `{PREFIX}_LIBRARY` isn't consulted. -- The helper's decoders don't validate UTF-8 in decoded strings; the - producer has already done so. + loader, so `{PREFIX}_LIBRARY` isn't consulted, and the load-time checks + are calls the consumer makes itself. +- Nothing maps an error code to a type: a C consumer switches on `code` + itself, including the unknown-positive-code case. +- The helper's writers don't validate the UTF-8 of strings you encode; the + producer rejects a malformed one with `-3`. diff --git a/docs/src/generators/cpp.md b/docs/src/generators/cpp.md index e5cdeb11..76581fc7 100644 --- a/docs/src/generators/cpp.md +++ b/docs/src/generators/cpp.md @@ -1,11 +1,17 @@ # C++ -The C++ target emits a header-only C++17 library over the C ABI (revision -4). Records become value structs, rich enums become `std::variant`-backed -sum types, interfaces become copyable RAII classes, callback interfaces -become abstract classes you subclass, error domains become exception -hierarchies, async functions return `std::future`, and `iter` returns a -lazy range. +The C++ target emits a header-only C++17 library over the +[C ABI](../reference/abi.md) (revision 5). Records become aggregates with +memberwise equality, rich enums become `std::variant`-backed sum types, +interfaces become copyable RAII classes, callback interfaces become abstract +classes you subclass, error domains become exception hierarchies rooted in +one `Error` class, async functions return `std::future`, and `iter` +returns a lazy `Range`. + +C++ is a [Tier 1](../stability.md#target-tiers) target: it tracks every ABI +revision as it lands and runs the full conformance suite in CI. The header +compiles warning-free with `-Wall -Wextra -Werror` as C++17 (with +`-Wpedantic`) and as C++20. ## What gets generated @@ -21,7 +27,8 @@ For the kvstore sample that's `kvstore.h`, `kvstore.hpp`, `CMakeLists.txt`, and `README.md`. Everything in the wrapper lives in `namespace {prefix}` (`kvstore`), and each IDL module's free functions live in a nested namespace named after the module path (`kvstore::kv::stats::summarize`). Types sit at -the namespace root, since type names are unique across an API. +the namespace root, since type names are unique across an API. Machinery the +wrapper uses internally lives in `{namespace}::detail`. | `[generators.cpp]` key | Default | Meaning | |---|---|---| @@ -57,37 +64,34 @@ README, and a `CMakeLists.txt` that links the host's library into ### The library check `check_library()` verifies that the linked library implements C ABI revision -4 and every declaration the header was generated with, and throws +5 and every declaration the header was generated with, and throws `LoadError` naming the first mismatch. The header embeds each top-level -module's contract table as `(id, hash, path)` entries and compares them with -the table the library returns from `{prefix}_{module}_contract`: +module's contract table as `(id, hash, path)` entries, with the canonical +signature as a comment, and compares them with the table the library returns +from `{prefix}_{module}_contract`: ```cpp -inline void check_library() { - static const std::string failure = []() -> std::string { - uint32_t abi = kvstore_abi_version(); - if (abi != KVSTORE_ABI_VERSION) { - return "kvstore: C ABI revision mismatch (header " + std::to_string(KVSTORE_ABI_VERSION) + - ", library " + std::to_string(abi) + ")"; - } - if (std::string why = detail::contract_mismatch(kvstore_kv_contract, detail::kv_contract); !why.empty()) { - return "kvstore: " + why; - } - ... +inline constexpr ContractEntry kv_contract[] = { + {0x1bac41daa51c4fd5ull, 0x828679ef84968913ull, "kv.KvError.CallbackFailed"}, // code CallbackFailed = 1006 {string} + ... +}; ``` A declaration the library lacks fails with `kvstore: kv.Store.put is missing from the library`, and one whose signature differs with -`kvstore: kv.Store.put changed since these bindings were generated`. -Declarations the library has and the header doesn't are fine, so a library -that adds functions keeps working with older bindings. +`kvstore: kv.Store.put changed since these bindings were generated`. The +table has one entry per function and member, type, error domain, error code, +callback interface, and callback method. Entries the library has and the +header doesn't are fine, so a library that adds functions, error codes, or +callback methods keeps working with older bindings. A C++ binary links its library rather than loading it, and an exception thrown during static initialization can't be caught, so the check runs on first use instead: every free function, constructor, and static member calls `check_library()` before its first native call. The result is cached in a function-local static, so the cost after the first call is one -guard-variable read. Call it at startup to fail early. +guard-variable read. Call it at startup to fail early; `LoadError` derives +from `Error` (with code 0), so one `catch` handles it with everything else. ## Type mapping @@ -97,37 +101,63 @@ guard-variable read. Call it at startup to fail early. | `string` | `std::string` | `std::string_view` | | `bytes` | `std::vector` | `const std::vector&` | | C-style enum | `enum class E : int32_t` | by value | -| record | `struct R` | `const R&` | +| record | `struct R` (aggregate) | `const R&` | | rich enum | `struct E` with `std::variant<...> value` | `const E&` | | interface | RAII class | `const I&` (borrowed) | | `I?` | `std::optional` | `const std::optional&` | | callback interface, `Cb?` | abstract class | `std::shared_ptr` (empty for none when optional) | -| `T?`, `[T]`, `{K: V}` | `std::optional`, `std::vector`, `std::unordered_map` | by const reference | -| `iter` | lazy range class (returns only) | n/a | - -Strings cross the ABI as a UTF-8 pointer and a length, so interior NULs -survive in both directions. A returned string is copied into a `std::string` -and the producer's run is released with `{prefix}_free_bytes`. Records, rich -enums, optionals, lists, and maps cross as one value buffer. The header has -one writer and one reader per record, rich enum, and distinct composite type -(`detail::write_list_Entry`, `detail::read_map_string_Store`), and every call -site delegates to them. +| `T?` of a scalar, `bool`, or C-style enum | `std::optional` | by value | +| `[P]` of a numeric primitive | `std::vector

` | `const std::vector

&` | +| other `T?`, `[T]`, `{K: V}` | `std::optional`, `std::vector`, `std::unordered_map` | by const reference | +| `iter` | `Range` (returns only) | n/a | + +How a value crosses the ABI follows its type: + +- Strings cross as a UTF-8 pointer and a length, so interior NULs survive in + both directions. A returned string is copied into a `std::string` and the + producer's run is released with `{prefix}_free_bytes`. +- An optional scalar, `bool`, or C-style enum (`i64?`, `Mode?`) crosses + directly as a presence flag and a value, never through a buffer. +- A list of a numeric primitive other than `u8` (`[i32]`, `[f64]`) crosses + as a typed array: the wrapper passes the vector's own storage (`.data()`, + `.size()`) without copying, and a returned array is copied into a new + `std::vector` before the producer's run is released. +- Records, rich enums, and every other optional, list, and map cross as one + value buffer. The header has one `detail::write` and one `detail::read` + overload per record and rich enum; optionals, vectors, and maps are encoded + by generic templates in the runtime, so a new composite shape never adds + code. + +Records are aggregates: build one with braces (`Entry{"key", value}`), and +every member not given takes its default member initializer (zero for +scalars and enums, empty otherwise), so a partial initializer compiles +without `-Wmissing-field-initializers` warnings. An interface wrapper has no +empty state, so a member holding one by value (or holding a record that +does) has no default, and a record with such a member has no default +constructor. Records and rich enums +compare with `==` and `!=` memberwise: floating-point members as IEEE values +(a NaN member is unequal to itself), and interface members by identity. ## Objects and lifetime An interface class holds one strong reference to a reference-counted -producer object: +producer object through a `detail::Handle`, so the class itself follows the +Rule of Zero: ```cpp class Store { - kvstore_kv_Store* raw_; - public: - /** The C type this class wraps. */ using raw_type = kvstore_kv_Store; - - /** Adopts one strong reference to a producer object: `Store(adopt, raw)`. */ - explicit Store(adopt_t, kvstore_kv_Store* h) noexcept : raw_(h) {} + explicit Store(adopt_t, raw_type* raw) noexcept : raw_(raw) {} + ... +private: + struct traits { + using raw_type = kvstore_kv_Store; + static raw_type* clone(const raw_type* raw) noexcept { return kvstore_kv_Store_clone(raw); } + static void destroy(raw_type* raw) noexcept { kvstore_kv_Store_destroy(raw); } + }; + detail::Handle raw_; +}; ``` - The destructor releases the reference with `_destroy`; copying takes a new @@ -141,10 +171,11 @@ public: handed to a callback are adopted. - An object inside a record, list, map, or optional crosses as a token minted with `_clone`, so a record can be copied and destroyed freely. -- `handle()` borrows the C pointer (compare two wrappers' handles to test - identity); `clone_handle()` returns a new reference the caller owns. An - interface member named `handle`, `clone_handle`, or `raw_type` gains a - trailing `_` ([reserved member names](../reference/naming.md#identifiers-in-generated-code)). +- `==` and `!=` compare identity: two wrappers are equal when they share one + producer object. `handle()` borrows the C pointer; `clone_handle()` + returns a new reference the caller owns. An interface member named + `handle`, `clone_handle`, `raw_type`, or `traits` gains a trailing `_` + ([reserved member names](../reference/naming.md#identifiers-in-generated-code)). The C++ object model makes lifetimes the caller's responsibility: a wrapper must outlive the calls made through it, and destroying a wrapper on one @@ -153,29 +184,38 @@ object. A moved-from wrapper holds null. ## Errors -A call that declares errors (`throws`) throws an exception derived from -`{namespace}::Error`, which derives from `std::runtime_error` and carries -`code()`: - -- Each module that declares an error domain gets a domain class - (`KvError : Error`) and one subclass per code (`KeyNotFoundError`). Both - names end in `Error`, which is appended when the IDL name lacks it (the - `KitchenErrors` domain becomes `KitchenErrorsError`). A code with payload - fields exposes them as public members (`e.key`). -- A positive code throws its typed subclass. A negative runtime code - throws the root `Error` with that code: -1 generic, -2 producer panic, -3 - marshalling failure, and -4 a callback implementation that failed. -- -5 throws `Cancelled`, which also derives from `Error`. - -A call that declares no errors can still fail, but only through a producer -bug, so it follows the [trap policy](../guides/errors-and-memory.md#the-trap-policy): -it throws `InternalError`, a `std::runtime_error` that isn't an `Error`, -whose `what()` names the runtime code and the producer's message and whose -`code()` and `message()` return them. A value buffer the wrapper can't -decode is also an `InternalError` with code -3. - -`LoadError` (also a `std::runtime_error`) reports a library that doesn't -match the headers. Wrappers are never `noexcept`. +Every exception the library throws derives from `{namespace}::Error`, which +derives from `std::runtime_error` and carries `code()`. Which class a failed +call throws depends on what it declares: + +- `throws: SomeDomain`: each error domain gets a class (`KvError : Error`) + and one subclass per code (`KeyNotFoundError : KvError`). The names come + from the shared naming rule, which appends `Error` unless the IDL name + already ends in it (the `KitchenErrors` domain is `KitchenError`). A code + with payload fields exposes them as public members (`e.key`); a field + named `code` or `what` gains a trailing `_` so it doesn't hide `code()` or + `what()`. A declared code throws its class. Domains are open: a positive + code these bindings don't know (a newer producer added it) throws the + domain class itself, with the code and message intact. A module may + declare several domains, and each call throws only its own, so two domains + can reuse a code value. +- `throws: any`: a failure throws `Error` itself, with code -1 and the + producer's message. +- Nothing declared: the call can still fail, but only through a producer + bug, so it follows the + [trap policy](../guides/errors-and-memory.md#the-trap-policy) and throws + `InternalError` (an `Error`) with the runtime code and message. + +On a call that declares errors, a runtime code throws the root `Error` with +that code (-2 producer panic, -3 marshalling failure, -4 a callback +implementation that failed); under every policy, -5 throws `Cancelled`. A value +buffer the wrapper can't decode is an `InternalError` with code -3, and a +required callback interface passed as an empty `std::shared_ptr` is an +`Error` with code -3, refused before the call. `LoadError` reports a library +that doesn't match the headers. Wrappers are never `noexcept`. + +Error messages cross the ABI as a pointer and a length, so they keep any byte +the producer sent; an empty message is `""`. ```cpp try { @@ -183,7 +223,9 @@ try { } catch (const kvstore::KeyNotFoundError& e) { std::cerr << e.what() << " (" << e.key << ")\n"; } catch (const kvstore::KvError& e) { - // Any other kv code. + // Any other kv code, known or not. +} catch (const kvstore::Error& e) { + // A runtime failure (e.code() < 0). } ``` @@ -214,8 +256,10 @@ try { } ``` -`std::future` has no cancellation of its own, so destroying a future doesn't -cancel the call; cancel its token instead. +An async result crosses like a return of its type: an optional scalar as a +flag and a value, a typed array as a pointer and a count. `std::future` has +no cancellation of its own, so destroying a future doesn't cancel the call; +cancel its token instead. ## Callback interfaces @@ -227,54 +271,62 @@ class Policy { public: virtual ~Policy() = default; + virtual std::optional ttl_for(std::string_view key, std::optional requested) = 0; virtual Entry admit(const Entry& entry) = 0; virtual Store route(std::string_view key, Store home) = 0; }; ``` Pass an implementation as `std::shared_ptr`. The wrapper boxes the pointer -as the vtable context and hands the producer a static vtable whose header -records its `size` and whose `free` deletes the box when the producer -releases the implementation, which runs your destructor if nothing else -holds it. For an optional callback parameter (`Loader?`), an empty -`std::shared_ptr` (or `nullptr`) passes none; a required one throws -`std::invalid_argument` when empty. The producer may call methods, and -release the implementation, from any thread, so implementations must be -thread-safe. +as the vtable context and hands the producer a static vtable (from +`detail::Callbacks`) whose header records its `size` and whose `free` +deletes the box when the producer releases the implementation, which runs +your destructor if nothing else holds it. For an optional callback parameter +(`Loader?`), an empty `std::shared_ptr` (or `nullptr`) passes none. The +producer may call methods, and release the implementation, from any thread, +so implementations must be thread-safe; the vtable never sets the +thread-affine flag. Arguments follow the parameter rules seen from your side: a string is a -`std::string_view` valid for the call, bytes and buffered values arrive by -const reference to a decoded copy, and an object arrives by value as a -wrapper you own. - -A method can return any family. A direct value is returned as is; an object -(`Store`, `std::optional`) hands the producer a fresh reference; a -string, bytes, or buffered value is copied into a run allocated with -`{prefix}_alloc`, which the producer adopts. A required object return that's -null (a moved-from wrapper) is refused by the producer with -3, as is a -record the producer can't decode. - -A method declared `throws` may throw its module's domain exception: the -code, the message (`what()`), and the code's fields reach the producer, -which decodes them into its own error type. +`std::string_view` valid for the call, a typed array, bytes, or a buffered +value arrives by const reference to a copy, an optional scalar as +`std::optional`, and an object by value as a wrapper you own. + +A method can return any type. A scalar is returned as is and an optional +scalar as a flag and a value; an object (`Store`, `std::optional`) +hands the producer a fresh reference; a string, bytes, typed array, or +buffered value is copied into a run allocated with `{prefix}_alloc`, which +the producer adopts. A required object return that's null (a moved-from +wrapper) and a value the producer can't decode are refused by the producer +with -3. + +Each method reports an exception according to what it declares, and nothing +unwinds through the C frame: + +- `throws: SomeDomain`: throw the domain's class or one of its codes' + classes. The code, the message (`what()`), and the code's fields reach the + producer, which decodes them into its own error type. Any other exception + is a plain failure (code -1 with its `what()`). +- `throws: any`: any exception is a failure, code -1 with its `what()`. +- Nothing declared: any exception is a callback failure, -4. ```cpp Entry admit(const Entry& entry) override { - if (entry.key == "secret") throw kvstore::RejectedError("secrets are not stored", entry.key, "no secrets"); + if (entry.key == "secret") throw kvstore::RejectedError("not stored", entry.key, "no secrets"); return entry; } ``` -Any other exception, or a domain exception from a method that doesn't -declare errors, reaches the producer as a callback failure (-4) with the -exception's `what()`; nothing unwinds through the C frame. Whether the -original call then fails is the producer's decision: a Rust producer that -propagates the failure with `?` makes a throwing call throw the same -`RejectedError` (or the root `Error` with code -4). +Whether the original call then fails is the producer's decision. A Rust +producer that propagates a typed failure with `?` makes the call throw the +same `RejectedError`, with the message the producer renders for it; the +kvstore sample turns any other callback failure into `CallbackFailedError` +carrying your exception's message. ## Iterators -An `iter` function returns a move-only range class: +An `iter` function returns `Range`, a lazy, single-pass, move-only +range: ```cpp for (const std::string& key : store.keys(std::nullopt)) { @@ -282,16 +334,25 @@ for (const std::string& key : store.keys(std::nullopt)) { } ``` -Each step pulls one element. The range releases the producer iterator when -it's exhausted or destroyed, so abandoning a loop early leaks nothing. -`next()` returns `std::optional` for manual iteration. +Each step pulls one element; an element crosses like a return of its type, +so `iter` yields `std::optional` (an absent element isn't the +end) and `iter<[i32]>` yields `std::vector`. The range releases the +producer iterator exactly once: when it's exhausted, when a step throws, on +`close()`, or from the destructor, so abandoning a loop early leaks nothing. +`next()` returns `std::optional` for manual iteration. The iterator type +models an input iterator with a sentinel end, so under C++20 a `Range` is +a `std::ranges::input_range`. ## Known limitations -- The ranges are single-pass input ranges; copy them into a `std::vector` - for random access. +- Ranges are single-pass input ranges; copy them into a `std::vector` for + random access. +- A typed array argument is a `const std::vector&`; C++17 has no + `std::span`, so other contiguous storage must be copied into a vector first. - A callback interface must be a `std::shared_ptr` to a subclass; there's no lambda overload. +- C++ implementations don't declare their vtables thread-affine, so the + producer may call any method from any thread. - Exceptions not derived from `std::exception` are reported to the producer with a generic message. - The library check runs on first use, not at load time (see above). diff --git a/docs/src/generators/dart.md b/docs/src/generators/dart.md index 9c3746a7..593d4805 100644 --- a/docs/src/generators/dart.md +++ b/docs/src/generators/dart.md @@ -1,25 +1,34 @@ # Dart The Dart target generates a standalone Dart package that calls the -producer's C ABI (revision 4) through `dart:ffi`. It works in Dart +producer's C ABI (revision 5) through `dart:ffi`. It works in Dart command-line apps and in Flutter apps on the desktop, Android, and iOS. Records become value classes, rich enums sealed class hierarchies, interfaces disposable wrapper classes, async functions `Future`s, and callback interfaces abstract classes you implement. +Dart is a [Tier 2](../stability.md#target-tiers) target: it may lag behind +new ABI work, but it's held to the same conformance suite as every other +target. + ## What gets generated ```text dart/ - pubspec.yaml package `{package}`, depends on `ffi` + pubspec.yaml package `{package}`, depends on `ffi` README.md - lib/{package}.dart the whole binding: runtime, types, and functions + lib/{package}.dart the library: its imports and parts + lib/src/runtime/*.dart the runtime (loader, errors, codecs, ...) and contract tables + lib/src/{module}.dart one part per module (`kv.dart`, `kv/stats.dart`) ``` -The package name defaults to the library's C prefix (`kvstore` for the -kvstore sample), so consumers write `import 'package:kvstore/kvstore.dart';`. -Module-level functions use their bare names (`openStore`, `renderReport`), -which global names keep unique. +The whole binding is one Dart library split into parts, so you import a +single file. The package name defaults to the library's C prefix +(`kvstore` for the kvstore sample), so consumers write +`import 'package:kvstore/kvstore.dart';`. Module-level functions use their +bare names (`openStore`, `renderReport`), which global names keep unique. +Runtime sections the API doesn't use (async, callbacks, typed arrays, ...) +aren't emitted. ## Configuration @@ -31,8 +40,8 @@ sdk = ">=3.10.0 <4.0.0" # the pubspec's SDK constraint (this is the default) The package needs Dart 3.10 or newer, the first release with `NativeCallable.isolateGroupBound`, which delivers void callback methods from -any thread (see [Threading](#threading)), and the `ffi` package, which -supplies `Arena` and `calloc`. +any thread (see [Threading](#threading)), and the `ffi` package (2.1 or +newer), which supplies `Arena` and `calloc`. ## Install, build, and load @@ -64,16 +73,23 @@ all of them. See [Packaging](../guides/packaging.md). ### Load-time checks Before any other call, the bindings check the library and throw a -`NativeLibraryError` (an `Error`) if it doesn't match: +`NativeLibraryException` if it can't be opened or doesn't match: -- `kvstore_abi_version()` must report revision 4. +- `kvstore_abi_version()` must report revision 5. - For every top-level module, the bindings embed the contract entries they - were generated with (one `(id, hash, path)` record per declaration), call - `kvstore_{module}_contract`, and require each entry to be present with an - equal hash. A missing entry fails with "kvstore: kv.Store.put is missing - from the library"; a different hash with "kvstore: kv.Store.put changed - since these bindings were generated". Declarations the library has and the bindings don't are - fine, so adding a function never breaks a deployed binding. + were generated with (one `(id, hash, path)` record per declaration, error + code, and callback method, each with its canonical signature as a + comment), call `kvstore_{module}_contract`, and require each entry to be + present with an equal hash. A missing entry fails with "kvstore: + kv.Store.put is missing from the library"; a different hash with + "kvstore: kv.Store.put changed since these bindings were generated". + Entries the library has and the bindings don't are fine, so adding a + function, an error code, or a callback method never breaks a deployed + binding. + +`NativeLibraryException` is an `Exception`, thrown from whichever call into +the library comes first, so an app can catch it and report a broken install. +The load isn't cached when it fails: the next call tries again. ## Type mapping @@ -83,28 +99,44 @@ Before any other call, the bindings check the library and throw a | `f32`, `f64` | `double` | by value | | `bool` | `bool` | C `bool` | | `string` | `String` | UTF-8 `(ptr, len)`, so interior NULs survive | -| `bytes` | `List` (a `Uint8List` when returned) | `(ptr, len)` | +| `bytes` | `Uint8List`; a parameter takes any `List` | `(ptr, len)` | | Record | a `final class` with final fields, named constructor arguments, and value equality | value buffer | | Plain enum | an enhanced `enum` with `value` and `fromValue` | `int32_t` | | Rich enum | a `sealed` class with one `final` subclass per variant | value buffer | | Interface | a `final class` wrapper | object pointer | -| `T?` | `T?` | value buffer (`Interface?` is a nullable pointer, `Cb?` a null vtable) | -| `[T]`, `{K:V}` | `List`, `Map` | value buffer | +| `T?` of a scalar or plain enum | `T?` | a presence flag and the value (no buffer) | +| `[T]` of a number (`i8` to `i64`, `u16` to `u64`, `f32`, `f64`) | `List` or `List` | a C array and its element count (no buffer) | +| Any other `T?` | `T?` | value buffer (`Interface?` is a nullable pointer, `Cb?` a null vtable) | +| Any other `[T]`, `{K:V}` | `List`, `Map` | value buffer | | `iter` | `Iterable` | native iterator | | Callback interface | an `abstract class` you implement | `ctx` plus vtable | +A numeric list argument is copied into native memory for the call (a typed +list such as `Int32List` is copied as is; any other list is converted +first). A returned numeric list is a fixed-length typed list behind the +`List` or `List` type (`Int32List`, `Float64List`, and so on), +copied out of the producer's array before it's released. A `bytes` value +the bindings return, decode, or pass to a callback is always a `Uint8List`; +a `bytes` parameter (or a callback's `bytes` return) takes any `List`, +so a list literal works too. Inside records and other value buffers, +optionals and lists keep the value-buffer encoding. + Records and rich-enum variants implement `==`, `hashCode`, and `toString`: lists, maps, and byte arrays compare element by element, and interface wrappers by identity. Wrapper functions and members use lowerCamelCase; types use UpperCamelCase. A name that collides with a Dart keyword or a type -the bindings use gains a trailing `_` (`class_`, `String_`), as does an -interface member named like a member every wrapper declares or inherits -(`dispose`, `hashCode`, `toString`, `runtimeType`, `noSuchMethod`): a method -`dispose` is `dispose_()`. See [Naming](../reference/naming.md#identifiers-in-generated-code). - -Each distinct optional, list, or map type gets one private codec pair -(`_packListOfEntry`, `_unpackMapOfStringToString`), built on the runtime's -generic `writeList`/`readList` helpers, so no call site inlines a loop. +the bindings use gains a trailing `_` (`class_`, `String_`, `Int32List_`), as +does an interface member named like a member every wrapper declares or +inherits (`dispose`, `hashCode`, `toString`, `runtimeType`, `noSuchMethod`): +a method `dispose` is `dispose_()`. A record field named like one of those +`Object` members, and an error-code field named `code` or `message`, gains +the `_` too (kvstore's `CallbackFailed { message }` has a `message_` +field). See [Naming](../reference/naming.md#identifiers-in-generated-code). + +Each distinct optional, list, or map type that crosses inside a value buffer +gets one private codec pair named by its canonical shape (`_pack_list_Entry`, +`_unpack_map_string_string`), built on the runtime's generic +`writeList`/`readList` helpers, so no call site inlines a loop. ## Objects and lifetime @@ -131,33 +163,47 @@ element) is encoded as a fresh reference, so the wrapper stays usable. ## Errors -A module's error domain becomes a sealed exception class, extending -`NativeException`, with one final subclass per code. Payload fields are typed -properties, and the code's documented message is the default for the -optional trailing `message` argument: +Each error domain becomes an exception class extending `NativeException`, +named by the shared rule (`KvError` is `KvException`, `KitchenErrors` is +`KitchenException`), with one final subclass per code (`KeyNotFound` is +`KeyNotFoundException`). Payload fields are typed properties, and the +code's documented message is the default for the optional trailing +`message` argument: ```dart try { store.get('missing'); } on KeyNotFoundException catch (e) { print('${e.code}: ${e.message} (${e.key})'); // 1001: key not found: missing (missing) -} on KvException { - // any other KvError code +} on KvException catch (e) { + // any other KvError code, including ones these bindings don't know } ``` -Only functions declared `throws` raise exceptions. A positive code becomes -its domain subclass; a negative runtime code becomes a plain -`NativeException` with that code. The runtime codes are constants on +A module may declare several domains, and a function names the one it +throws; codes are only unique within a domain, so the bindings decode a +failure by the function's domain (calculator's `CalcError.DivisionByZero` +and `ParseError.NotANumber` are both code 1). + +Domains are open. A positive code the bindings don't declare (from a newer +library that added a code) arrives as the domain class itself +(`KvException`) with its code and message, never as a crash, so catch the +domain class to stay forward compatible. A negative runtime code becomes a +plain `NativeException` with that code. The runtime codes are constants on `NativeException`: `genericCode` (-1), `panicCode` (-2), `marshalCode` (-3), `foreignCode` (-4, a callback implementation failed), and `cancelledCode` (-5, raised as `CancelledException`). -A failure of a function that isn't declared `throws` is a bug in the +A function declared `throws: any` fails with a plain `NativeException` +carrying `genericCode` and the producer's message (kvstore's `importLines` +fails with "line 2: expected key=value"). + +A failure of a function that isn't declared to throw is a bug in the producer, so it follows the [trap policy](../guides/errors-and-memory.md#the-trap-policy): the binding throws a `NativeError`, an `Error` (not an `Exception`) carrying -the runtime `code` and the producer's `message`. Cancellation always -surfaces as `CancelledException`. +the runtime `code` and the producer's `message`. A marshalling failure of +such a function (codec's `echoChar('ab')`) is a `NativeError` with +`marshalCode`. Cancellation always surfaces as `CancelledException`. ## Async and cancellation @@ -192,10 +238,13 @@ parameter (`Policy?`) takes `null` for none, which passes a null vtable. ```dart class AdmitAll implements Policy { + @override + int? ttlFor(String key, int? requested) => requested; + @override Entry admit(Entry entry) { if (entry.key.startsWith('secret')) { - throw RejectedException(entry.key, 'no secrets', 'secrets are not stored'); + throw RejectedException(entry.key, 'no secrets'); } return entry; } @@ -208,49 +257,53 @@ store.setPolicy(AdmitAll()); ``` A method that returns a value hands it to the producer in the shape the ABI -expects: a direct value by value; an object as a fresh strong reference the -producer adopts; and a string, bytes, or record as a run the bindings -allocate with `{prefix}_alloc` and the producer frees. Object arguments are -owned by the implementation; call `dispose()` on them when you're done (or -let the finalizer release them). - -Exceptions never unwind into the producer. In a method declared `throws`, -throwing the module's domain exception (here a `KvException` subclass) +expects: a scalar by value; an optional scalar as a presence flag and an out +value; an object as a fresh strong reference the producer adopts; and a +string, bytes, numeric list, or record as a run the bindings allocate with +`{prefix}_alloc` and the producer frees. Arguments arrive copied (a numeric +list as a typed list), and object arguments are owned by the +implementation; call `dispose()` on them when you're done (or let the +finalizer release them). A method may call back into the library: every +call has its own error and out slots, so a call made from inside a callback +never disturbs the call that's waiting on it. + +Exceptions never unwind into the producer. In a method declared to throw a +domain, throwing that domain's exception (here a `KvException` subclass) reports its code and its fields, encoded as a value buffer, so the producer sees exactly that error: `put` above fails with the same -`RejectedException`. Any other exception, or any exception from a method -that isn't declared `throws`, reports `foreignCode` (-4) with the -exception's text. +`RejectedException` (with the message the producer renders from its +fields). Any other exception, and any exception from a `throws: any` +method, reports `genericCode` (-1) with the exception's text (a +`NativeException`'s `message`); from a method that isn't declared to throw, +it reports `foreignCode` (-4). ### Threading How a method runs depends on its return type: - **A method that returns a value** is a `NativeCallable.isolateLocal` - trampoline. It runs synchronously, so the producer must call it on the - isolate's thread while a call from Dart is in progress, as a policy or a - filter consulted during the call is. + trampoline. It runs synchronously, so it can only run on the isolate's + thread while a call from Dart is in progress, as a policy or a filter + consulted during the call is. - **A void method** is a `NativeCallable.isolateGroupBound` forwarder. The producer may call it from any thread; the forwarder copies its arguments and returns at once, and the method runs later on the event loop, in the zone that passed the instance. An exception there is an uncaught error of that zone, and a void method can't fail the producer's call even when it's - declared `throws`. The vtable's `free` is forwarded the same way, after + declared to throw. The vtable's `free` is forwarded the same way, after every call made before it. -A value-returning method called from any other thread (a producer worker -thread, an async executor) aborts the process. That's a Dart VM rule, not a -choice of these bindings. Only an `isolateLocal` callable can run the -isolate's code synchronously, and the VM aborts before any Dart code runs -when one is called from a thread that isn't running the isolate. The entry -points that accept any thread can't take its place: an `isolateGroupBound` -callable runs outside the isolate, even on the isolate's own thread, so it -can neither run the method nor hand it to the isolate while the isolate is -blocked in the very call that invoked it, and a `listener` returns before -the method runs. So don't give a producer an implementation with -value-returning methods that it calls from its own threads. In the kvstore -sample, a `Listener`'s `accepts` is consulted by `compact` on a worker -thread, so no listener should be subscribed while a compaction runs. +Every vtable the bindings pass is flagged thread-affine +(`{PREFIX}_VTABLE_THREAD_AFFINE`, ABI section 1.7). The producer records the +thread that passed it and refuses to call a value-returning method from any +other thread: the method doesn't run, and the producer sees it fail with +`foreignCode` (-4) and the message "callback called off its thread". Without +the flag, the Dart VM would abort the process, since only an `isolateLocal` +callable can run the isolate's code synchronously and the VM aborts before +any Dart code runs when one is called from a thread that isn't running the +isolate. In the kvstore sample, `compact` tells listeners from a worker +thread: a Dart `Listener`'s `accepts` is refused there, so the listener is +detached, and compaction carries on. ## Iterators @@ -267,19 +320,30 @@ for (final key in store.keys('user.')) { ## Performance notes -Synchronous calls share one error slot and one length slot per isolate, and -stage arguments in a single `Arena` per call. When the API declares no -callback interfaces, synchronous calls are bound as leaf calls (`isLeaf: -true`), which skips the VM's safepoint transition; a leaf call blocks this -isolate group's garbage collector until it returns, so it suits fast -functions. +Every call takes a frame from a per-isolate pool: the native error slot, the +return's out slots, and an `Arena` for staged arguments. A call made from a +callback takes a frame of its own, and a frame goes back to the pool when +its call returns, so steady-state calls allocate no native memory for their +slots. Optional scalars and numeric lists skip the value buffer entirely. +When the API declares no callback interfaces, synchronous calls are bound as +leaf calls (`isLeaf: true`), which skips the VM's safepoint transition; a +leaf call blocks this isolate group's garbage collector until it returns, so +it suits fast functions. ## Known limitations -- A value-returning callback method called from a thread that isn't the - isolate's (a producer worker thread, an async executor) aborts the - process; void methods are safe from any thread (see +- A value-returning callback method can only run on the isolate's thread, + during a call from Dart. A producer that calls one from its own threads + (a worker thread, an async executor) gets a -4 failure instead of a + result; void methods are safe from any thread (see [Threading](#threading)). +- The producer pins a vtable to the OS thread that passed it. The main + isolate of a Dart program, and Flutter's root isolate, stay on one + thread, but the VM may move another isolate (one started with + `Isolate.spawn` or `Isolate.run`) to a different thread between event-loop + turns. In such an isolate, pass an implementation and use it within one + synchronous stretch of code, or expect its value-returning methods to fail + with -4 after an `await`. - Void callback methods are always delivered asynchronously, even when the producer calls them on the isolate's thread, so their exceptions can't fail the producer's call. diff --git a/docs/src/generators/dotnet.md b/docs/src/generators/dotnet.md index f134201b..a474fa8e 100644 --- a/docs/src/generators/dotnet.md +++ b/docs/src/generators/dotnet.md @@ -1,12 +1,16 @@ # .NET The .NET target emits a standalone C# class library over the C ABI -(revision 4). Native calls go through source-generated +(revision 5). Native calls go through source-generated [`[LibraryImport]`](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation) -declarations whose slots are all blittable, and callbacks go through -`[UnmanagedCallersOnly]` function pointers, so the bindings need no runtime -marshalling and are trim- and AOT-friendly. The project targets `net8.0` -and builds with any newer SDK. +declarations whose slots are all blittable, with runtime marshalling +disabled for the assembly (`[assembly: DisableRuntimeMarshalling]`), and +callbacks go through `[UnmanagedCallersOnly]` function pointers, so the +bindings are trim- and AOT-friendly. The project targets `net8.0` and builds +with any newer SDK. + +.NET is a [Tier 1](../stability.md#target-tiers) target: it tracks every ABI +revision as it lands and runs the full conformance suite in CI. ## What gets generated @@ -14,7 +18,7 @@ For a package named `kvstore`: ```text dotnet/ - Kvstore.cs generated API: types, wrappers, codecs, native imports + Kvstore.cs generated API: types, wrappers, native imports, contract Runtime.cs fixed runtime: exceptions, loader, codec, async and iterator helpers Kvstore.csproj SDK-style project with the NuGet metadata README.md @@ -31,7 +35,9 @@ name = "Acme.Storage" Each module becomes a static class named by its full path (`kv` is `Kv`, `kv.stats` is `KvStats`); records, enums, interfaces, callback interfaces, and exceptions sit directly in the namespace, since every type name is -unique across the API. +unique across the API. The project generates an XML documentation file, so +the IDL's docs show up in IntelliSense; backticked identifiers in them +(`` `new_op` ``) are rewritten to their C# spelling (`NewOp`). ## Build and load @@ -45,46 +51,62 @@ dotnet pack bindings/dotnet/Kvstore.csproj -c Release The bindings load the native library by its base name (`kvstore`), so .NET probes `libkvstore.dylib`, `libkvstore.so`, or `kvstore.dll` next to the app and on the platform's library path. Set `KVSTORE_LIBRARY` (the -`{PREFIX}_LIBRARY` variable) to a full path to load a specific file. The -override is a `DllImportResolver` on the generated assembly. +`{PREFIX}_LIBRARY` variable) to a full path to load that file instead (and +only that file). The loader is a `DllImportResolver` on the generated +assembly. `weaveffi package` writes the project to `dotnet/Kvstore/` with each desktop platform's library under `runtimes//native/`, where NuGet resolves it at restore time, and runs `dotnet pack` on it to produce -`dotnet/Kvstore.{version}.nupkg` (so it needs the .NET SDK). See -[Packaging](../guides/packaging.md). +`dotnet/Kvstore.{version}.nupkg` (so it needs the .NET SDK; without it the +package is skipped with a warning). See [Packaging](../guides/packaging.md). ## Load-time checks -The first native call runs `NativeMethods`' static constructor, which -installs the resolver, checks `{prefix}_abi_version()` against 4, and then -checks every top-level module's [contract table](../reference/abi.md#load-time-checks). -The bindings embed the `(id, hash, path)` entry of every declaration they -were generated with: +Every import resolves through the generated resolver, which loads the +library once, checks `{prefix}_abi_version()` against 5, and then checks +every top-level module's +[contract table](../reference/abi.md#load-time-checks). The bindings embed +the `(id, hash, path)` entry of every declaration they were generated with, +including one per error code and one per callback-interface method: ```csharp -static partial void VerifyContracts() +static partial void VerifyContracts(IntPtr library) { - VerifyContract("kvstore_kv_contract", &kvstore_kv_contract, new (ulong, ulong, string)[] + VerifyContract(library, "kvstore_kv_contract", new (ulong, ulong, string)[] { - (0xbe2b91d3c1f42f80UL, 0xf47a03bed8a6a39aUL, "kv.Store.put"), + (0x...UL, 0x...UL, "kv.Store.put"), // method put(string, bytes, EntryKind, i64?) -> Entry throws KvError // ... one entry per declaration in `kv` and its submodules }); - VerifyContract("kvstore_report_contract", &kvstore_report_contract, new (ulong, ulong, string)[] - { - // ... - }); } ``` Each entry must be in the library's table with an equal hash; entries the library has and the bindings don't are fine, so a library that only adds -declarations keeps working with older bindings. A failure throws -`InvalidOperationException` naming the declaration (`kv.Store.put is -missing from the library kvstore` or `kv.Store.put changed since these -bindings were generated`), surfaced as the `InnerException` of a -`TypeInitializationException`, so stale bindings fail before any call can -misread memory. +declarations (new functions, error codes, or callback methods) keeps working +with older bindings. + +A library that can't be found or loaded, or that doesn't match, throws +`NativeLoadException`, a catchable exception naming the problem (`couldn't +find the kvstore native library ...`, `kv.Store.put is missing from the +library kvstore`, `kv.Store.put changed since these bindings were +generated`). The failure is remembered: every later call throws the same +exception, before any call can misread memory. To check at startup instead +of on the first call, call the generated library class: + +```csharp +try +{ + KvstoreLibrary.Check(); +} +catch (NativeLoadException e) +{ + Console.Error.WriteLine($"can't use kvstore: {e.Message}"); +} +``` + +`KvstoreLibrary` (`{Namespace}Library`, from the namespace's last segment) +also exposes `AbiVersion`. ## Type mapping @@ -92,24 +114,60 @@ misread memory. |---|---|---| | `i8` ... `u64`, `f32`, `f64` | `sbyte` ... `ulong`, `float`, `double` | by value | | `bool` | `bool` | one byte | -| `string` | `string` | UTF-8 `(ptr, len)`; interior NULs survive | -| `bytes` | `byte[]` (parameters also take `ReadOnlySpan`) | `(ptr, len)` | +| `string` | `string` | UTF-8 `(ptr, len)`, encoded into a pooled buffer; interior NULs survive | +| `bytes` | `ReadOnlySpan` as a parameter, `byte[]` otherwise | `(ptr, len)`, a parameter pinned in place | | C-style enum | `enum` with the IDL discriminants | `int32_t` | -| record | `sealed class` with get-only properties | value buffer | -| rich enum | `abstract class` with one nested sealed class per variant | value buffer | -| `T?` | `T?` | value buffer (`Iface?` is a nullable pointer, `Cb?` a null vtable) | -| `[T]` | `T[]` | value buffer | -| `{K: V}` | `Dictionary` | value buffer | +| record | positional `sealed record` | value buffer | +| rich enum | `abstract record` with one nested `sealed record` per variant | value buffer | +| `T?` of a scalar, `bool`, or C-style enum | `T?` (`int?`, `Color?`) | a presence flag plus the value, no buffer | +| other `T?` | `T?` | value buffer (`Iface?` is a nullable pointer, `Cb?` a null vtable) | +| `[P]` of `i8` ... `u64` (not `u8`), `f32`, `f64` | `ReadOnlySpan

` as a parameter, `P[]` as a result | a typed array: a parameter is pinned in place, a result is copied once | +| other `[T]` | `IReadOnlyList` | value buffer | +| `{K: V}` | `IReadOnlyDictionary` | value buffer | | interface | `sealed class : IDisposable` | object pointer | | callback interface | `interface I{Name}` | `ctx` plus a static vtable | | `iter` | `IEnumerable` | iterator handle | -Strings are encoded to UTF-8 and pinned for the call; returned strings, -bytes, and buffers are copied and released with `{prefix}_free_bytes`. -Records and rich enums encode themselves (`WriteTo` and `ReadFrom`), and -each distinct optional, list, or map type gets one encode and decode pair in -the internal `FfiCodecs` class (`WriteListOfEntry`, -`ReadMapOfStringToStore`), which every call site and field shares. +A typed-array or bytes parameter is a span, so an array, a slice of one +(`values.AsSpan(1, 3)`), or stack memory passes without a copy; it must be +aligned for its element type (managed arrays always are), or the producer +rejects it as a marshalling failure (-3). Inside a value buffer (a record +field, a list element) the same types are ordinary lists: +`IReadOnlyList`. + +Returned strings, bytes, typed arrays, and buffers are copied and released +with `{prefix}_free_bytes`. Records and rich enums encode themselves +(`WriteTo` and `ReadFrom`); optionals, lists, and maps go through the +runtime's generic codec (`WriteList`, `ReadMap`, ...), handed a static lambda +per element, so no code is generated per collection shape. + +## Records + +A record is a positional record, so it has value equality, `with` +expressions, deconstruction, and a readable `ToString`: + +```csharp +public sealed record Entry(string Key, byte[] Value, EntryKind Kind, uint Version, long? ExpiresAt, + IReadOnlyList Tags, IReadOnlyDictionary Metadata) +``` + +A record with a byte array, list, or map field overrides `Equals` and +`GetHashCode` to compare those fields by content, so two decodes of the same +entry are equal, and `entry with { Tags = new[] { "x" } }` isn't. Any +`IReadOnlyList` or `IReadOnlyDictionary` works as a field value +(an array, a `List`, an immutable collection). A field spelled like the +record itself or like a member every record has (`ToString`, `Equals`) gains +a trailing `_`. + +A rich enum is a closed hierarchy you can `switch` on: + +```csharp +switch (change) +{ + case Change.Put put: Console.WriteLine(put.Entry.Key); break; + case Change.Removed { Expired: true } removed: Console.WriteLine($"{removed.Key} expired"); break; +} +``` ## Objects and lifetime @@ -135,27 +193,34 @@ itself gains a trailing `_`: an IDL method `dispose` is `Dispose_()` ## Errors -What a failed call throws depends on whether it's declared `throws` +What a failed call throws follows its `throws` clause ([the trap policy](../guides/errors-and-memory.md#the-trap-policy)): -- **A throwing call** throws its module's domain exception. Each error - domain becomes an abstract class deriving from `NativeException`, with one - nested sealed class per code whose fields are typed properties; catch the - nested class for one code or the domain class for any of them. A runtime - code (-1 generic, -2 panic, -3 marshalling failure, -4 a failed callback) - throws `NativeException` itself, whose `Code` property carries it and - whose constants name it (`GenericErrorCode`, `PanicErrorCode`, - `MarshalErrorCode`, `ForeignErrorCode`). -- **A call that isn't `throws`** can't fail by contract, so a failure is a - producer bug: it throws `NativeBugException`, an - `InvalidOperationException` whose `Code` is the runtime code and whose - message names the code and the producer's message. +- **`throws: SomeDomain`** throws the domain's exception. Each error domain + becomes a class deriving from `NativeException`, named with one + `Exception` suffix (`KvError` is `KvException`, `KitchenErrors` is + `KitchenException`), with one nested sealed class per code whose fields + are typed properties; catch the nested class for one code or the domain + class for any of them. Domains are open: a positive code the bindings + don't know (one the library added later) throws the domain class itself, + with that `Code` and the producer's message. Codes are only unique within + a domain, so a call's own domain decides the type. +- **`throws: any`** throws `NativeException` itself with `Code` -1 + (`GenericErrorCode`) and the producer's message. +- **A runtime code** of a throwing call (-2 panic, -3 marshalling failure, + -4 a failed callback) throws `NativeException` with that `Code`; its + constants name them (`PanicErrorCode`, `MarshalErrorCode`, + `ForeignErrorCode`). +- **A call that can't fail** (no `throws`) failing anyway is a producer + bug: it throws `NativeBugException`, an `InvalidOperationException` whose + `Code` is the runtime code and whose message names the code and the + producer's message. - **Cancellation** (-5) always throws `OperationCanceledException`. For the `kvstore` sample's `KvError` domain: ```csharp -public abstract class KvException : NativeException +public class KvException : NativeException { ///

key not found public sealed class KeyNotFound : KvException @@ -163,7 +228,6 @@ public abstract class KvException : NativeException /// The domain code, 1001. public const int ErrorCode = 1001; - /// The key that was looked up. public string Key { get; } public KeyNotFound(string key, string? message = null) : base(ErrorCode, message ?? "key not found") @@ -172,7 +236,7 @@ public abstract class KvException : NativeException } } - // ... Expired, StoreFull, InvalidPath, Rejected + // ... Expired, StoreFull, InvalidPath, Rejected, CallbackFailed } ``` @@ -187,97 +251,89 @@ catch (KvException.KeyNotFound e) } ``` -The message is the producer's (a Rust producer's `Display` output); a code -class constructed in C# defaults to the IDL message. If the API declares a -type named `NativeException` or `NativeBugException`, that class takes the -namespace as a prefix (`KvstoreNativeException`). +The message is the producer's (a Rust producer's `Display` output, empty +messages arriving as the IDL default); a code class constructed in C# +defaults to the IDL message. A code field named `message` (or `code`) is +`Message_` (or `Code_`), since `Exception` already has the property. If the +API declares a type named like a runtime class (`NativeException`, +`NativeBugException`, `NativeLoadException`), the runtime class takes the +namespace's last segment as a prefix (`KvstoreNativeException`). ## Async and cancellation -An async function returns `Task` or `Task`. The launcher receives an +An async function returns `Task` or `Task`, and every one takes an +optional `CancellationToken`. The launcher receives an `[UnmanagedCallersOnly]` completion and a `GCHandle` to the pending call; the completion runs on a producer thread, decodes the result, releases everything the call owned, and completes the task (continuations run -asynchronously). A `cancellable` function also takes a -`CancellationToken`: +asynchronously). ```csharp -public static Task DoCancellable(string input, CancellationToken cancellationToken = default) -{ - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - var inputBytes = Ffi.Utf8(input); - var ffiCall = new FfiCall(cancellationToken); - try - { - fixed (byte* inputPtr = inputBytes) - { - NativeMethods.kitchen_sink_kitchen_do_cancellable(inputPtr, (nuint)inputBytes.Length, ffiCall.CancelToken(), &CompleteDoCancellable, ffiCall.Context); - } - } - catch - { - ffiCall.Abandon(); - throw; - } - return ffiCall.Task; -} +public Task VersionOf(string key, CancellationToken cancellationToken = default) ``` -`CancelToken()` creates the native token and registers the -`CancellationToken` to cancel it. When the producer completes with -5, the -task becomes canceled, so `await` throws `TaskCanceledException` (an -`OperationCanceledException`). The consumer's token reference is destroyed -after completion, and a registration racing the completion never touches a -destroyed token. +- For a `cancellable` function the token cancels the native call through + the native cancel token; when the producer completes with -5 the task + becomes canceled, so `await` throws `TaskCanceledException` (an + `OperationCanceledException`). +- For any other function, cancelling stops waiting: the task becomes + canceled at once, and the native call, which can't be stopped, finishes + in the background. Its result is released then (a returned object + wrapper is disposed right away). +- An already-cancelled token returns a canceled task without launching. + +The consumer's native cancel token is destroyed after completion, and a +registration racing the completion never touches a destroyed token. ## Callbacks -A callback interface becomes a C# interface the consumer implements. Its -methods take and return the same C# types as the rest of the API: +A callback interface becomes a C# interface the consumer implements, with +the same types as the rest of the API: ```csharp public interface IPolicy { + /// Reported to the library with its code and fields. + long? TtlFor(string key, long? requested); + /// Reported to the library with its code and fields. Entry Admit(Entry entry); Store Route(string key, Store home); } -public interface ILoader +public interface IScorer { - string Name(); - Store? Fallback(string key); - /// Reported to the library with its code and fields. - byte[] Load(string key); + double[] Scores(ReadOnlySpan sizes); } ``` Passing an implementation pins it in a `GCHandle` (the `ctx`) and hands the producer one process-wide vtable of `[UnmanagedCallersOnly]` trampolines, which starts with the `{size, flags, free}` header (`size` is the vtable's -real size as C# lays it out). The `free` entry releases the handle when the -producer drops its last reference, possibly on another thread. An optional -callback parameter (`ILoader? loader`) passes a null vtable for `null`. - -Trampolines run on whatever thread the producer calls from. String, bytes, -and buffer arguments are copied, and object arguments are adopted: each is a +real size as C# lays it out, `flags` is 0: methods may run on any thread). +The `free` entry releases the handle when the producer drops its last +reference, possibly on another thread. An optional callback parameter +(`ILoader? loader`) passes a null vtable for `null`. + +Trampolines run on whatever thread the producer calls from. String and +buffer arguments are decoded; typed-array and bytes arguments are +`ReadOnlySpan`s over the producer's memory, valid only during the call +(copy with `ToArray()` to keep them); object arguments are adopted, each a new wrapper the implementation owns. Return values go back to the producer by family: -- a scalar, `bool`, or C-style enum is the C return; +- a scalar, `bool`, or C-style enum is the C return, and an optional one is + a presence flag plus an out slot; - an object is a fresh reference the producer adopts (`CloneHandle()`); a `null` for a required object reaches the producer as null, which it - rejects as a marshalling failure (-3); -- a string, bytes, record, rich enum, optional, list, or map is encoded - into a run allocated with `{prefix}_alloc` and written to the method's - out slots, which the producer adopts and frees. + rejects as a marshalling failure; +- a string, bytes, typed array, record, rich enum, optional, list, or map is + written into a run allocated with `{prefix}_alloc` and handed over through + the method's out slots, which the producer adopts and frees. An exception thrown by an implementation never unwinds into native code. -A method declared `throws` that throws its module's domain exception +A method declared `throws: SomeDomain` that throws that domain's exception reports the code, the message, and the code's fields (encoded as the error payload with `{prefix}_error_set_payload`), so the producer sees exactly that domain error: @@ -287,26 +343,28 @@ public Entry Admit(Entry entry) { if (entry.Key.StartsWith("secret")) { - throw new KvException.Rejected(entry.Key, "no secrets", "secrets are not stored"); + throw new KvException.Rejected(entry.Key, "no secrets"); } - return entry; + return entry with { Kind = EntryKind.Encrypted }; } ``` -Any other exception, or one thrown from a method that isn't `throws`, is -reported as -4 with the exception's message. What the failure means for the -call in progress is the producer's decision. When a Rust producer -propagates it with `?`, the original (throwing) call throws that same domain -exception, or `NativeException` with `ForeignErrorCode` for a -4. +Any other exception is reported with code -1 and its message (-4 from a +method that can't fail). What the failure means for the call in progress is +the producer's decision; the `kvstore` sample turns it into +`KvException.CallbackFailed` carrying the message. ## Iterators -An `iter` function launches eagerly, so launch errors throw at the call, -and returns a single-use `IEnumerable` that pulls one item per -`MoveNext`. The native iterator is a `SafeHandle`, destroyed when -enumeration ends, when the enumerator is disposed (`foreach` does this on -early exit), or by its finalizer. Enumerating the same sequence twice -throws `InvalidOperationException`. +An `iter` function returns an `IEnumerable` that streams lazily, one +native call per item. It's re-enumerable: each enumeration (each +`GetEnumerator`, so each `foreach` or LINQ pass) launches a new native +iterator, so a failure to start throws from `GetEnumerator`, at the start +of the loop, rather than from the call. The enumerator owns its native +iterator as a `SafeHandle`, destroyed when enumeration ends, when the +enumerator is disposed (`foreach` does this on early exit), or by its +finalizer. Span parameters of an iterator function are copied once, since +the sequence outlives the call. ## Known limitations @@ -315,10 +373,13 @@ throws `InvalidOperationException`. through `using static`. - Only one `DllImportResolver` can exist per assembly. If you compile the generated sources into an assembly that already has one, the - `{PREFIX}_LIBRARY` override is skipped and default probing applies. -- Async functions that aren't `cancellable` take no `CancellationToken`. + `{PREFIX}_LIBRARY` override and the implicit load-time check are skipped; + call `{Namespace}Library.Check()` yourself. - Object references cloned into a value buffer leak if encoding a later field throws before the call (for example a disposed wrapper in the same list). - C# can name an undeclared enum value (`(Color)3`); the producer rejects - it as a marshalling failure (-3). + it as a marshalling failure (-3), also inside an optional (`Color?`). +- A record's hash code of a list or map field uses its count, so records + that differ only in collection contents share a hash (equality still + tells them apart). diff --git a/docs/src/generators/go.md b/docs/src/generators/go.md index 433afff4..469537f5 100644 --- a/docs/src/generators/go.md +++ b/docs/src/generators/go.md @@ -1,11 +1,15 @@ # Go The Go target emits a self-contained cgo module that binds the library's C -ABI. Records and rich enums are plain Go values, interfaces are -reference-counted wrappers with `Close`, throwing calls return `error` -values you match with `errors.As`, async functions block on a +ABI (revision 5). Records and rich enums are plain Go values, interfaces are +reference-counted wrappers with `Close`, calls that declare errors return +`error` values you match with `errors.As`, async functions block on a `context.Context`, and `iter` returns standard-library `iter.Seq` -sequences, so the module requires Go 1.23 or later. +sequences. The module requires Go 1.24 or later. + +Go is a [Tier 2](../stability.md#target-tiers) target: it may lag behind a +new ABI revision for a while, but it runs the same conformance suite as +every other target before a release. ## What's generated @@ -13,28 +17,36 @@ For a library whose identity is `kvstore`: ```text go/ - go.mod module path (the package name, or `name`) + go.mod module path (the package name, or `name`), go 1.24 README.md kvstore.h a copy of the C header bindings.go the API: types, wrappers, trampolines, load-time checks - runtime.go load-time checks, errors, strings, objects, callbacks, async + runtime.go Check, errors, strings, typed arrays, objects, callbacks, + iterators, async codec.go the value-buffer writer and reader ``` +Every Go file carries the standard `// Code generated ... DO NOT EDIT.` +line before its package clause, so linters skip it and editors warn before +you change it. + Every name comes from the package identity: | Name | Default | Override | |------|---------|----------| | Module path | the package `name` | `[generators.go] name` | -| Package name | the C prefix | none | +| Package name | the C prefix without underscores (`kitchen_sink` is `kitchensink`) | `[generators.go] package` | | Linked library | `-l{library}` | none | | Header | `{library}.h` | none | ```toml [generators.go] name = "github.com/example/kvstore" +package = "kvstore" ``` +A package name that's a Go keyword gains a trailing underscore. + All modules render into one Go package, and Go spells every exported name in PascalCase with Go's initialisms (`user_id` is `UserID`, `ttl_seconds` is `TTLSeconds`): @@ -49,15 +61,23 @@ is `TTLSeconds`): | C-style enum `EntryKind`, variant `Volatile` | `EntryKind`, `EntryKindVolatile` | | rich enum `Change`, variant `Put` | `Change`, `ChangePut` | | error domain `KvError`, code `KeyNotFound` | `KvError`, `*KeyNotFoundError` | +| error domain `KitchenErrors` | `KitchenError` (one `Error` suffix, never doubled) | A free function whose bare name would clash with a type, an enum -constant, a constructor, a static, an error type, the runtime's `Error` -or `DebugLive`, or another free function's bare name is prefixed with its -module path: the kvstore sample's async `kv.open_store` sits beside the -`OpenStore` constructor, so it's `KvOpenStore` (and a function in module +constant, a constructor, a static, an error type, the runtime's `Check`, +`Error`, or `DebugLive`, or another free function's bare name is prefixed +with its module path: the kvstore sample's async `kv.open_store` sits beside +the `OpenStore` constructor, so it's `KvOpenStore` (and a function in module `kv.stats` would get a `KvStats` prefix). Methods never clash this way, since each lives on its own type. +Doc comments are the IDL's docs. A package-level declaration's comment +starts with its name, as Go convention asks, so a doc that doesn't is +prefixed with `Name: `. Backticked API names become Go doc links +(`` `new_op` `` is `[NewOp]`, a method `[Store.Get]`) or their Go spelling +(a field `ExpiresAt`, a parameter `ttlSeconds`), and a deprecation becomes +the final `Deprecated:` paragraph that Go tools recognize. + ## Build and load The bindings include the bundled header and link with @@ -108,33 +128,28 @@ directory to the module path's repository; see ## Load-time checks -Importing the package runs `init`, which checks the library's ABI revision -and then, for every top-level module, the library's -[contract table](../reference/abi.md#load-time-checks) against the entries -the bindings were generated with: +`Check() error` checks the library's ABI revision and then, for every +top-level module, the library's +[contract table](../reference/abi.md#load-time-checks) against the rows +the bindings were generated with, including one row per error code and per +callback method: ```go -func init() { - wvCheckABI() - var n C.size_t - table := C.kvstore_kv_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0x0969575bfbb012d7, 0xebd38766e3532c4f, "kv.Store.fork"}, - // ...one entry per declaration - }) - table = C.kvstore_report_contract(&n) - wvCheckContract(table, n, []wvContractEntry{ - {0xa21a2e7274bf28c6, 0x342e0b83b12d73ec, "report.render_report"}, - {0xd504fae45f64ab45, 0xd430b8de9d574a4c, "report.ReportError"}, - }) +if err := kvstore.Check(); err != nil { + log.Fatal(err) // kvstore: kv.Store.put changed since these bindings were generated } ``` -A mismatch panics with an error naming the declaration: -`kvstore: kv.Store.put is missing from the library` or -`kvstore: kv.Store.put changed since these bindings were generated`. -Declarations the library has and the bindings don't are fine, so a library -that only adds functions keeps working with older bindings. +The checks run once, on the first call to `Check` or to any function of the +package, and `Check` returns the same result from then on. Importing the +package never panics. A function called after a failed check panics with +the error `Check` returns, so call `Check` at startup when you'd rather +handle a mismatched library as an error. The error names the first +declaration that fails: `kvstore: kv.Store.put is missing from the library` +or `kvstore: kv.Store.put changed since these bindings were generated`. +Rows the library has and the bindings don't are fine, so a library that +only adds functions, error codes, or callback methods keeps working with +older bindings. ## Type mapping @@ -148,38 +163,50 @@ that only adds functions keeps working with older bindings. | C-style enum | `type E int32` plus `E{Variant}` constants | `int32_t` | | rich enum | sealed `interface` plus one struct per variant | value buffer | | record | plain struct with exported fields | value buffer | -| `T?` | `*T`; a slice, map, rich enum, `[]byte`, or wrapper stays as it is and nil is none | value buffer | -| `[T]`, `{K: V}` | `[]T`, `map[K]V` | value buffer | +| scalar `T?` (integer, float, `bool`, C-style enum) | `*T` | a presence flag and the value | +| other `T?` | `*T`; a slice, map, rich enum, `[]byte`, or wrapper stays as it is and nil is none | value buffer | +| numeric `[T]` (`i8` to `i64`, `u16` to `u64`, `f32`, `f64`) | `[]T` | a typed array (pointer and element count) | +| other `[T]`, `{K: V}` | `[]T`, `map[K]V` | value buffer | | interface | `*Iface` | object pointer | | `Iface?` | `*Iface` (nil is none) | object pointer or null | | callback interface | a Go `interface` you implement | handle plus a static vtable | -| `Cb?` | the same interface (nil is none) | handle plus a vtable, or a null vtable | +| `Cb?` | the same interface (nil is none) | handle plus a vtable, or null | | `iter` | `iter.Seq[T]` or `iter.Seq2[T, error]` | iterator handle | Strings pass a view of Go memory with no copy and no NUL terminator, so they may contain `\x00`. A Go string can hold bytes that aren't UTF-8; the library rejects them as a marshalling failure (`-3`). -Each type that crosses inside a value buffer has one codec pair, -`wvWrite{T}` and `wvRead{T}`, built from the pairs of its parts and shared -by every parameter, result, field, and callback argument of that type: +A numeric slice parameter passes the slice's own backing array for the +call, with no copy and no encoding; cgo keeps it in place until the call +returns, and the library only reads it. A returned array is copied into a +new slice and the library's run is freed. An empty result is an empty, +non-nil slice. Inside a record, list, or map, numeric lists and optional +scalars use the value-buffer encoding like everything else; the Go types +are the same either way. + +Each type that crosses inside a value buffer has one codec pair, shared by +every parameter, result, field, and callback argument of that type. A +composite is named after its shared stem (the same name every target uses): ```go -func wvWriteMapStringStore(w *wvWriter, v map[string]*Store) { +func wvWrite_map_string_Store(w *wvWriter, v map[string]*Store) { wvWriteMap(w, v, (*wvWriter).writeString, wvWriteStore) } ``` ## Objects and lifetime -Each wrapper owns one strong reference. `Close() error` releases it (so a -wrapper satisfies `io.Closer`), and a finalizer releases it if you forget. -Every call borrows the native pointer for its duration: +Each wrapper embeds the runtime's object core, which owns one strong +reference. `Close() error` releases it (so a wrapper satisfies +`io.Closer`), and a `runtime.AddCleanup` cleanup releases it some time after +an unclosed wrapper becomes unreachable. Every call borrows the native +pointer for its duration: ```go func (s *Store) Get(key string) (Entry, error) { cSelf := s.native() - defer s.ref.release() + defer s.release() cKeyPtr, cKeyLen := wvStr(key) var cRetLen C.size_t var cErr C.kvstore_error @@ -205,10 +232,17 @@ Because every wrapper has `Close`, an IDL method named `close` is ## Errors -A function declared `throws` returns `(T, error)`. A positive code comes -back as its code's type, a pointer to a struct holding the code's payload -fields and a `Message`, and every code type of a domain implements the -domain's sealed interface: +A function's `throws` decides its Go signature: + +| IDL | Go signature | A failure is | +|-----|--------------|--------------| +| `throws: KvError` | `(T, error)` | a code of the domain, or an `*Error` for a runtime code | +| `throws: any` | `(T, error)` | an `*Error` with code `-1` and the library's message | +| no `throws` | `T` | a bug: the wrapper panics with an `*Error` | + +A domain code comes back as its code's type, a pointer to a struct holding +the code's payload fields and a `Message`, and every code type of a domain +implements the domain's sealed interface: ```go _, err := store.Get("missing") @@ -222,28 +256,34 @@ if errors.As(err, &kvErr) { } ``` +Domains are open: a positive code the bindings don't declare (from a newer +library) comes back as the domain's `*Unknown{Domain}` type +(`*UnknownKvError`), which keeps the code and message and still matches the +domain's interface. Codes are unique only within a domain, so the +calculator's `CalcError.DivisionByZero` and `ParseError.NotANumber` can both +be 1; each function maps codes through its own domain. + `Error()` returns the library's message, or the code's default message when `Message` is empty. A runtime code (`-1` generic, `-2` a panic in the -library, `-3` a marshalling failure, `-4` a callback failure) or a code -outside the domain comes back as the package's `*Error`, which keeps the -code and message and never matches a domain interface. - -A function that isn't `throws` can only fail because of a bug, so its -wrapper has a plain signature and panics with an `*Error` instead (see +library, `-3` a marshalling failure, `-4` a callback failure) comes back as +the package's `*Error`, which keeps the code and message and never matches a +domain interface. A function that declares no errors panics with that +`*Error` instead (see [the trap policy](../guides/errors-and-memory.md#the-trap-policy)). The panic is an ordinary recoverable one, and its text names the code and the library's message, as in `kvstore: (code -3)`. ## Async and cancellation -Async functions take a `context.Context` first and always return an error. -They block the calling goroutine on a channel that a completion trampoline -fills from the library's thread; call them from a goroutine of your own for -concurrency. A `cancellable` function creates a native cancel token, -cancels it when the context is done, and returns `ctx.Err()` -(`context.Canceled` or `context.DeadlineExceeded`) for the cancelled -completion (code `-5`), or `context.Canceled` if the library cancelled the -call on its own: +Async functions take a `context.Context` first and always return an error; +they never panic on a failure. A function with no `throws` reports its +failure as an `*Error` like a `throws: any` one. They block the calling +goroutine on a channel that a completion trampoline fills from the +library's thread; call them from a goroutine of your own for concurrency. A +`cancellable` function creates a native cancel token, cancels it when the +context is done, and returns `ctx.Err()` (`context.Canceled` or +`context.DeadlineExceeded`) for the cancelled completion (code `-5`), or +`context.Canceled` if the library cancelled the call on its own: ```go ctx, cancel := context.WithTimeout(context.Background(), time.Second) @@ -256,60 +296,65 @@ if errors.Is(err, context.DeadlineExceeded) { Any other async function returns `ctx.Err()` as soon as the context is done and discards the native result when it arrives. A context that's already -done never launches the call. A throwing async function returns its domain -errors like a sync one; any other panics on failure. +done never launches the call. An optional scalar result is a `*T` and a +numeric list result a `[]T`, as in a sync call. ## Callbacks A callback interface is a Go interface. Passing an implementation stores it in a handle table and hands the library the address of one static vtable -per interface: the header's `size` and `flags`, the `free` entry that -deletes the handle once the library is done, and one `//export` trampoline -per method. The library may call methods from any thread, and `free` may run -on any thread too. +per interface: the header's `size` and `flags` (0: Go may run a method on +any thread), the `free` entry that deletes the handle once the library is +done, and one `//export` trampoline per method. The library may call +methods from any thread, and `free` may run on any thread too. ```go type Policy interface { - // Admit an entry about to be stored, returning it as it should be - // stored. ... + // The TTL a write of `key` gets, given the TTL the caller + // requested (absent for none): ... // - // Return a KvError to report that code, with its fields, to the native - // caller. + // Return a KvError to report one of its codes, with its fields, to the native + // caller; any other error reports code -1 with its text. + TTLFor(key string, requested *int64) (*int64, error) Admit(entry Entry) (Entry, error) - // Route returns the store `key` belongs in: ... - Route(key string, home *Store) *Store + Route(key string, home *Store) (*Store, error) } ``` -- **Arguments.** Strings, bytes, and buffered values are copied or decoded - for the call. An object argument (`home` above) is a new wrapper that - owns its own reference: keep it, return it, or `Close` it (the finalizer - releases it otherwise). +- **Arguments.** Strings, bytes, typed arrays, and buffered values are + copied or decoded for the call, and an optional scalar is a `*T`. An + object argument (`home` above) is a new wrapper that owns its own + reference: keep it, return it, or `Close` it. - **Returns.** A method may return any type but an iterator or a callback - interface. A direct value returns by value; a string, `[]byte`, or - buffered value is copied into a run allocated with the library's - allocator; an object returns a fresh reference to the wrapper's object - (the wrapper stays yours). Returning nil for a required object, or a value - the library can't read (text that isn't UTF-8, an undeclared enum value), - fails the call in progress with `-3`. -- **Errors.** A method declared `throws` returns `(T, error)` or `error`. - Returning one of the domain's code types (for example - `&kvstore.RejectedError{Key: k, Reason: "no secrets", Message: "secrets - are not stored"}`) reports that code, its message, and its payload - fields, so the original caller receives exactly that error. Any other - error reports `-4` with its text. A method that panics, throwing or not, - is recovered and reported as `-4` with the panic's text; nothing unwinds - into the library. -- **Optional callbacks.** A `Cb?` parameter accepts nil, which passes a null - vtable. A required one panics on nil. + interface. A direct value returns by value, an optional scalar as its + flag and value; a string, `[]byte`, typed array, or buffered value is + copied into a run allocated with the library's allocator; an object + returns a fresh reference to the wrapper's object (the wrapper stays + yours). Returning nil for a required object, or a value the library can't + read (text that isn't UTF-8, an undeclared enum value), fails the call in + progress. +- **Errors.** A method that declares `throws` returns `(T, error)` or + `error`. Returning one of its domain's code types (for example + `&kvstore.RejectedError{Key: k, Reason: "no secrets"}`) reports that code + and its payload fields, so the original caller can receive exactly that + error. Any other error reports code `-1` with its text. A method that + panics, throwing or not, is recovered and reported as `-4` with the + panic's text; nothing unwinds into the library. What the caller finally + sees is the producer's choice: a Rust producer renders a typed error's + message from its fields, and the kvstore sample turns every other + callback failure into `KvError.CallbackFailed`. +- **Nil implementations.** A `Cb?` parameter accepts nil, which passes no + callback; so does a typed nil (an interface holding a nil pointer, map, + slice, func, or channel), which would otherwise reach the library as an + implementation whose every call panics. A required one panics on either. ## Iterators An `iter` return becomes a lazy sequence: each `range` launches the native iterator, pulls one element per step, and destroys the iterator when -the loop ends, including on an early `break`. A throwing function returns -an `iter.Seq2[T, error]` that yields its error as a final `(zero, err)` -pair: +the loop ends, including on an early `break`. A function that declares +errors returns an `iter.Seq2[T, error]` that yields its error as a final +`(zero, err)` pair: ```go for key, err := range store.Keys(nil) { @@ -320,13 +365,18 @@ for key, err := range store.Keys(nil) { } ``` +Elements may be optional scalars (`iter.Seq[*int64]`) or numeric lists +(`iter.Seq[[]int32]`), which cross as a flag and a value or as a typed +array. + ## Leak checks `DebugLive(kind)` reports the library's live objects (0), callbacks (1), iterators (2), cancel tokens (3), and byte runs (4), and `DebugLive(-1)` is 1 when the library counts at all. Every count is 0 unless the library was built with the `leak-check` feature. Wrappers you don't close are released -by finalizers, so run `runtime.GC()` before reading the counts. +by cleanups, so run `runtime.GC()` (and give the cleanups a moment) before +reading the counts. ## Limitations @@ -334,6 +384,10 @@ by finalizers, so run `runtime.GC()` before reading the counts. safe; only a free function whose Go name collides gains its module path (see [What's generated](#whats-generated)). - The library is bound at link time, so there's no run-time override of - which library file loads beyond the platform loader's search path. + which library file loads beyond the platform loader's search path, and + `Check` reports a mismatched library only after the program has linked + and started. - A non-cancellable async call abandoned by its context keeps running in the library until it finishes. +- Vtables are never thread-affine, so a producer may call a Go callback + from any thread; Go's scheduler makes that safe. diff --git a/docs/src/generators/kotlin.md b/docs/src/generators/kotlin.md index cf0c9ebc..038a0a30 100644 --- a/docs/src/generators/kotlin.md +++ b/docs/src/generators/kotlin.md @@ -1,12 +1,16 @@ # Kotlin The Kotlin target emits a Gradle library module: Kotlin sources over a JNI -shim that calls the producer's C ABI (revision 4). The default flavor is an +shim that calls the producer's C ABI (revision 5). The default flavor is an Android library whose shim builds through the NDK's CMake; the `jvm` flavor is a plain Kotlin/JVM library for desktop and server JVMs. Records become data classes, interfaces `AutoCloseable` wrappers, async functions `suspend` functions, callback interfaces Kotlin `interface`s, and each IDL module an -`object`. +`object` whose functions are `@JvmStatic`, so Java calls them as static +methods. + +Kotlin is a [Tier 1](../stability.md#target-tiers) target: it tracks every +ABI revision as it lands and runs the full conformance suite in CI. ## What gets generated @@ -24,8 +28,8 @@ kotlin/ src/main/kotlin/kitchen_sink/ Kitchen.kt, Shared.kt one file per top-level module JniBridge.kt internal: one native per C symbol, callback dispatch - Runtime.kt FfiException, NativeBugException, NativeIterator, loader, lifetime - Buffers.kt value-buffer reader and writer (when the API needs them) + Runtime.kt FfiException, NativeBugException, NativeIterator, NativeLibrary, lifetime + Buffers.kt value-buffer reader and writer, content equality (when the API needs them) Codecs.kt one codec per composite type (when the API has any) Async.kt coroutine bridge (when the API has async) ``` @@ -87,7 +91,7 @@ classpath's `natives//` when present, else with absolute path to load the producer from there instead. `JNI_OnLoad` then makes the two [load-time checks](../reference/abi.md#load-time-checks). -It compares the producer's ABI revision with 4, and for every top-level +It compares the producer's ABI revision with 5, and for every top-level module it looks up each [contract](../reference/abi.md#load-time-checks) entry the bindings were generated with in the producer's table (`kitchen_sink_kitchen_contract`). If either check fails, loading fails with @@ -98,8 +102,25 @@ kitchen_sink: kitchen.Gadget.describe is missing from the library kitchen_sink: kitchen.Item changed since these bindings were generated ``` -A producer that adds declarations still loads: only the entries the -bindings know about are checked. +A producer that adds declarations, error codes, or callback methods still +loads: only the entries the bindings know about are checked. + +A failed load is catchable. `NativeLibrary.load()` loads and checks the +library eagerly and throws the `UnsatisfiedLinkError` that names the +problem, the same one on every later call, so an app can probe for the +library at startup and fall back gracefully: + +```kotlin +try { + NativeLibrary.load() +} catch (e: UnsatisfiedLinkError) { + // The library is missing, or it doesn't match these bindings. +} +``` + +Without that call, the first use of the bindings loads them and throws the +same `UnsatisfiedLinkError`; every later use throws `NoClassDefFoundError`. +Both are `LinkageError`s, so `catch (e: LinkageError)` covers either. ## Type mapping @@ -114,7 +135,9 @@ bindings know about are checked. | enum | `enum class` with `value` | `jint` | | record | `data class` | value buffer | | rich enum | `sealed class` | value buffer | -| `T?`, `[T]`, `{K:V}` | `T?`, `List`, `Map` | value buffer | +| `T?` of a scalar or enum | `Int?`, `Priority?`, ... | a presence flag and the value in; a box (`Integer?`) out | +| `[T]` of `i8`...`f64` (not `bool`, `u8`) | `List`, `List`, ... | a primitive array (`IntArray`, `LongArray`, ...) | +| other `T?`, `[T]`, `{K:V}` | `T?`, `List`, `Map` | value buffer | | interface, `Iface?` | wrapper class, nullable wrapper | address (`0` is none) | | callback interface, `Cb?` | Kotlin `interface`, nullable | the implementing object (`null` is none) | | `iter` | `NativeIterator` | iterator address | @@ -128,11 +151,31 @@ each one crosses in the signed type of the same width, converted bit for bit `ULong.MAX_VALUE`. Strings cross as UTF-8 bytes with an explicit length, so interior NULs and supplementary characters round-trip exactly. -Value buffers are encoded and decoded by generated functions: one `pack` -and `unpack` pair per record and rich enum (next to its declaration) and -one per distinct composite type in `Codecs.kt` (`packListOfEntry`, -`unpackMapOfStringToStore`, `packOptionalOfStoreInfo`), so every call site -calls a function instead of inlining a loop. +An optional scalar or C-style enum (`i64?`, `Priority?`) never goes through +a value buffer at a call boundary. As an argument it crosses as its presence +flag and its value, exactly like its C slots; out of native code (a return, +an async result, an iterator item, a callback return) it's a nullable box +(`java.lang.Long` and the like). A list of fixed-width numbers (`[f64]`, +`[u64]`) crosses as a primitive array: the wrapper copies the `List` into a +`DoubleArray` (unsigned elements bit for bit), and the shim copies that once +more, with `GetDoubleArrayRegion`, into storage the call borrows (on the +stack for small arrays); a returned array is copied once into a new +`DoubleArray` (`NewDoubleArray`), and the list you get is a view of it +(`asList()`), not another copy. The same types nested in a record or a +collection still travel in the value buffer. + +Value buffers are encoded and decoded by generated functions: one `pack_` +and `unpack_` pair per record and rich enum (next to its declaration) and +one per distinct composite type in `Codecs.kt`, named with the composite's +canonical stem (`pack_list_Entry`, `unpack_map_string_Store`, +`pack_opt_StoreInfo`), so every call site calls a function instead of +inlining a loop. + +Records holding bytes compare by content: a `data class` with a `ByteArray` +field (or one inside a list, map, or optional) overrides `equals`, +`hashCode`, and `toString` to use the array's content, since a +`ByteArray`'s own `equals` is identity. Other data classes keep the +generated members. ## Naming @@ -149,11 +192,15 @@ the top level of the package. Two rules keep the generated names valid: `Module` suffix: a `kv.stats` module beside a `Stats` record is `Kv.StatsModule`. -Parameters and methods are lowerCamelCase; record fields, variant fields, -and error payload fields keep their IDL spelling (`expires_at`). Keywords +Functions, methods, parameters, record fields, variant fields, and error +payload fields are lowerCamelCase (`expires_at` is `expiresAt`). Keywords gain a trailing underscore (`class_`), as does a method named like a member every wrapper already declares or inherits (`close`, `handle`, `toString`, -`wait`, ...; see [reserved member names](../reference/naming.md#identifiers-in-generated-code)). +`wait`, ...; see [reserved member names](../reference/naming.md#identifiers-in-generated-code)), +and an error payload field named like a property every exception has +(`message`, `code`, `cause`, ...): `CallbackFailed.message_`. Identifiers in +backticks in IDL docs and deprecation messages are rewritten to these +spellings (``Use `new_op` instead`` reads ``Use `newOp` instead``). ## Objects and lifetime @@ -162,6 +209,7 @@ An interface wrapper owns one strong reference: ```kotlin class Gadget private constructor(address: Long) : AutoCloseable { internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::kitchen_Gadget_destroy)) + ... /** Render the gadget as a human-readable string */ fun describe(): String = handle.borrow { _self -> @@ -184,21 +232,32 @@ and the decoder adopts each one into a new wrapper. ## Errors -A callable declared `throws` raises an `FfiException`. Each error domain -becomes a sealed subclass named with an `Exception` suffix (`KvError` -becomes `KvException`; `KitchenErrors` becomes `KitchenErrorsException`), -with one class per code whose payload fields are properties and whose -message defaults to the IDL's: +Every error is an `FfiException` with a `code` and the producer's message. +Each error domain becomes a subclass named with the shared `Exception` +spelling (`KvError` becomes `KvException`; `KitchenErrors` becomes +`KitchenException`), with one nested class per code whose payload fields are +properties and whose message defaults to the IDL's: ```kotlin -sealed class KitchenErrorsException(code: Int, message: String) : FfiException(code, message) { - class NotFound(message: String = "Item not found") : KitchenErrorsException(1, message) +open class KitchenException internal constructor(code: Int, message: String) : FfiException(code, message) { + class NotFound(message: String = "Item not found") : KitchenException(1, message) ``` -A runtime failure of a throwing call keeps the root `FfiException` class -with its negative `code`: `-1` a generic producer error, `-2` a producer -panic, `-3` a marshalling failure, and `-4` a callback implementation that -failed (the message is the callback's). +A callable declared `throws: KitchenErrors` raises that domain's +exceptions. Domains are open: a code the bindings don't declare (one a newer +library added) raises a plain `KitchenException` carrying that `code` and +the library's message, so an app catches it with the rest of its domain. +Codes are unique only within a domain, so each call maps its codes through +its own domain: code 1 is `CalcException.DivisionByZero` from `divide` and +`ParseException.NotANumber` from `parse`. + +A callable declared `throws: any` fails with code `-1` and a message, +raised as the root `FfiException`. A runtime failure of any throwing call is +the root `FfiException` too, with its negative `code`: `-1` a generic +producer error, `-2` a producer panic, `-3` a marshalling failure, and `-4` +a callback implementation that failed. Every throwing function carries +`@Throws` for its exception class, so Java callers can catch it as the +checked exception it is. A callable that isn't `throws` can't report an error, so any failure is a bug in the library, and the binding raises the unchecked @@ -218,7 +277,8 @@ by its scope, or through `withTimeout`) cancels the token, so the producer drops the pending work: ```kotlin -suspend fun doCancellable(input: String): String = awaitNative(true, 0, { _raw -> decodeUtf8(_raw as ByteArray) }) { _token, _done -> +@JvmStatic +suspend fun doCancellable(input: String): String = awaitNative(true, 0, { decodeUtf8(it as ByteArray) }) { _token, _done -> JniBridge.kitchen_do_cancellable(encodeUtf8(input), _token, _done) } ``` @@ -231,10 +291,12 @@ API has async functions. ## Callbacks -A callback interface is a plain Kotlin `interface`, and an optional callback +A callback interface is a plain Kotlin `interface` (a `fun interface` when +it has one method, so a lambda implements it), and an optional callback parameter (`Cb?`) takes `null` for none, which passes a null vtable. Passing an implementation pins it with a JNI global reference until the producer -calls the vtable's `free`. +calls the vtable's `free`. Every vtable's `flags` is 0: the JVM can run a +method on any thread. ```kotlin interface Policy { @@ -242,12 +304,20 @@ interface Policy { * Admit an entry about to be stored, returning it as it should be * stored. ... * - * Throw a [KvException] to report a typed error to the library. + * Throw a [KvException] to report a typed error to the library; anything else + * thrown reaches it as an untyped failure with its message. */ + @Throws(KvException::class) fun admit(entry: Entry): Entry + ... +} - fun route(key: String, home: Store): Store +fun interface Scorer { + @Throws(Exception::class) + fun scores(sizes: List): List } + +val ranked = store.rank { sizes -> sizes.map { it.toDouble() } } ``` - **Parameters.** Strings, bytes, and value buffers are decoded before the @@ -255,18 +325,19 @@ interface Policy { reference the producer handed over; close it, or let the cleaner release it. - **Returns.** A method may return any type but an iterator or a callback - interface. A scalar or enum is returned by value. An object is returned as - a new strong reference (the wrapper keeps its own). A string, bytes, or - value buffer is encoded and copied into a run allocated with - `{prefix}_alloc`, which the producer adopts. + interface. A scalar or enum is returned by value, and an optional one as a + box. An object is returned as a new strong reference (the wrapper keeps + its own). A string, bytes, value buffer, or typed array is copied into a + run allocated with `{prefix}_alloc` (8-aligned), which the producer adopts. - **Failures.** Whatever a method throws is caught in the dispatch shim on `JniBridge`; nothing unwinds through native frames. A method declared - `throws` may throw its module's domain exception (`KvException.Rejected`, - say), which reaches the producer with its code, message, and payload - fields, so a producer that propagates it hands the original caller the - same typed error. Anything else, including a domain exception from a - method that doesn't declare `throws`, reaches the producer as code `-4` - with the exception's message. + `throws: KvError` may throw that domain's exceptions + (`KvException.Rejected`, say), which reach the producer with their code and + payload fields, so a producer that propagates one hands the original caller + the same typed error (with the message the producer renders from the + fields). Anything else it throws, and anything a `throws: any` method + throws, reaches the producer as code `-1` with the exception's message. A + method that doesn't throw reports any failure as code `-4`. Producer threads may call an implementation from anywhere. The shim attaches a thread to the JVM the first time it calls in (as a daemon) and detaches it @@ -277,16 +348,20 @@ thread again as soon as the call returns. ## Iterators An `iter` return is a `NativeIterator`, which is both an `Iterator` -and `AutoCloseable`. Each `hasNext()` pulls at most one element. The native -iterator is released when the sequence is exhausted, on `close()`, or by the -cleaner. +and `AutoCloseable`. Each `hasNext()` pulls at most one element, which the +shim hands over as one object (a box, an array, or `null` for an absent +optional). The native iterator is released when the sequence is exhausted, +on `close()`, or by the cleaner. A `NativeIterator` isn't thread-safe: +iterate it from one thread at a time. ## Known limitations - Gradle builds aren't exercised by the conformance suite, which compiles the same sources with `kotlinc` and the shim with CMake. -- Interior NULs in an error message are cut at the first NUL, both in - messages the producer reports and in a failing callback's message. +- A typed-array argument is copied twice (the `List` into a primitive + array, then into native memory for the call), because a `List` holds + boxes and the JVM can't lend its arrays to a call that may call back into + Kotlin. - Kotlin's type system can't express a callback return the producer would reject (a null object for a non-optional `I`, or a malformed record), so those `-3` paths aren't reachable from Kotlin. diff --git a/docs/src/generators/node.md b/docs/src/generators/node.md index 249d516f..8168e31a 100644 --- a/docs/src/generators/node.md +++ b/docs/src/generators/node.md @@ -1,10 +1,13 @@ # Node.js The Node.js target generates an npm package: an ES module with TypeScript -declarations, and a small N-API addon that calls the library's C ABI. The -JavaScript API is shared with the [WebAssembly target](wasm.md); only the -transport underneath differs, so code written against one runs against the -other. +declarations, and a small N-API addon that calls the library's +[C ABI](../reference/abi.md) (revision 5). The JavaScript API is shared with +the [WebAssembly target](wasm.md); only the transport underneath differs, so +code written against one runs against the other. + +Node.js is a [Tier 1](../stability.md#target-tiers) target: it tracks every +ABI revision as it lands and runs the full conformance suite in CI. ## What gets generated @@ -18,7 +21,9 @@ node/ ├── kvstore.h a copy of the C header the addon compiles against ├── index.js the API: one namespace export per top-level module ├── index.d.ts TypeScript declarations -├── runtime.js the shared runtime (codec, errors, wrappers, load-time checks) +├── runtime.js the shared runtime (codec, checks, errors, wrappers, load-time checks) +├── debug.js the `./debug` export: leak counters for tests +├── debug.d.ts └── README.md ``` @@ -70,25 +75,29 @@ See [Packaging](../guides/packaging.md) for which addons are prebuilt. Importing the package loads the addon and, before any other call, checks that the library matches the bindings (see [Load-time checks](../reference/abi.md#load-time-checks)). The library must -implement ABI revision 4, and every top-level module's contract table must -hold each declaration the bindings were generated with. `index.js` embeds -those entries: +implement ABI revision 5, and every top-level module's contract table must +hold each row the bindings were generated with: one per declaration, error +code, and callback method. `index.js` embeds those rows, each with its +canonical signature: ```js const $contract = [ - ['kv', [ - [0x0969575bfbb012d7n, 0xebd38766e3532c4fn, 'kv.Store.fork'], + ['kitchen_sink_kitchen_contract', [ + [0x01d4f09bbddc986fn, 0xffcf2bd3b04aa002n, 'kitchen.Gadget.new'], // constructor new(i64) -> Gadget // ... ]], ]; -$verify($raw, 'kvstore', 'kvstore', 4, $contract); +$verify($raw, 'kitchen_sink', 'kitchen_sink', 5, $contract); ``` If a check fails, the import throws an `Error` naming the declaration: `kvstore: kv.Store.put is missing from the library` or `kvstore: -kv.Store.put changed since these bindings were generated`. Declarations the -library has and the bindings don't are fine, so a library that only adds to -its API keeps working with older bindings. +kv.Store.put changed since these bindings were generated`. Rows the library +has and the bindings don't are fine, so a library that only adds to its API +(declarations, error codes, callback methods) keeps working with older +bindings. The import fails the same way when the addon can't be found or +the library can't be loaded, so `await import(...)` in a `try` can fall +back. ## Modules and names @@ -97,9 +106,12 @@ are nested namespaces, so equal names in different modules never collide: ```js export const kitchen = Object.freeze({ - KitchenErrorsError: kitchen$KitchenErrorsError, + KitchenError: kitchen$KitchenError, NotFoundError: kitchen$NotFoundError, InvalidInputError: kitchen$InvalidInputError, + PantryError: kitchen$PantryError, + OutOfStockError: kitchen$OutOfStockError, + SpoiledError: kitchen$SpoiledError, Priority: kitchen$Priority, Gadget: kitchen$Gadget, boolId: kitchen$boolId, @@ -112,15 +124,17 @@ reserved words, which are legal property names (`store.delete(key)`), except that an instance method named `close` or `constructor`, and a static named `name`, `length`, `prototype`, or `caller`, gains one too ([reserved member names](../reference/naming.md#identifiers-in-generated-code)). -Record fields keep their IDL spelling. +Record fields keep their IDL spelling. Doc comments in `index.d.ts` name +declarations in this spelling too: an IDL doc or deprecation message that +says `` `new_op` `` reads `` `newOp` `` there. ## Type mapping | IDL type | TypeScript type | Notes | |----------|-----------------|-------| -| `i8`, `i16`, `i32`, `u8`, `u16`, `u32` | `number` | | -| `i64`, `u64` | `bigint` | Integral numbers are accepted as arguments; out-of-range values throw a `RangeError` | -| `f32`, `f64` | `number` | NaN, the infinities, and `-0` round-trip | +| `i8`, `i16`, `i32`, `u8`, `u16`, `u32` | `number` | A fractional, non-finite, or out-of-range argument throws a `RangeError`; nothing wraps | +| `i64`, `u64` | `bigint` | Integral numbers in range are accepted as arguments; anything else throws a `RangeError` | +| `f32`, `f64` | `number` | NaN, the infinities, and `-0` round-trip; an `f32` argument rounds as `Float32Array` does | | `bool` | `boolean` | | | `string` | `string` | UTF-8, passed as pointer and length, so interior NULs survive | | `bytes` | `Uint8Array` | | @@ -129,18 +143,23 @@ Record fields keep their IDL spelling. | rich enum | tagged union | `{ tag: 'Circle', radius: 2.5 }` | | interface | `class` | See [Objects](#objects-and-lifetime) | | callback interface | `interface` | Any object with the methods; see [Callback interfaces](#callback-interfaces) | -| `T?` | `T \| null` | `undefined` is accepted as `null` | -| `[T]` | `T[]` | | +| `T?` | `T \| null` | `undefined` is accepted as `null`. An optional scalar or C-style enum crosses directly as a flag and a value, with no buffer | +| `[T]` | `T[]` | A list of `i8`, `i16`, `i32`, `i64`, `u16`, `u32`, `u64`, `f32`, or `f64` crosses as a typed array: an argument may be an array (checked element by element) or the matching typed array (`Float64Array` for `[f64]`, passed as is), and a result is a plain array | | `{K: V}` | `Record` | `Record` for integer keys of 32 bits or fewer, `Partial>` for C-style enum keys; keys are property names (strings) at run time, and a `Map` is accepted as an argument | -| `iter` | `IterableIterator` | Lazy | +| `iter` | `NativeIterator` | Lazy; an `IterableIterator` with `close()` and `[Symbol.dispose]()` | -Arguments are type-checked: a value of the wrong type throws a `TypeError` -before the native call. +Arguments are checked before the native call: a value of the wrong type +throws a `TypeError`, and an integer that is fractional or out of its type's +range a `RangeError`, whether it's an argument, an element of a list, a +field of a record, or a callback's return value. The addon checks integer +arguments on the `double` it receives before converting it, so the result +is the same on every platform. Value buffers are encoded and decoded by one function per type: each record and rich enum (`$w$kv$Entry`, `$r$kv$Entry`), each interface carried as an -object token, and each distinct optional, list, and map type in the API -(`$w_list_opt_Entry` writes `[Entry?]`). Call sites name these functions. +object token, and each distinct optional, list, and map type in the API, +named by its canonical stem (`$w_list_opt_Entry` writes `[Entry?]`). Call +sites name these functions. ## Objects and lifetime @@ -176,20 +195,28 @@ Two wrappers may share one native object. Every error extends the package's root class, `{Package}Error` (here `KitchenSinkError`), which carries the ABI `code`: -- Each error domain is a class (`kitchen.KitchenErrorsError`), and each of - its codes a subclass (`kitchen.NotFoundError`) with a static `CODE` and the - IDL message as its default message. Payload fields become properties. A +- Each error domain is a class, named by the shared rule (a domain + `KitchenErrors` is `kitchen.KitchenError`, `KvError` stays `KvError`, and + `Failure` becomes `FailureError`), and each of its codes a subclass + (`kitchen.NotFoundError`) with a static `CODE` and the IDL message as its + default message. A module may declare several domains, and codes of + different domains may share a value. Payload fields become properties. A code without fields is constructed as `new kv.InvalidPathError(message?)`; a code with fields takes them first: `new kv.RejectedError({ key, reason }, message?)`. +- A function declared `throws: SomeDomain` fails with that domain's classes. + Domains are open: a positive code the bindings don't know (the library + added it later) is an instance of the domain class itself, with its + `code` and message. +- A function declared `throws: any` fails with the root class, code -1, and + the library's message. - A cancelled call rejects with `CancelledError` (code -5). -- Runtime failures throw the root class itself: -1 generic, -2 producer - panic, -3 marshalling failure, -4 a callback implementation failed. +- Other runtime failures throw the root class itself: -2 producer panic, -3 + marshalling failure, -4 a callback implementation failed. -Only functions declared `throws` map codes onto their module's domain; a -failure of any other call is a producer bug, and the binding throws the -root class with the code and the producer's message (see -[the trap policy](../guides/errors-and-memory.md#the-trap-policy)). +A failure of a function that doesn't declare `throws` is a producer bug, and +the binding throws the root class with the code and the producer's message +(see [the trap policy](../guides/errors-and-memory.md#the-trap-policy)). ## Async and cancellation @@ -219,24 +246,37 @@ form: | IDL return | Return from JavaScript | What the library receives | |------------|------------------------|---------------------------| -| direct (numbers, `bool`, C-style enums) | the value | the value | +| direct (numbers, `bool`, C-style enums) | the value | the value (integers range-checked) | +| optional scalar (`i32?`, `Mode?`, ...) | the value, or `null` | a flag and the value | +| numeric list (`[f64]`, ...) | an array or the matching typed array | a copy in a run the library adopts | | `string`, `bytes` | a `string`, a `Uint8Array` | a copy in a run the library adopts | -| record, rich enum, `T?`, `[T]`, `{K:V}` | the value | its encoding, in a run the library adopts | +| record, rich enum, other `T?`, `[T]`, `{K:V}` | the value | its encoding, in a run the library adopts | | `I` | a wrapper (never `null`) | a new strong reference to its object | | `I?` | a wrapper or `null` | a new strong reference, or null | -The implementation keeps its own wrapper; the library gets a reference of -its own. A method that throws, or returns a value of the wrong type, fails -the native call with code -4 and the exception's message; nothing unwinds -through native code. A method declared `throws` may instead throw an error -of its module's domain, and the library receives that code, message, and -fields: +Arguments arrive the same way the API returns values: optional scalars as +the value or `null`, numeric lists as plain arrays. The implementation keeps +its own wrapper; the library gets a reference of its own. Nothing unwinds +through native code; how a failure reaches the library follows the method's +`throws`: + +- A method declared `throws: SomeDomain` may throw one of that domain's + errors, and the library receives its code and fields. (A Rust producer + re-renders the message from the fields.) Any other exception is reported + as code -1 with its message. +- A method declared `throws: any` reports any exception as code -1 with its + message. +- A method that doesn't declare `throws` reports an exception, or a return + value of the wrong type, as code -4 with its message. ```js const policy = { + ttlFor(key, requested) { + return key.startsWith('tmp/') ? 60n : requested; + }, admit(entry) { if (entry.key.startsWith('secret')) { - throw new kv.RejectedError({ key: entry.key, reason: 'no secrets' }, 'secrets are not stored'); + throw new kv.RejectedError({ key: entry.key, reason: 'no secrets' }); } return { ...entry, tags: ['admitted'] }; }, @@ -284,10 +324,29 @@ synchronous call, or for the event loop, simply runs later. So: ## Iterators -An `iter` result is a lazy iterator: each `next()` makes one native call. -The native iterator is destroyed when it's exhausted, when it fails, on -`return()` (which `for...of` calls on `break`), on `close()`, or when the -iterator is garbage collected. +An `iter` result is a lazy `NativeIterator`: each `next()` makes one +native call. The native iterator is destroyed when it's exhausted, when it +fails, on `return()` (which `for...of` calls on `break`), on `close()` or a +`using` declaration, or when the iterator is garbage collected. + +```ts +using chunks = kitchen.streamChunks(); +const first = chunks.next(); // released when the block exits +``` + +## Leak counters + +The package's `./debug` export reads the library's live-resource counters, +for tests of code that uses the bindings. It isn't part of the API: + +```js +import { debugLive } from 'kvstore/debug'; + +debugLive(0); // live objects, as a bigint (1 callbacks, 2 iterators, 3 cancel tokens, 4 byte runs) +``` + +Kind -1 is `1n` when the library counts at all (built with `weaveffi`'s +`leak-check` feature); otherwise every counter is `0n`. ## Known limitations @@ -299,5 +358,8 @@ iterator is garbage collected. - `{PREFIX}_LIBRARY` selects the library at build time, not at run time. - Windows builds link `{library}.lib`, and the DLL must be on the loader's search path. +- A numeric list result is copied into a plain array. As an argument, the + matching typed array is lent to the library without a copy, while a plain + array is checked and copied first. - The package is ESM only, and its declarations need TypeScript 5.2 or later (for `Symbol.dispose`). diff --git a/docs/src/generators/python.md b/docs/src/generators/python.md index f766a1a5..221d907e 100644 --- a/docs/src/generators/python.md +++ b/docs/src/generators/python.md @@ -1,19 +1,23 @@ # Python The Python target produces a pip-installable package of pure-Python -`ctypes` bindings over the C ABI (revision 4). There's no compilation step, +`ctypes` bindings over the C ABI (revision 5). There's no compilation step, no native extension, and no third-party runtime dependency; the package -works on any Python 3.9+ interpreter that can load the producer library. +works on any Python 3.10+ interpreter that can load the producer library. The implementation module is fully annotated and the package ships `py.typed`, so type checkers read the bindings themselves (there's no separate stub to drift out of date) and the generated code passes `mypy --strict`. -Records and rich enums are dataclasses that cross the boundary as value -buffers. Interfaces are reference-counted wrapper classes. Callback -interfaces are abstract base classes the consumer subclasses. Async -functions are coroutines whose cancellation cancels the native call, and -`iter` returns are lazy Python iterators. +Records and rich enums are frozen dataclasses that cross the boundary as +value buffers. Optional scalars and numeric lists cross directly (a flag +plus a value, and typed arrays). Interfaces are reference-counted wrapper +classes. Callback interfaces are abstract base classes the consumer +subclasses. Async functions are coroutines whose cancellation cancels the +native call, and `iter` returns are lazy `NativeIterator[T]` objects. + +Python is a [Tier 1](../stability.md#target-tiers) target: it tracks every +ABI revision as it lands and runs the full conformance suite in CI. ## What gets generated @@ -37,12 +41,14 @@ package is its C prefix, so a crate named `key-store` installs as [generators.python] name = "kvstore-python" # distribution name (default: package name) import_name = "kvstore_py" # import package (default: C prefix) -requires_python = ">=3.10" # default ">=3.9" +requires_python = ">=3.12" # default ">=3.10" ``` -The module lists its public names in `__all__` (the error classes, every -type, and the free functions), so `from kvstore import *` and the package's -`__init__.py` export the API and none of the runtime's helpers. +The module lists its public names in `__all__` (the error classes, +`LibraryLoadError`, `NativeIterator`, every type, and the free functions), +so `from kvstore import *` and the package's `__init__.py` export the API +and none of the runtime's helpers. Annotations use builtin generics and +`X | None`. ## Install and load @@ -61,30 +67,43 @@ At import, the module loads the producer library from the first of: It then makes the two [load-time checks](../reference/abi.md#load-time-checks) before binding anything else. `kvstore_abi_version()` must report revision -4, and every top-level module's contract table must contain each +5, and every top-level module's contract table must contain each declaration the bindings were generated with, with an equal hash. The -expected entries are embedded in the module, one `(id, hash, path)` per -declaration: +expected rows are embedded in the module, one `(id, hash, path)` per +declaration (every function, member, type, error code, and callback +method), each with its canonical signature: ```python -_CONTRACTS: Dict[str, List[Tuple[int, int, str]]] = { +_CONTRACTS: dict[str, list[tuple[int, int, str]]] = { "kvstore_kv_contract": [ - (0x0969575bfbb012d7, 0xebd38766e3532c4f, "kv.Store.fork"), + (0x00f1b5c049a1cd69, 0x36b344095a0f5bf9, "kv.Store.rank"), # method rank(Scorer) -> [string] throws KvError ... ], "kvstore_report_contract": [...], } ``` -A failed check raises `ImportError` naming the declaration -(`kv.Store.put is missing from the library` or `kv.Store.put changed since -these bindings were generated`), so a stale binding never misreads a -buffer at runtime. Declarations the library has and the bindings don't are -fine, so a library that grew new functions still loads. Every C prototype -is then bound once, in a module-level table: +When the library can't be loaded, or a check fails, the import raises +`LibraryLoadError`, an `ImportError` subclass whose message says what went +wrong and whose `path` names the library: + +```text +kvstore.kvstore.LibraryLoadError: kv.Store.put changed since these bindings were generated (/opt/lib/libkvstore.dylib) +``` + +The other messages are `kv.Store.put is missing from the library`, a C ABI +mismatch naming both revisions, a library that doesn't export +`kvstore_abi_version`, and the loader's own error with a hint to set +`KVSTORE_LIBRARY`. A stale binding never misreads a buffer at runtime. +Declarations the library has and the bindings don't are fine, so a library +that grew new functions, error codes, or callback methods still loads. +Since the failure happens during the import, catch it as `ImportError` +around the `import` statement. + +Every C prototype is then bound once, in a module-level table: ```python -_c_kv_Store_count = _bind("kvstore_kv_Store_count", ctypes.c_uint32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_kv_Store_count = _bind("kvstore_kv_Store_count", ctypes.c_uint64, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) ``` On macOS, the system `python3` strips `DYLD_LIBRARY_PATH`, so prefer @@ -92,23 +111,44 @@ On macOS, the system `python3` strips `DYLD_LIBRARY_PATH`, so prefer ## Type mapping +Parameters accept abstract containers (any `Sequence` or `Mapping`) and +returns hand out concrete ones (`list`, `dict`). + | IDL type | Python type | Crosses the ABI as | |---|---|---| -| `i8` to `u64` | `int` | `c_int8` to `c_uint64` | +| `i8` to `u64` | `int` | `c_int8` to `c_uint64`, range-checked | | `f32`, `f64` | `float` | `c_float`, `c_double` | | `bool` | `bool` | `c_bool` | | `string` | `str` | UTF-8 bytes plus length (`c_char_p`, `c_size_t`) | | `bytes` | `bytes` | pointer plus length | | C-style enum | `IntEnum` subclass | `c_int32` | -| record | `@dataclass` | value buffer | -| rich enum | base class plus one `@dataclass` per variant | value buffer | -| `T?` | `Optional[T]` | value buffer | -| `[T]`, `{K:V}` | `List[T]`, `Dict[K, V]` | value buffer | +| record | frozen `@dataclass` | value buffer | +| rich enum | base class plus one frozen `@dataclass` per variant | value buffer | +| scalar or enum `T?` | `T \| None` | `bool` flag plus the value | +| other `T?` | `T \| None` | value buffer | +| `[i8]` to `[u64]`, `[f32]`, `[f64]` | `Sequence[int]` or `Sequence[float]` in, `list` out | typed array (pointer plus element count) | +| other `[T]`, `{K:V}` | `Sequence[T]`, `Mapping[K, V]` in; `list[T]`, `dict[K, V]` out | value buffer | | interface | wrapper class | `c_void_p` | -| interface `?` | `Optional[...]` | `c_void_p`, NULL for `None` | +| interface `?` | `... \| None` | `c_void_p`, NULL for `None` | | callback interface | subclass of the generated ABC | context key plus vtable | -| callback interface `?` | `Optional[...]` | context key plus vtable, NULL for `None` | -| `iter` | `Iterator[T]` | iterator handle | +| callback interface `?` | `... \| None` | context key plus vtable, NULL for `None` | +| `iter` | `NativeIterator[T]` | iterator handle | + +`ctypes` truncates an out-of-range integer to the parameter's C width +without a word, so every integer argument is checked first: a value out of +range raises `OverflowError` naming the parameter +(`a: 2147483648 is out of range for i32`), and a non-integer raises +`TypeError`. The checks run before anything is lent or registered for the +call, so a failed check leaks nothing. + +An optional scalar (`i64?`, `bool?`, `Priority?`) never touches a value +buffer: `None` passes a false flag, and a returned flag says whether the +out value is present. A numeric list parameter accepts any sequence or +iterable of numbers (a `list`, a `tuple`, a `range`, a generator); it's +packed into an `array.array` with range checks and lent to the call, and an +`array.array` of the matching type code is lent as is, without a copy. A +`str` or `bytes` argument is refused rather than reinterpreted. Returned +typed arrays are copied into a `list` and released. Strings and bytes pass without per-byte loops: a `str` is encoded once and its bytes are lent to the call, so interior NUL characters survive. @@ -117,18 +157,41 @@ released with `{prefix}_free_bytes`. Every record and rich enum has one writer and one reader function (`_write_Entry`, `_read_Entry`), and so does every distinct composite type -the API uses (`_write_list_string`, `_read_map_string_Store`, -`_read_opt_Entry`); a call site makes one call instead of inlining a loop. -Lists of a fixed-width number type are packed and unpacked with one -`struct` call. A decoded buffer that's truncated, has trailing bytes, an -invalid flag, invalid UTF-8, an undeclared enum value, or a repeated map -key raises `InternalError` with code `-3`. +the API uses, named by the composite's canonical stem +(`_write_list_string`, `_read_map_string_Store`, `_read_opt_Entry`); a call +site makes one call instead of inlining a loop. Lists of a fixed-width +number type inside a buffer are packed and unpacked with one `struct` call. +A value out of range for its field raises `OverflowError` while encoding. A +decoded buffer that's truncated, has trailing bytes, an invalid flag, +invalid UTF-8, an undeclared enum value, or a repeated map key raises +`InternalError` with code `-3`. + +Records are `@dataclass(frozen=True, slots=True)`: they compare and hash +by value (a record holding a `list` or `dict` field compares by value but +can't be hashed), and a changed copy comes from `dataclasses.replace`: + +```python +entry = store.get("alpha") +renamed = dataclasses.replace(entry, tags=["archived"]) +``` + +Rich-enum variants work with `isinstance` and with `match`: + +```python +match change: + case kvstore.ChangePut(entry=entry, replaced=True): + print("replaced", entry.key) + case kvstore.ChangeRemoved(key=key): + print("removed", key) +``` All modules share one Python namespace. Nested modules' declarations are exported next to their parents' under their bare names (`summarize`, not `kv_stats_summarize`), which global names keep unique. The module starts with `from __future__ import annotations`, so annotations name types -declared later in the file without quotes. +declared later in the file without quotes. Backticked identifiers in IDL +docs and deprecation messages are rewritten to their Python spellings in +docstrings and `DeprecationWarning`s. ## Objects and lifetime @@ -164,13 +227,18 @@ the whole argument list has encoded, so a failed encoding leaks nothing. ## Errors -A call that declares errors (`throws`) raises the error domain in scope for -its module. Every such exception derives from the package's root `Error` -(named `{PascalName}Error` if the API declares its own `Error` type), which -carries `code` and `message`. A module's error domain becomes a subclass -(`KvError`) with one subclass per code (`KeyNotFound`), each also reachable -as `KvError.KeyNotFound` and carrying its stable `CODE`. A code's payload -fields are constructor arguments and attributes: +Every declared error derives from the package's root `Error` (named +`{PascalName}Error` if the API declares its own `Error` type), which +carries `code` and `message`. Each error domain is a subclass named by the +shared rule (`KvError` stays `KvError`, `KitchenErrors` becomes +`KitchenError`, `Failure` becomes `FailureError`), and a module may declare +several. Each code is a subclass of its domain named `{Code}Error` +(`KeyNotFoundError`), also reachable as `KvError.KeyNotFound`, carrying its +stable `CODE`. When `{Code}Error` would shadow a Python builtin +(`TimeoutError`) or another declaration, it's qualified with the domain's +stem (`KvTimeoutError`). A code's payload fields are constructor arguments +and attributes; a field named like an exception attribute (`message`, +`code`, `args`) gets a trailing `_`: ```python try: @@ -179,10 +247,17 @@ except kvstore.KvError.KeyNotFound as e: print(e.code, e.key, e.message) # 1001 nope key not found: nope ``` -A negative code on a throwing call (a panic, a marshalling failure, or a -failed callback) raises the root `Error` with that code; the constants are -`GENERIC_ERROR_CODE` (-1), `PANIC_ERROR_CODE` (-2), `MARSHAL_ERROR_CODE` -(-3), and `FOREIGN_ERROR_CODE` (-4). +What a failed call raises follows its `throws`: + +- `throws: SomeDomain`: the code's class. Codes are matched within the + callable's domain, so two domains may reuse a value. Domains are open: a + positive code these bindings don't know (from a newer library) raises the + domain's base class with its code and message. +- `throws: any`: the root `Error` with code -1 and the producer's message. +- A negative code on a throwing call (a panic, a marshalling failure, or a + failed callback) raises the root `Error` with that code; the constants + are `GENERIC_ERROR_CODE` (-1), `PANIC_ERROR_CODE` (-2), + `MARSHAL_ERROR_CODE` (-3), and `FOREIGN_ERROR_CODE` (-4). A call that declares no errors can only fail through a bug, so it follows the [trap policy](../guides/errors-and-memory.md#the-trap-policy): it @@ -191,6 +266,9 @@ hierarchy whose message names the code and the producer's message (`InternalError: (-3) ...`). A cancelled async call (-5) raises `asyncio.CancelledError` instead. +Error messages cross as UTF-8 bytes plus a length, so they may contain any +character; an empty message is `""`. + ## Async and cancellation An async function is an `async def` that must run on an `asyncio` event @@ -211,6 +289,9 @@ async def open_store(path: str) -> Store: return _result ``` +Optional scalar and typed-array results arrive directly (`int | None`, +`list[int]`), like sync returns. + For a `cancellable` function, each call owns a native cancel token. Cancelling the awaiting task (directly, through `asyncio.wait_for`, or by cancelling a `gather`) cancels the token, waits for the producer's @@ -224,62 +305,72 @@ A callback interface is an `abc.ABC` with one abstract method per IDL method. Passing an instance registers it in a handle table under an integer key, which the producer receives as `ctx` along with the address of the interface's one static vtable; `None` for an optional callback parameter -passes a null vtable. The vtable starts with the revision 4 header (its +passes a null vtable. The vtable starts with the revision 5 header (its `size`, `flags`, and the shared `free` entry), and the producer's `free(ctx)` removes the table entry, so an implementation lives exactly as -long as the producer holds it. +long as the producer holds it. `flags` is 0: the producer may call any +method from any thread. Trampolines run on whichever thread the producer calls from; `ctypes` acquires the GIL first. A trampoline adopts the object arguments first, -converts the rest, calls the method, and hands the return back: - -- a number, `bool`, or enum as the C return value; +converts the rest (an optional scalar arrives as `T | None`, a typed array +as a `list`), calls the method, and hands the return back: + +- a number, `bool`, or enum as the C return value, checked for its type + and range inside the trampoline (so a wrong return is reported as a + failure rather than truncated); +- an optional scalar through the `out_value` slot, with the C return + saying whether it's present; - an object as a fresh strong reference the producer adopts (`None` is allowed only for `I?`); -- a `str`, `bytes`, record, or other buffered value through the - `out_ptr`/`out_len` slots, in a run allocated with `{prefix}_alloc` that - the producer adopts and frees. +- a `str`, `bytes`, typed array, record, or other buffered value through + the `out_ptr`/`out_len` slots, in a run allocated with `{prefix}_alloc` + that the producer adopts and frees. ```python -def _Loader_load(ctx: Optional[int], key_ptr: Optional[int], key_len: int, out_ptr: Any, out_len: Any, out_err: Any) -> None: +def _Loader_load(ctx: int | None, key_ptr: int | None, key_len: int, out_ptr: Any, out_len: Any, out_err: Any) -> None: try: - _ret = _callback_get(ctx).load(_peek_bytes(key_ptr, key_len).decode("utf-8")) - _callback_return_bytes(out_ptr, out_len, bytes(_ret)) + _callback_return_bytes(out_ptr, out_len, bytes(_callback_get(ctx).load(_peek_bytes(key_ptr, key_len).decode("utf-8")))) except BaseException as exc: - _callback_fail(out_err, exc, KvError) + _callback_fail(out_err, exc, _GENERIC, KvError) ``` -Nothing unwinds through C. A method that declares errors may raise its -module's domain codes, which reach the producer as that code with the -fields as the error payload (`{prefix}_error_set` and -`{prefix}_error_set_payload`): +Nothing unwinds through C. A method that throws a domain may raise that +domain's codes, which reach the producer as that code with the fields as +the error payload (`{prefix}_error_set` and `{prefix}_error_set_payload`): ```python class EmptyLoader(kvstore.Loader): def name(self) -> str: return "empty" - def fallback(self, key: str) -> Optional[kvstore.Store]: + def fallback(self, key: str) -> kvstore.Store | None: return None def load(self, key: str) -> bytes: - raise kvstore.KeyNotFound(key=key, message="not in the loader") + raise kvstore.KeyNotFoundError(key=key) ``` -Any other exception, a return of the wrong type, or a domain error raised -by a method that doesn't declare errors reaches the producer as -`FOREIGN_ERROR_CODE` (-4) with the exception's message. A Rust producer -receives either as the `Err` of the method's `Result`. +A Rust producer decodes such a code into its typed error, so the caller of +the original function sees the domain's own message rendered from the +fields. Any other exception, or a return of the wrong type, reaches the +producer as code -1 with the exception's message (-4 for a method that +declares no errors); a Rust producer receives it as a `ForeignError`, which +the kvstore sample turns into `KvError.CallbackFailed`. ## Iterators -An `iter` return is a lazy iterator: each `next()` makes one native -`next` call through the pre-bound function, and the handle is released on -exhaustion, `close()`, or garbage collection. +An `iter` return is a `NativeIterator[T]`, a lazy iterator: each +`next()` makes one native `next` call through the pre-bound function, and +the handle is released on exhaustion, `close()`, the end of a `with` block, +or garbage collection. A closed iterator is exhausted. ```python for key in store.keys(None): print(key) + +with store.entries("user.") as entries: + first = next(entries) # the rest is released when the block ends ``` ## Packaging @@ -300,12 +391,17 @@ no wheel platform and are skipped. See [Packaging](../guides/packaging.md). loops that cross the boundary per element will feel it. The bindings keep the per-call work small (prototypes and vtable addresses are resolved at import, and the error check is inline), but each call still - allocates its error slot. -- `ctypes` truncates an out-of-range integer argument to the parameter's C - width instead of raising; inside a value buffer, `struct` raises. -- All modules share one namespace (see "Type mapping"). + allocates its error slot, and each integer argument costs a range check. +- A typed-array parameter is copied into an `array.array` unless it already + is one of the matching type code; a NumPy array is copied element by + element. +- All modules share one namespace (see "Type mapping"), which also holds + the typing names the module imports (`Any`, `Callable`, `ClassVar`, + `Iterable`, `Iterator`, `Mapping`, `Sequence`): a declared type with one + of those names still works at run time, but type checkers reject the + module. - A long-running callback stalls the producer thread that called it, and a callback that waits for the thread blocked in the calling Python code deadlocks. -- `iter` returns are annotated as `Iterator[T]`, so a type checker - doesn't know about their `close()` method. +- `LibraryLoadError` is raised during the import, so it can only be caught + as `ImportError` (the package isn't importable to name the class). diff --git a/docs/src/generators/ruby.md b/docs/src/generators/ruby.md index d815857c..6fb97e68 100644 --- a/docs/src/generators/ruby.md +++ b/docs/src/generators/ruby.md @@ -1,9 +1,14 @@ # Ruby -The Ruby target generates a pure-Ruby gem that binds the C ABI (revision 4) +The Ruby target generates a pure-Ruby gem that binds the C ABI (revision 5) through the [ffi](https://github.com/ffi/ffi) gem. There's no native -extension to compile: the gem depends on `ffi ~> 1.15` and loads the -producer's shared library at `require` time. Ruby 2.7 or newer is required. +extension to compile: the gem depends on `ffi ~> 1.16` and loads the +producer's shared library at `require` time. Ruby 3.2 or newer is required +(the gemspec says so in `required_ruby_version`). + +Ruby is a [Tier 2](../stability.md#target-tiers) target: it may lag a new +ABI revision for a while, but it passes the same conformance suite as every +other target before a release. ## What gets generated @@ -11,12 +16,12 @@ For a package named `kvstore` (prefix `kvstore`, library `kvstore`): ```text ruby/ - kvstore.gemspec gem `kvstore`, depends on ffi ~> 1.15 + kvstore.gemspec gem `kvstore`, Ruby >= 3.2, depends on ffi ~> 1.16 README.md lib/ kvstore.rb require 'kvstore': module Kvstore, the bindings kvstore/ - runtime.rb loader, load-time checks, errors, codec, object base + runtime.rb loader, load-time checks, errors, marshalling ``` Names come from the package identity: @@ -28,12 +33,38 @@ Names come from the package identity: | `require` path | the C prefix (`key_store`) | none | Every IDL module, nested ones included, lands in the one Ruby module, and a -function keeps its bare name (`render_report` in module `report`), which -global names keep collision-free. +function keeps its bare name (`render_report` in module `report`). Type and +function names are global in the IDL (validation rejects duplicates), so +nothing collides, and an API with one module (most of them) doesn't stutter +(`Calculator.add`, not `Calculator::Calculator.add`). + +The module's public surface is only the idiomatic API: error classes, enum +constants, records, rich enums, interface classes, callback-interface +modules, and module functions. The C functions are attached in a private +`Native` module, and the marshalling the wrappers share lives in a private +`Bridge` module (both are `private_constant`, so `Kvstore::Native` raises +`NameError`). `lib/{prefix}/runtime.rb` is fixed code shipped with the +generator (the source lives in `targets/ruby/runtime/runtime.rb`); only the +module name, the C prefix, and the library names are substituted into it. + +A wrapper converts its arguments and hands the C call to one runtime helper +as a block, so the generated code stays short: + +```ruby +def get(key) + key_ptr = Bridge.string_arg(key) + Bridge.pin(self) do |self_| + Bridge.call(KvError, [:buffer, Entry]) do |out_len, out_err| + Native.kvstore_kv_Store_get(self_, key_ptr, key_ptr.bytesize, out_len, out_err) + end + end +end +``` -`lib/{prefix}/runtime.rb` is fixed code shipped with the generator (the -source lives in `targets/ruby/runtime/runtime.rb`); only the module name, -the C prefix, and the library names are substituted into it. +`Bridge.call` allocates the error and out slots, raises per the function's +error domain, and receives the result (here, decodes an `Entry` from the +returned value buffer and releases it); `Bridge.pin` keeps the receiver +alive for the call. ## Install, build, and load @@ -70,30 +101,36 @@ with `gem push`; Android, iOS, and `wasm32` builds are skipped. See ## Load-time checks `require` checks, before anything else is attached, that the library -implements C ABI revision 4, and then that every declaration the bindings +implements C ABI revision 5, and then that every declaration the bindings were generated with is in its top-level module's [contract table](../reference/abi.md#load-time-checks) with the same -signature. The expected entries are embedded in the bindings: +signature. The expected rows are embedded in the bindings, each with its +canonical signature as a comment: ```ruby module Kvstore - _wv_check_contract!( + Bridge.check_contract!( kvstore_kv_contract: [ - [0x0969575bfbb012d7, 0xebd38766e3532c4f, 'kv.Store.fork'], - # ...one entry per declaration in `kv` and its submodules + # method get(string) -> Entry throws KvError + [0x..., 0x..., 'kv.Store.get'], + # ...one row per declaration, error code, and callback method in `kv` ], kvstore_report_contract: [ - [0xa21a2e7274bf28c6, 0x342e0b83b12d73ec, 'report.render_report'], - [0xd504fae45f64ab45, 0xd430b8de9d574a4c, 'report.ReportError'], + # ... ], ) ``` -A failed check raises `LoadError` naming the declaration -(`kvstore: kv.Store.put is missing from the library` or +Any failure raises `Kvstore::LoadError`, a subclass of Ruby's `LoadError`, +so both `rescue Kvstore::LoadError` and a plain `rescue LoadError` around +the `require` catch it: a library that can't be opened, a different ABI +revision, a missing contract table, or a declaration that's missing or +changed (`kvstore: kv.Store.put is missing from the library`, `kvstore: kv.Store.put changed since these bindings were generated`). -Declarations the library has and the bindings don't are fine, so a library -that only adds to its API keeps working with older bindings. +Declarations, error codes, and callback methods the library has and the +bindings don't are fine, so a library that only adds to its API keeps +working with older bindings. A failed `require` isn't recorded as loaded, +so it can be retried (after setting `{PREFIX}_LIBRARY`, for example). ## Type mapping @@ -105,10 +142,11 @@ that only adds to its API keeps working with older bindings. | `string` | `String` (UTF-8) | `:pointer` + `:size_t` | | `bytes` | `String` (binary) | `:pointer` + `:size_t` | | C-style enum | `Integer` constants in a module | `:int32` | -| record | plain value class with keyword `initialize` and `==` | value buffer | -| rich enum | base class with one nested class per variant | value buffer | -| `T?` | `T` or `nil` | value buffer | -| `[T]`, `{K: V}` | `Array`, `Hash` | value buffer | +| record | `Data` class | value buffer | +| rich enum | module of `Data` variant classes | value buffer | +| `T?` of a scalar, `bool`, or C-style enum | `T` or `nil` | `:bool` + `T` (no buffer) | +| `[T]` of a numeric scalar (not `u8`, not `bool`) | `Array` of `Integer` or `Float` | `:pointer` + `:size_t` (a typed array) | +| any other `T?`, `[T]`, `{K: V}` | `T` or `nil`, `Array`, `Hash` | value buffer | | interface | wrapper class | `:pointer` | | `Interface?` | wrapper or `nil` | `:pointer` (NULL for `nil`) | | callback interface | any object with the methods | context + vtable | @@ -118,60 +156,95 @@ that only adds to its API keeps working with older bindings. Trailing optional parameters (`T?`, `Interface?`, `Cb?`) default to `nil`, so `store.keys` is `store.keys(nil)`. -Strings cross as a pointer plus a length, so interior NUL characters -survive. A string argument must be a `String` (or convert with `to_str`) -holding valid UTF-8; anything else raises `TypeError` or `ArgumentError` -before the call. Returned strings are copied, tagged UTF-8, and released -with `{prefix}_free_bytes`. Integers written into a value buffer are -range-checked (`RangeError` rather than silent wrapping), and an object -argument must be a wrapper of the declared interface (`TypeError` -otherwise). - -Value buffers use the [value-buffer format](../reference/value-buffers.md), -encoded and decoded by the runtime's `WvBufferWriter` and `WvBufferReader` -and by generated codec methods: one `_wv_write_*`/`_wv_read_*` pair per -record and rich enum, and one per distinct composite type the API uses -(`[Entry?]` is `_wv_write_list_opt_entry`, `{string:string}` is -`_wv_write_map_string_string`), so no call site inlines a loop. +Arguments are checked before the call: + +- An integer (or a C-style enum value) must be an `Integer` in its type's + range: `RangeError` rather than silent wrapping, `TypeError` for a + `Float`. That holds for scalars, optional scalars, typed-array elements, + and integers inside value buffers. +- A string must be a `String` (or convert with `to_str`) holding valid + UTF-8; a string in another encoding is converted. Anything else raises + `TypeError` or `ArgumentError`. Strings cross as a pointer plus a length, + so interior NUL characters survive. Returned strings are copied, tagged + UTF-8, and released. +- A typed-array argument must be an `Array` (or convert with `to_ary`); its + range-checked elements are packed into an aligned native array + (`FFI::MemoryPointer#put_array_of_*`) the library borrows for the call. A + typed-array return is copied into a new `Array` and released. +- An optional scalar crosses as a presence flag plus the value, never + through a value buffer: `nil` in, `nil` out. +- An object argument must be a wrapper of the declared interface, and a + record argument an instance of the record's class (`TypeError` + otherwise). + +Records are `Data` classes: immutable, constructed with keywords or +positionally, with value equality and hashing, `with`, `to_h`, and pattern +matching: + +```ruby +entry = Kvstore::Entry.new(key: 'k', value: 'v'.b, kind: Kvstore::EntryKind::VOLATILE, + version: 1, expires_at: nil, tags: [], metadata: {}) +entry.with(version: 2) +``` + +A rich enum is a module whose variants are `Data` classes that include it, +so `change.is_a?(Kvstore::Change)` holds and `case change in +Kvstore::Change::Put(entry:)` works; `change.tag` (and each variant's `TAG`) +is the wire tag. + +Value buffers use the [value-buffer format](../reference/value-buffers.md). +Nothing per composite type is generated: the runtime's codec walks a +literal type description (`[:list, [:opt, Entry]]`, `[:map, :string, :i64]`), +and each record, variant, and error payload registers its field layout +once, at the end of the bindings (`Bridge.record(Entry, key: :string, ...)`). + +A field named like a method a record or error relies on gains a trailing +`_` (`hash_`, `to_h_`; an error field `message` is `message_`), and so does +a type named like a constant the bindings use (`Error_`, `Data_`, `FFI_`); +keywords are fine as member names (`entry.end`). See +[reserved member names](../reference/naming.md#identifiers-in-generated-code). ## Objects and lifetime -Each interface becomes a subclass of the runtime's `WvObject`, holding +Each interface becomes a subclass of the runtime's `Bridge::Handle`, holding one strong reference in an `FFI::AutoPointer`: ```ruby - # An embedded key-value store. - class Store < WvObject - WV_PTR = StorePtr - - # Create an empty store with the path `memory`. - def initialize + # An embedded key-value store. Each object owns its entries, logical + # clock, listeners, and policy; the last release drops them (and frees + # the consumer's callbacks). + class Store < Bridge::Handle ``` - `close` releases the reference now and is idempotent; otherwise the GC finalizer releases it. The object itself lives until its last reference anywhere (another wrapper, a record field, the library) goes. +- `closed?` says whether the wrapper was closed. +- Two open wrappers of the same native object are `==` (and `eql?`, with + equal `hash`), so a record holding objects compares as expected. - `dup` and `clone` make an independent wrapper with its own reference. - A constructor named `new` becomes `initialize`; other constructors are class methods (`Store.open`). An interface without one hides `new`. -- Every call pins its receiver and object arguments (`_wv_pin`), so a - `close` from another thread or from a callback during the call takes - effect only when the call returns. +- Every call pins its receiver and object arguments, so a `close` from + another thread or from a callback during the call takes effect only when + the call returns. - An object inside a value buffer carries its own reference, minted just before the call; an object decoded from one is adopted into a new wrapper. - Using a closed wrapper raises `Error` ("Kvstore::Store used after close"). -- A method spelled like one the wrapper relies on (`close`, `handle`, - `initialize`, `class`, `dup`, `hash`, ...), or a static or factory named - `allocate` or `name`, gains a trailing `_`: an IDL method `close` is - `close_` ([reserved member names](../reference/naming.md#identifiers-in-generated-code)). +- A method spelled like one the wrapper relies on (`close`, `initialize`, + `class`, `dup`, `hash`, `inspect`, ...), or a static or factory named + `allocate`, `new`, or `name`, gains a trailing `_`: an IDL method `close` + is `close_`. ## Errors -Every error derives from `{Module}::Error`, which carries `code`. A module -with an error domain gets a domain class (`KvError < Error`) with one -nested class per code (`KvError::KeyNotFound`, carrying `CODE` and any -payload fields as attributes): +Every error a call reports derives from `{Module}::Error` (a +`StandardError`), which carries `code`. Each error +domain becomes a class under it, named by the shared rule (`KvError` stays +`KvError`, `KitchenErrors` becomes `KitchenError`, `Failure` becomes +`FailureError`), with one nested class per code. A code's class carries +`CODE`, its documented `MESSAGE`, and a reader per payload field: ```ruby begin @@ -183,26 +256,51 @@ rescue Kvstore::KvError::KeyNotFound => e end ``` +A module may declare several domains, and a function names the one it +throws, so codes are only unique per domain: `Calculator.divide` raises +`CalcError::DivisionByZero` for code 1 and `Calculator.parse` raises +`ParseError::NotANumber` for its code 1. Domains are open: a positive code +these bindings don't know (a newer library added it) raises the domain +class itself, with the code and the library's message. + What a failure raises follows the [trap policy](../guides/errors-and-memory.md#the-trap-policy): | Call | Domain code | Runtime code (`-1` to `-4`) | `-5` | |------|-------------|-----------------------------|------| -| declared `throws` | the code's class (`KvError::Expired`) | `Error` with that code and the library's message | `Cancelled` | -| not `throws` | not possible | `NativeBugError` | `Cancelled` | +| `throws: Domain` | the code's class (`KvError::Expired`), or the domain class for an unknown code | `Error` with that code and the library's message | `Cancelled` | +| `throws: any` | not possible | `Error` (a failure is code `-1` and its message) | `Cancelled` | +| no `throws` | not possible | `NativeBugError` | `Cancelled` | `NativeBugError < Error` marks a bug in the library (a panic, an argument it couldn't accept, a callback failure it let through a call that can't report one), never an outcome to handle. Its message names the code and the library's message, as in `native call failed with code -2: {the panic message}`; a malformed value the library returned raises it with code `-3`. +The runtime codes are constants on `Error` (`Error::GENERIC`, +`Error::PANIC`, `Error::MARSHAL`, `Error::CALLBACK`, `Error::CANCELLED`). + +An error code's class can be raised from a callback implementation; its +payload fields are required keywords: + +```ruby +raise Kvstore::KvError::Rejected.new('read-only', key: entry.key, reason: 'read-only') +``` ## Async and cancellation -An async function blocks the calling thread until the library's completion -fires, then returns the result or raises. The wait is a `Queue#pop`, which -releases the GVL, so other threads keep running; run the call in a `Thread` -for concurrency. A cancellable function takes a `cancel:` keyword: +An async function blocks its caller until the library's completion fires, +then returns the result or raises. The wait is a `Thread::Queue#pop`, so: + +- It releases the GVL; other threads keep running. Run calls in threads + for concurrency. +- Under a Fiber scheduler (`Fiber.set_scheduler`, such as the `async` gem's), + `Queue#pop` in a non-blocking Fiber yields to the scheduler, so only the + calling Fiber waits and the thread's other Fibers keep running; the + completion wakes it through the scheduler's `unblock`. The conformance + suite checks this with two overlapping calls. + +A cancellable function takes a `cancel:` keyword: ```ruby token = Kvstore::CancelToken.new @@ -220,9 +318,13 @@ end `Cancelled` (code `-5`) unless it already completed. A token stays cancelled, can be shared by several calls, and is released by `close` or the GC. Without a token, interrupting the waiting thread (`Thread#raise`, -`Timeout`) still cancels the library's work through a private token. Each -async function has one completion trampoline constant, so nothing the -library may still call is ever garbage-collected. +`Timeout`) still cancels the library's work through a private token. The +completion functions are shared by every async call with the same result +slots and live for the process, so nothing the library may still call is +ever garbage-collected. + +Optional scalars and typed arrays arrive as async results directly (a flag +and a value, or a typed array the runtime copies and releases). ## Callback interfaces @@ -256,58 +358,66 @@ entry = store.get_or_load('settings', FileLoader.new) Passing an implementation stores it in a handle table and hands the library its key, so Ruby object addresses never cross the boundary; the entry is removed when the library calls the vtable's `free`, which may happen on any -library thread. Each interface has one static vtable (an `FFI::Struct` whose -`{size, flags, free}` header is followed by one trampoline per method). A -required callback parameter rejects `nil` with `TypeError`; an optional one +library thread. Each interface has one vtable for the life of the process +(the `{size, flags, free}` header, then one trampoline per method), with +`flags` 0: the methods may be called from any thread. A required callback +parameter rejects `nil` with `TypeError`; an optional one (`loader = nil`) passes a null vtable. Arguments and returns follow the [ABI](../reference/abi.md#callback-interfaces): -- Strings, bytes, and value buffers arrive as copies; an object argument - arrives as a wrapper that adopted the reference the library passed, which - the implementation may keep. -- A returned number, boolean, or C-style enum crosses by value (checked - against its type's range); a returned object (`Store` or `Store?`) as a - fresh reference the library adopts (`nil` is null, which the library - rejects with `-3` where the return isn't optional); and a returned string, - bytes, or value buffer (a record, a rich enum, an optional, a list, or a - map) as a run allocated with `{prefix}_alloc`, which the library adopts. -- A method declared `throws` in the IDL may raise one of its module's - domain error classes: the library receives its code, its message, and its - fields as the payload, exactly as if the producer had raised it. Any other - exception (including `NotImplementedError`, and a domain error from a - method not declared `throws`) reaches the library as a callback failure - (`-4`) with the exception's message. Nothing unwinds through C. +- Strings, bytes, value buffers, and typed arrays arrive as copies; an + optional scalar arrives as the value or `nil`; an object argument arrives + as a wrapper that adopted the reference the library passed, which the + implementation may keep. +- A returned number, boolean, C-style enum, or optional scalar crosses by + value (checked against its type's range); a returned object (`Store` or + `Store?`) as a fresh reference the library adopts (`nil` is null, which + the library rejects where the return isn't optional); and a returned + string, bytes, typed array, or value buffer as a run allocated with + `{prefix}_alloc`, which the library adopts. +- A method that throws a domain may raise that domain's code classes: the + library receives the code and the fields as the payload, exactly as if + the producer had raised it (the caller then sees the domain's own message + for those fields). Any other exception, including `NotImplementedError`, + is reported as code `-1` with its message (`-4` for a method that declares + no errors). Nothing unwinds through C. ### Threads and the GVL Every call into the library except the trivial runtime helpers (`_clone`, `_destroy`, an iterator's `_destroy`, `{prefix}_free_bytes`, `{prefix}_alloc`, `{prefix}_debug_live`, the error and cancel-token functions, and the -load-time checks) releases the GVL -(`blocking: true`), so a long call doesn't stall other Ruby threads. That's -also what lets a callback arrive while the call that triggered it is in -flight: a callback on the calling thread reacquires the GVL there, and one -from a library thread (such as the notifications `compact` sends from its -worker) runs on the ffi gem's callback thread, which needs the GVL too. +load-time checks) releases the GVL (`blocking: true`), so a long call +doesn't stall other Ruby threads. That's also what lets a callback arrive +while the call that triggered it is in flight: a callback on the calling +thread reacquires the GVL there, and one from a library thread (such as the +notifications `compact` sends from its worker) runs on the ffi gem's +callback thread, which needs the GVL too. ## Iterators -An `iter` return is a lazy `Enumerator`. The library's iterator starts -on the first pull, each step makes one `next` call, and the handle is -destroyed exactly once, when iteration finishes, raises, or stops early -(`first(2)`, `break`). +An `iter` return is a lazy `Enumerator`, with every `Enumerable` method. +Each enumeration launches its own native iterator on its first pull, makes +one `_next` call per element, and destroys the iterator exactly once, when +iteration finishes, raises, or stops early (`first(2)`, `break`), so the +same `Enumerator` can be enumerated again. An external enumeration (`next`) +releases its iterator the same way when it reaches the end or raises; one +abandoned midway can't run its `ensure`, so the iterator handle is an +`FFI::AutoPointer` whose GC finalizer destroys it once the suspended +enumeration is collected. Either path destroys the iterator exactly once. +Elements that are optional scalars or typed arrays arrive directly, like +returns. ## Known limitations -- Async calls block the calling thread; there's no Fiber-scheduler or - promise-based surface. -- An `Enumerator` driven externally with `next` and then abandoned - releases its iterator only when the GC collects it. +- Async calls block the calling thread (or, under a Fiber scheduler, the + calling Fiber); there's no promise-based surface. - Callback interfaces are duck-typed: a missing method is detected only - when the library calls it. + when the library calls it (and reported to it as a failure). - A callback that blocks waiting for the thread that made the triggering call deadlocks. -- A deprecated callable warns on every call (`warn ... uplevel: 1`); - silence it with `$VERBOSE = nil` like any Ruby warning. +- A deprecated callable warns on every call in the `:deprecated` warning + category, which Ruby hides by default; enable it with + `Warning[:deprecated] = true` (or `ruby -W:deprecated`). diff --git a/docs/src/generators/swift.md b/docs/src/generators/swift.md index ce9b193f..b566b458 100644 --- a/docs/src/generators/swift.md +++ b/docs/src/generators/swift.md @@ -1,11 +1,15 @@ # Swift -The Swift target emits a standalone SwiftPM package that wraps the C ABI in -idiomatic Swift: `throws` for declared errors, plain `Hashable` structs and -enums for values, ARC-managed `final class` wrappers for objects, class-bound +The Swift target emits a standalone SwiftPM package that wraps the +[C ABI](../reference/abi.md) (revision 5) in idiomatic Swift: `throws` with +typed `Error` enums for declared errors, plain `Hashable` structs and enums +for values, ARC-managed `final class` wrappers for objects, class-bound protocols for callback interfaces, `async` functions with task cancellation for async callables, and lazy `Sequence`s for iterators. +Swift is a [Tier 1](../stability.md#target-tiers) target: it tracks every +ABI revision as it lands and runs the full conformance suite in CI. + ## What gets generated For a package named `kvstore`: @@ -15,23 +19,25 @@ swift/ Package.swift Sources/ CKvstore/ - kvstore.h # copy of the C header - module.modulemap # system library: links `kvstore` + kvstore.h # copy of the C header + module.modulemap # system library: links `kvstore` Kvstore/ - Kvstore.swift # runtime support and the wrapper API + Kvstore.swift # the wrapper API + WeaveFFIRuntime.swift # runtime support: load check, codec, errors ``` The package, product, and Swift module are the package name in PascalCase (`key-store` becomes `KeyStore`); set `[generators.swift] name` to override it. The C module is always the Swift module's name with a `C` prefix. The C symbol prefix and the native library name come from the package -identity. +identity. `WeaveFFIRuntime.swift` depends only on those names, so it changes +only when they do. -Types (records, enums, error domains, interfaces, callback protocols, and -iterator classes) sit at file scope, since type names are unique across an -API. Each IDL module's free functions become `static func`s of a caseless -namespace `enum` named after the module in PascalCase, a submodule's enum -nests inside its parent's, and the module's doc comment documents its enum: +Types (records, enums, error domains, interfaces, and callback protocols) +sit at file scope, since type names are unique across an API. Each IDL +module's free functions become `static func`s of a caseless namespace `enum` +named after the module in PascalCase, a submodule's enum nests inside its +parent's, and the module's doc comment documents its enum: ```swift /// An embedded key-value store with listeners, policies, read-through @@ -48,7 +54,10 @@ public enum Kv { Parameters, methods, record fields, and enum cases are lowerCamelCase (`expiresAt`, `.keyNotFound`), and every argument is labeled -(`Kv.Stats.summarize(store: store, prefix: nil)`). +(`Kv.Stats.summarize(store: store, prefix: nil)`). Doc comments carry over, +with backticked API names in their Swift spelling (`` `new_op` `` becomes +`` `newOp` ``), and deprecated declarations get +`@available(*, deprecated, message:)`. | `[generators.swift]` key | Default | Meaning | |---|---|---| @@ -58,9 +67,11 @@ Parameters, methods, record fields, and enum cases are lowerCamelCase ## Build and link -The package builds as-is with SwiftPM (its manifest declares -`swift-tools-version:5.9`). Depend on it by path or from a repository, and -tell the linker where `libkvstore` lives: +The package builds as-is with SwiftPM 5.9 or later (its manifest declares +`swift-tools-version:5.9`, so it compiles in the Swift 5 language mode); the +sources also compile without warnings in the Swift 6 language mode, with +complete concurrency checking. Depend on it by path or from a repository, +and tell the linker where `libkvstore` lives: ```swift dependencies: [.package(path: "bindings/swift")], @@ -94,32 +105,49 @@ and publish the package; apps on any Apple platform then link with no flags Before the first call (the first constructor, static, or free function), the bindings check, once per process, that the library implements C ABI -revision 4 and that every declaration they were generated with is in its +revision 5 and that every declaration they were generated with is in its top-level module's [contract table](../reference/abi.md#load-time-checks) -with the same signature. The expected entries are embedded in the wrapper: +with the same signature. The expected rows are embedded in the wrapper, one +per function, member, type, callback method, error domain, and error code: ```swift -let wvContract: Void = { - wvCheckAbiVersion() - wvCheckContract(kvstore_kv_contract, [ +func wvCheckContracts() -> KvstoreLibrary.LoadError? { + if let failure = wvCheckContract(kvstore_kv_contract, [ (0x0969575bfbb012d7, 0xebd38766e3532c4f, "kv.Store.fork"), - // ...one entry per declaration in `kv` and its submodules - ]) - wvCheckContract(kvstore_report_contract, [ + // ...one row per declaration in `kv` and its submodules + ]) { + return failure + } + return wvCheckContract(kvstore_report_contract, [ // ... ]) -}() +} ``` -Declarations the library adds are fine. A missing or changed one stops the -process with a message naming it, since a stale binding would otherwise -corrupt memory: +Declarations, error codes, and callback methods the library adds are fine. +To handle a mismatch, call `{Module}Library.check()` at startup. It runs the +checks (or returns their cached result) and throws a `LoadError` naming the +first mismatch: + +```swift +do { + try KvstoreLibrary.check() +} catch let error as KvstoreLibrary.LoadError { + // .abiMismatch(found:expected:), .missing(declaration:), or .changed(declaration:) + print(error.localizedDescription) +} +``` ```text Kvstore: kv.Store.put is missing from the library 'kvstore'; regenerate the bindings or rebuild the library Kvstore: kv.Store.put changed since these bindings were generated; regenerate the bindings or rebuild the library 'kvstore' ``` +A call made while the library doesn't match stops the process with the same +message, whether or not `check()` ran, since a stale binding would otherwise +corrupt memory. `KvstoreLibrary.abiVersion` is the revision the bindings +need. + ## Type mapping | IDL type | Swift type | At the ABI | @@ -129,19 +157,28 @@ Kvstore: kv.Store.put changed since these bindings were generated; regenerate th | `bool` | `Bool` | C `bool` | | `string` | `String` | UTF-8 pointer and length | | `bytes` | `Data` | Pointer and length | -| C-style enum | `enum E: Int32, Sendable` | `int32_t` | -| Record | `struct`, `Sendable` (and `Hashable` when every field is) | Value buffer | -| Rich enum | `enum` with associated values | Value buffer | -| `T?`, `[T]`, `{K: V}` | `T?`, `[T]`, `[K: V]` | Value buffer | -| Interface | `final class`, `@unchecked Sendable` | Object pointer | +| C-style enum | `enum E: Int32, CaseIterable, Sendable` | `int32_t` | +| Record | `struct`, `Hashable` and `Sendable` | Value buffer | +| Rich enum | `enum` with associated values, `Hashable` and `Sendable` | Value buffer | +| `T?` of a number, `bool`, or C-style enum | `T?` | Presence flag and value | +| `[T]` of a number other than `u8` | `[T]` | Typed array: pointer and element count | +| Any other `T?`, `[T]`, `{K: V}` | `T?`, `[T]`, `[K: V]` | Value buffer | +| Interface | `final class`, `Hashable` by identity, `@unchecked Sendable` | Object pointer | | `Interface?` | optional wrapper | Nullable pointer | | Callback interface | `any P`, where `protocol P: AnyObject, Sendable` | Context and vtable | | `Callback?` | `(any P)?` | Context and nullable vtable | -| `iter` | generated `Sequence` class | Iterator handle | - -Strings cross as pointer and length in both directions, so interior NULs -survive. Value buffers decode in place from the library's memory before it's -released. Floats round-trip bit for bit, including NaN and `-0.0`. +| `iter` | `NativeSequence` | Iterator handle | + +An optional scalar (`Int64?`, `Bool?`, `Priority?`) crosses as a flag and a +value in every position, never through a buffer. A numeric array parameter +lends the array's own storage for the call (`withUnsafeBufferPointer`, no +copy); a returned array is copied once out of the library's run, which is +then released. Strings cross as pointer and length in both directions, so +interior NULs survive. Value buffers decode in place from the library's +memory before it's released. Floats round-trip bit for bit, including NaN +and `-0.0`. `usize` and `isize` in a Rust producer are `u64` and `i64` +(`UInt64`, `Int64`); a `char` and a custom type cross as their IDL type +(usually `String`). Every type that crosses inside a value buffer conforms to the internal `WvCodable` protocol: the primitives in the runtime, `Optional`, `Array`, and @@ -170,7 +207,7 @@ releases it, a constructor named `new` becomes `init`, and other constructors and statics become `static func`s: ```swift -public final class Gadget: @unchecked Sendable { +public final class Gadget: WvObject, Hashable, @unchecked Sendable { let ptr: OpaquePointer init(ptr: OpaquePointer) { @@ -181,12 +218,19 @@ public final class Gadget: @unchecked Sendable { kitchen_sink_kitchen_Gadget_destroy(ptr) } - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it. func clonePtr() -> OpaquePointer { wvNonNull(kitchen_sink_kitchen_Gadget_clone(ptr)) } + /// Whether two wrappers hold the same native object. + public static func == (lhs: Gadget, rhs: Gadget) -> Bool { + lhs.ptr == rhs.ptr + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ptr) + } + /// Create a gadget with the given id public init(id: Int64) { wvLoad() @@ -200,7 +244,7 @@ public final class Gadget: @unchecked Sendable { public func describe() -> String { var err = WvError() var outLen = 0 - let rv = kitchen_sink_kitchen_Gadget_describe(ptr, &outLen, &err) + let rv = kitchen_sink_kitchen_Gadget_describe(self.ptr, &outLen, &err) wvTrap(&err) return wvTakeString(rv, outLen) } @@ -211,35 +255,62 @@ Swift keeps every argument and the receiver alive for the whole call, so a wrapper can't be freed mid-call. Writing an object into a value buffer or returning it from a callback method clones its reference, and reading one adopts the reference into a new wrapper, so two wrappers can share one native -object. There's no `close()`: release happens when the last Swift reference -goes away. An interface member spelled like one the class declares (`ptr`, -`clonePtr`, `wvRead`, `wvWrite`) gains a trailing `_` ([reserved member +object. Wrappers are `Equatable` and `Hashable` by that native identity: two +wrappers are `==` exactly when they hold the same object (`_clone` keeps the +pointer value), whatever its state. So a record or enum carrying objects is +`Hashable` too, and `store.share() == store`. There's no `close()`: release +happens when the last Swift reference goes away. An interface member spelled +like one the class declares (`ptr`, `clonePtr`, `wvRead`, `wvWrite`) gains a +trailing `_` ([reserved member names](../reference/naming.md#identifiers-in-generated-code)). ## Errors -A module's error domain becomes an `enum` conforming to `Error`, -`LocalizedError`, and `Sendable`, named after the domain with an `Error` -suffix unless it already has one (`KvError` stays `KvError`; `KitchenErrors` -becomes `KitchenErrorsError`). It has one case per code, carrying the message -(the code's documented message when the library sends none) and any payload -fields. `errorCode` returns the numeric code: +Each error domain becomes an `enum` conforming to `Error`, `LocalizedError`, +`Hashable`, and `Sendable`, named through the shared naming rule: the +domain's name with one `Error` suffix (`KvError` stays `KvError`, +`KitchenErrors` becomes `KitchenError`, `Failure` becomes `FailureError`). A +module may declare several. Each code is a case carrying the message (the +code's documented message when the library sends none) and the code's +fields; `errorCode` and `message` read them back: ```swift -public enum KvError: Error, LocalizedError, Sendable { +public enum KvError: Error, LocalizedError, Hashable, Sendable { + /// key not found case keyNotFound(message: String, key: String) + /// entry expired case expired(message: String, key: String, expiredAt: Int64) - case invalidPath(message: String) // ... + /// A code these bindings don't declare, from a newer library. + case unknown(code: Int32, message: String) +} +``` + +Domains are open: a code the producer added after the bindings were +generated arrives as `.unknown` with its code and message, never as a crash, +so a `switch` over a domain needs that case (or a `default`). A field named +`message` gains a trailing `_` (`case callbackFailed(message: String, +message_: String)`), and so does a code spelled like a member of the enum +(`errorCode`, `message`, `errorDescription`) or like `unknown` (the +catch-all case then becomes `unknown_`). + +A function declared `throws: KvError` raises that enum for the domain's codes +and `{SwiftModule}RuntimeError` (`KvstoreRuntimeError`, with `errorCode` and +`message`) for runtime failures: `-2` panic, `-3` marshalling, and `-4` when a +callback implementation failed. A function declared `throws: any` raises the +runtime error with code `-1` (`KvstoreRuntimeError.untypedCode`) and the +producer's message: + +```swift +do { + _ = try store.importLines(text: "c=3\nbroken") +} catch let error as KvstoreRuntimeError { + print(error.errorCode, error.message) // -1 line 2: expected key=value } ``` -A function declared `throws` raises that enum for domain codes and -`{SwiftModule}RuntimeError` (`KvstoreRuntimeError`, with `errorCode` and -`message`) for unknown codes and runtime failures: -`-1` generic, `-2` panic, `-3` marshalling, and `-4` when a callback -implementation failed. Swift has no unchecked errors, so a function that -doesn't declare `throws` follows the [trap policy](../guides/errors-and-memory.md#the-trap-policy): +Swift has no unchecked errors, so a function that doesn't declare `throws` +follows the [trap policy](../guides/errors-and-memory.md#the-trap-policy): it stops the process with `fatalError` when the library reports a failure, naming the function, the code, and the message: @@ -253,12 +324,13 @@ An async function becomes an `async` function over a checked continuation; the library's completion callback resumes it exactly once, from any thread. It's marked `throws` only when the IDL function declares errors or is cancellable; otherwise a failure follows the trap policy -(`Kv.Stats.summarizeAll(stores:)` is `async -> Kvstore.Stats`). A -cancellable function runs inside -`withTaskCancellationHandler`: it creates a native cancel token, passes it to -the launch, and cancels it when the calling task is cancelled. A cancelled -call (code `-5`) throws `CancellationError`, even when the task was cancelled -before the call started: +(`Kv.Stats.summarizeAll(stores:)` is `async -> Kvstore.Stats`). Optional +scalars and numeric arrays arrive directly here too +(`versionOf(key:) async -> UInt32?`, `versions(keys:) async -> [UInt32]`). A +cancellable function runs inside `withTaskCancellationHandler`: it creates a +native cancel token, passes it to the launch, and cancels it when the calling +task is cancelled. A cancelled call (code `-5`) throws `CancellationError`, +even when the task was cancelled before the call started: ```swift public static func doCancellable(input: String) async throws -> String { @@ -268,13 +340,13 @@ public static func doCancellable(input: String) async throws -> String { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() wvWithUTF8(input) { input_ptr, input_len in - kitchen_sink_kitchen_do_cancellable(input_ptr, input_len, token.raw, { context, err, resultPtr, resultLen in + kitchen_sink_kitchen_do_cancellable(input_ptr, input_len, token.raw, { context, err, result_ptr, result_len in let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value if let err = err { - cont.resume(throwing: wvTakeError(err, wvCancelledOrTrap)) + cont.resume(throwing: wvTakeCancellation(err)) return } - cont.resume(returning: wvTakeString(resultPtr, resultLen)) + cont.resume(returning: wvTakeString(result_ptr, result_len)) }, context) } } @@ -289,22 +361,30 @@ reference for the call's duration. ## Callback interfaces -A callback interface becomes a class-bound protocol. Every requirement is -`throws`, so any implementation can fail the library's call in progress: +A callback interface becomes a class-bound protocol. A requirement is +`throws` when the IDL method declares errors (`throws: Domain` or +`throws: any`), and plain otherwise: ```swift public protocol Loader: AnyObject, Sendable { /// The loader's name, recorded in a loaded entry's metadata under /// `source`. + /// + /// - Throws: Any error, which fails the library's call with the error's + /// description. func name() throws -> String /// A store that may already hold `key`, consulted first, if any. + /// + /// - Throws: Any error, which fails the library's call with the error's + /// description. func fallback(key: String) throws -> Store? - /// The value for `key`. Fail with `KeyNotFound` naming + /// The value for `key`. Fail with `keyNotFound` naming /// `key` when there's none. /// - /// - Throws: ``KvError`` to report a declared failure with its fields, or any other error to fail the call. + /// - Throws: ``KvError`` to fail the library's call with that error and + /// its fields; any other error fails it with the error's description. func load(key: String) throws -> Data } ``` @@ -312,27 +392,31 @@ public protocol Loader: AnyObject, Sendable { Implement it with a class and pass an instance (`any Loader`) where the API expects one. The library may call it from any thread, which is why the protocol requires `Sendable`; a class guarding its own state can declare -`@unchecked Sendable`. Passing an implementation retains it until the library -calls the vtable's `free`, which may happen on any library thread, and -releases it then. An optional parameter (`loader: (any Loader)?`) passes a -null vtable for `nil`. +`@unchecked Sendable`. (The Swift vtables leave the thread-affine flag clear.) +Passing an implementation retains it until the library calls the vtable's +`free`, which may happen on any library thread, and releases it then. An +optional parameter (`loader: (any Loader)?`) passes a null vtable for `nil`. Arguments and returns follow the [ABI](../reference/abi.md#callback-interfaces): -- Strings, bytes, and value buffers arrive as copies; an object argument - arrives as a wrapper that adopted the reference the library passed, which - the implementation may keep. -- A returned number, `Bool`, or C-style enum crosses by value; a returned - object (`Store` or `Store?`) as a fresh reference the library adopts; and a - returned string, `Data`, or value buffer (a record, a rich enum, an - optional, a list, or a map) as a run allocated with `{prefix}_alloc`, which - the library adopts. -- A requirement declared `throws` in the IDL may throw the module's error - enum: the library receives its code, its message, and its fields as the - payload, exactly as if the producer had raised it. Any other error, and - any error from a requirement not declared `throws`, reaches the library as - a callback failure (`-4`) with the error's `localizedDescription`. Nothing - unwinds through C. +- Strings, bytes, numeric arrays, and value buffers arrive as copies; an + optional scalar arrives as `T?`; an object argument arrives as a wrapper + that adopted the reference the library passed, which the implementation + may keep. +- A returned number, `Bool`, or C-style enum crosses by value, and an + optional one as a presence flag plus the value; a returned object (`Store` + or `Store?`) as a fresh reference the library adopts; and a returned + string, `Data`, numeric array, or value buffer (a record, a rich enum, an + optional, a list, or a map) as a run allocated with `{prefix}_alloc`, + which the library adopts. +- A requirement declared `throws: KvError` may throw that enum: the library + receives its code, message, and fields as the payload, exactly as if the + producer had raised it (a Rust producer then renders the message from the + fields; a thrown `.unknown` sends its own code, which the producer treats + as any other failure). Any other error, from any throwing requirement, + reaches the library as an untyped failure (`-1`) with the error's + `errorDescription` (or its `String(describing:)`). Nothing unwinds + through C. ```swift final class FileLoader: Loader, @unchecked Sendable { @@ -353,21 +437,36 @@ let entry = try store.getOrLoad(key: "settings", loader: FileLoader()) ## Iterators -An `iter` return becomes a class conforming to `Sequence` and -`IteratorProtocol`. Each `next()` pulls exactly one element from the library; -the native iterator is destroyed on exhaustion or from `deinit` when iteration -stops early. `next()` can't throw, so for a throwing function an error -reported mid-stream ends iteration and is stored in the sequence's `error` -property. +An `iter` return is a `NativeSequence`, one generic class (a +`Sequence` and its own `IteratorProtocol`) for every iterator in the API. +Each `next()` pulls exactly one element from the library; the native +iterator is released as soon as the stream ends, or from `deinit` when +iteration stops early. The sequence is single-pass. `next()` can't throw, so +for a throwing function an error reported mid-stream ends iteration and is +kept in the sequence's `error` property; `collect()` pulls the rest and +throws it instead: + +```swift +for key in try store.keys(prefix: "user.") { print(key) } + +let expirations: [Int64?] = try store.expirations().collect() +``` ## Known limitations - The library is linked at build time, so `{PREFIX}_LIBRARY` isn't honored and - library paths come from the linker and loader. -- Load-time check failures, malformed value buffers, and failures of - non-throwing calls stop the process instead of throwing. + library paths come from the linker and loader. A library the dynamic + loader can't find stops the process before `main`, before `check()` can + report anything. +- Calls made while the library doesn't match (whether or not `check()` ran), + malformed value buffers, and failures of non-throwing calls stop the + process instead of throwing. - A type that shares its name with a module's namespace `enum` (the `Stats` record beside the `kv.stats` module's `Kv.Stats`) is qualified with the Swift module name where the two would clash (`Kvstore.Stats`). -- Interface wrappers aren't `Hashable`, so records that contain objects are - only `Sendable`. +- The runtime declares the public types `{Module}Library`, + `{Module}RuntimeError`, and `NativeSequence`; an IDL type with one of + those names doesn't compile. +- The manifest stays at `swift-tools-version:5.9` for Xcode 15 users, so + SwiftPM compiles the package in the Swift 5 language mode even under a + Swift 6 toolchain. diff --git a/docs/src/generators/wasm.md b/docs/src/generators/wasm.md index 343dcd61..39a1ea67 100644 --- a/docs/src/generators/wasm.md +++ b/docs/src/generators/wasm.md @@ -7,6 +7,10 @@ that stages values in the module's linear memory. The JavaScript API is the errors, callbacks, and iterators apply here unchanged; this page covers what differs. +WebAssembly is a [Tier 2](../stability.md#target-tiers) target: it may lag +behind a new ABI revision, but it passes the same conformance suite before a +release. + ## What gets generated For a library named `kvstore` (the snapshot fixtures use `kitchen_sink`): @@ -16,7 +20,9 @@ wasm/ ├── package.json ES module manifest ├── index.js `init()`, the raw entry points, and the API ├── index.d.ts TypeScript declarations -├── runtime.js the shared runtime (codec, errors, wrappers) +├── runtime.js the shared runtime (codec, checks, errors, wrappers) +├── debug.js the `./debug` export: leak counters for tests +├── debug.d.ts ├── linear.js the transport: loading, staging, table functions └── README.md ``` @@ -43,9 +49,9 @@ module to `target/weaveffi/wasm32/{library}.wasm`. ## Load `init()` loads the module and checks the contract before any other export -works: the module must implement ABI revision 4, and each top-level module's -contract table must hold every declaration the bindings were generated with -(see the [Node.js page](node.md#load-time-checks)). A failed check rejects +works: the module must implement ABI revision 5, and each top-level module's +contract table must hold every row the bindings were generated with (see +the [Node.js page](node.md#load-time-checks)). A failed check rejects with an `Error` naming the declaration (`kvstore: kv.Store.put changed since these bindings were generated`). Until the promise resolves, every other call throws. @@ -75,9 +81,11 @@ and a failed load can be retried. ## Transport -Each C symbol gets one entry point in `index.js` that checks its arguments, -copies strings and byte runs into linear memory for the call, and copies -returned strings and buffers out before releasing them: +Each C symbol gets one entry point in `index.js` that checks its arguments +(range-checking integers), splits an optional scalar into its flag and +value, copies strings, byte runs, and typed arrays into linear memory for +the call, and copies returned strings, buffers, and typed arrays out before +releasing them: ```js kitchen_sink_kitchen_echo_string: (a0) => { @@ -93,14 +101,15 @@ returned strings and buffers out before releasing them: }, ``` -Staged arguments are runs from `{prefix}_alloc`, released with -`{prefix}_free_bytes` once the call returns. Runs have alignment 1, so the -glue reads and writes multi-byte values through a `DataView`. One error -slot, one `out_len` slot, and one iterator item slot are reused by every -call; they and the callback vtables live in 8-byte-aligned blocks carved -from arenas the glue allocates once and never frees. `__debugLive(4)` -leaves those arenas out of its count of byte runs. Object handles are -linear-memory addresses. +Staged arguments are runs from `{prefix}_alloc` (8-aligned, so a typed +array's elements are aligned for their type), released with +`{prefix}_free_bytes` once the call returns. The glue reads and writes +multi-byte values through a `DataView`. One error slot and one each of the +`out_len`, `out_value`, and iterator item slots are reused by every call; +they and the callback vtables live in 8-byte-aligned blocks carved from +arenas the glue allocates once and never frees. The `./debug` export's +`debugLive(4)` leaves those arenas out of its count of byte runs. Object +handles are linear-memory addresses. Async completions and callback-interface methods are JavaScript functions installed in the module's function table. The glue uses @@ -113,9 +122,12 @@ re-exports it, so no experimental flag is needed. The mapping is the Node.js target's: 64-bit integers are `bigint`, `bytes` is `Uint8Array`, records are plain objects, rich enums are tagged unions, C-style enums are frozen objects, interfaces are classes with `close()` and -`[Symbol.dispose]()`, optionals are `T | null`, lists are arrays, maps are -`Record` objects (a `Map` is accepted as an argument), and `iter` is a -lazy `IterableIterator`. +`[Symbol.dispose]()`, optionals are `T | null`, lists are arrays (a numeric +list argument may also be the matching typed array), maps are `Record` +objects (a `Map` is accepted as an argument), and `iter` is a lazy +`NativeIterator` with `close()`. Integer arguments are range-checked the +same way, so a value that throws a `RangeError` on one target throws it on +the other. ## Async and cancellation @@ -132,34 +144,30 @@ wasm32`). A cancelled call rejects with `CancelledError` (code -5). ## Callbacks and errors Callback interfaces work as on Node.js: returns of every family (a -string, bytes, or buffer return is copied into a run from `{prefix}_alloc` -that the producer adopts, and an object return is a new reference), domain -errors from methods declared `throws` with their fields, and `null` for an -optional callback (`Cb?`), which passes a null vtable. The vtable starts -with its `size`, `flags`, and `free` entries, like any consumer's. +string, bytes, buffer, or typed-array return is copied into a run from +`{prefix}_alloc` that the producer adopts, an optional scalar is the +method's `bool` result plus the value behind its `out_value` slot, and an +object return is a new reference), errors reported by each method's +`throws` (a domain's codes with their fields, -1 for `throws: any`, -4 for +a method that doesn't throw), and `null` for an optional callback (`Cb?`), +which passes a null vtable. The vtable starts with its `size`, `flags` (0), +and `free` entries, like any consumer's. There are no threads, so a callback runs only while a call into the module is on the stack, on the calling thread, and the Node.js addon's threading -rules don't arise. An implementation that throws fails the call with code --4, as on Node.js. Because `wasm32-unknown-unknown` aborts on a panic, a -producer panic traps the module; the trap surfaces as the package's root -error with code -2, and the module may be unusable afterward. - -## Emscripten - -With `emscripten = true` under `[generators.wasm]`, `init` instead takes an -initialized Emscripten module (or the promise its `MODULARIZE` factory -returns), binds its underscore-prefixed exports, reads memory through -`HEAPU8`, and installs table functions with `addFunction`. Link the module -with `-sALLOW_TABLE_GROWTH`, `-sWASM_BIGINT`, and -`-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`, and export the library's C -symbols, including the runtime surface (`{prefix}_alloc`, -`{prefix}_free_bytes`, `{prefix}_error_set_payload`, and the rest) and each -`{prefix}_{module}_contract`. The packaged layout then ships glue only. +rules don't arise. -```ts -export declare function init(module: object | Promise): Promise; -``` +## Traps + +Because `wasm32-unknown-unknown` aborts on a panic, a producer panic traps +the module. The call that trapped fails with the package's root error, code +-2 (`the native library trapped: unreachable`). A trap can leave the +module's memory inconsistent, so it also poisons the instance: every later +call fails with code -2 (`kvstore: the WebAssembly instance trapped earlier +(...) and can't be used anymore`) without running, while releases (closing +a wrapper or an iterator, a garbage-collected wrapper, a cancel token) do +nothing, so no finalizer throws. Load the module again in a new process or +page to recover. ## Known limitations @@ -167,6 +175,6 @@ export declare function init(module: object | Promise): Promise; - An async call can't be cancelled once launched, since it completes before its launcher returns, and a future that waits on another thread or an external event fails with code -1. -- A producer panic traps rather than unwinding, which can leave the module's - state inconsistent. -- Emscripten mode isn't covered by the conformance suite. +- A producer panic traps rather than unwinding, and the instance is + unusable afterward (see [Traps](#traps)). +- Only `wasm32-unknown-unknown` builds are supported. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index fa714026..e8122eea 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -17,8 +17,9 @@ weaveffi --version ## Write the producer -Create a library crate, add the `weaveffi` crate, and build it as a C -dynamic library: +Create a library crate and add the `weaveffi` crate. It needs no +`crate-type`: WeaveFFI builds the C dynamic library itself (with `cargo +rustc --crate-type cdylib`). ```bash cargo new --lib mathlib @@ -26,12 +27,6 @@ cd mathlib cargo add weaveffi ``` -```toml -# Cargo.toml -[lib] -crate-type = ["cdylib"] -``` - Replace `src/lib.rs` with an annotated module. Everything here is safe Rust; the macro writes the `extern "C"` layer. @@ -43,16 +38,10 @@ pub mod math { #[weaveffi::error] #[derive(Debug)] pub enum MathError { - /// Division by zero. + /// division by zero DivisionByZero = 1, } - impl std::fmt::Display for MathError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("division by zero") - } - } - /// Add two integers. #[weaveffi::export] pub fn add(a: i32, b: i32) -> i32 { @@ -81,9 +70,13 @@ A few things to notice: - Every C symbol starts with the crate's library name, so this crate exports `mathlib_math_add`, `mathlib_math_div`, and `mathlib_math_greet`, plus the runtime (`mathlib_error_clear`, `mathlib_free_bytes`, and so on). -- `Result` makes `div` a throwing function. The enum's - discriminants are the stable error codes, and its `Display` output is the - message consumers see. +- `Result` makes `div` throw the `MathError` domain. The + enum's discriminants are the stable error codes, and the macro generates + its `Display` from each variant's doc comment (or a + `#[weaveffi(message = "...")]` template), which is the message consumers + see. A `Result` whose error type isn't a declared domain (a `String`, a + `std::io::Error`) works too: it throws an untyped error carrying its + `Display` text. - `weaveffi::export_runtime!()` appears exactly once, at the crate root. The [producer macro guide](guides/producer-macro.md) covers records, enums, @@ -93,15 +86,16 @@ objects, callbacks, iterators, and async functions. Run `weaveffi init` in the crate. It writes a `weaveffi.toml` whose `[project]` table points at the crate itself, then lists anything the crate -still needs (a `cdylib` crate type, the `export_runtime!` call, a +still needs (the `weaveffi` dependency, the `export_runtime!` call, a `#[weaveffi::module]`): ```bash weaveffi init ``` -The file it writes generates all eleven targets; uncomment `targets` to pick -a subset: +The file it writes generates the C header and the Python package; edit +`targets` to pick the languages you ship (without it, every target is +generated): ```toml # weaveffi.toml @@ -118,8 +112,9 @@ no arguments and works from any directory in the project: weaveffi generate ``` -`weaveffi generate` runs `cargo build`, then reads the API from the library -it built: the macro embeds a description of every exported declaration in +`weaveffi generate` builds the library with Cargo's `dev` profile (pass +`--profile ` for another), then reads the API from the library it +built: the macro embeds a description of every exported declaration in the library, so the bindings match the compiled code exactly, `#[cfg]` included. `weaveffi extract` prints that description as an IDL. @@ -132,7 +127,7 @@ that are no longer produced. ## Call it from Python Run `weaveffi dev` instead of `weaveffi generate` while you iterate: it -builds the debug library, generates, and copies the library into the +builds the library (the `dev` profile), generates, and copies the library into the generated Python package, which loads a bundled copy first. Then install the package: @@ -167,8 +162,9 @@ to load with an error naming the declaration that's missing or changed. The ## Call it from C The header is the contract every other binding is built on. Strings cross as -UTF-8 `(ptr, len)` runs, the caller owns a zeroed `mathlib_error`, and -returned strings are released with `mathlib_free_bytes`: +UTF-8 `(ptr, len)` runs, the caller owns a zeroed `mathlib_error` (whose +message is a length-delimited run, not a C string), and returned strings are +released with `mathlib_free_bytes`: ```c #include @@ -187,7 +183,8 @@ int main(void) { mathlib_math_div(1, 0, &err); if (err.code == mathlib_math_MathError_DivisionByZero) { - printf("error %d: %s\n", err.code, err.message); + printf("error %d: %.*s\n", err.code, + (int)err.message_len, (const char*)err.message_ptr); mathlib_error_clear(&err); } @@ -243,7 +240,7 @@ Edit `greeter.yml` to declare the API (the [IDL reference](reference/idl.md) has the full schema): ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: greeter functions: @@ -271,70 +268,93 @@ weaveffi generate --target c # writes bindings/c/greeter.h uint32_t greeter_abi_version(void) { return GREETER_ABI_VERSION; } -/* The contract table the header was generated with. */ +/* The contract table the header was generated with, sorted by id. */ const greeter_contract_entry* greeter_greeter_contract(size_t* out_len) { static const greeter_contract_entry table[] = GREETER_GREETER_CONTRACT; - *out_len = GREETER_GREETER_CONTRACT_LEN; + if (out_len != NULL) *out_len = GREETER_GREETER_CONTRACT_LEN; return table; } int32_t greeter_greeter_add(int32_t a, int32_t b, greeter_error* out_err) { (void)out_err; /* written only on failure */ - return a + b; + return (int32_t)((uint32_t)a + (uint32_t)b); } -void greeter_error_set(greeter_error* err, int32_t code, const char* message) { - greeter_error_clear(err); - err->code = code; - err->message = message ? strdup(message) : NULL; +/* One allocator for every byte run: returned strings and buffers, error + messages and payloads, and runs a consumer allocates with greeter_alloc. + Every run must be 8-aligned; malloc's alignment is at least 8. */ +uint8_t* greeter_alloc(size_t len) { return len ? calloc(len, 1) : NULL; } +void greeter_free_bytes(uint8_t* ptr, size_t len) { if (len) free(ptr); } + +static const uint8_t* run_copy(const uint8_t* ptr, size_t len) { + uint8_t* run = greeter_alloc(len); + if (run != NULL) memcpy(run, ptr, len); + return run; } -void greeter_error_clear(greeter_error* err) { - free((void*)err->message); - free((void*)err->payload_ptr); - memset(err, 0, sizeof *err); +/* The message is `message_len` bytes of UTF-8, not NUL-terminated; NULL is + the empty message. Both setters copy. */ +void greeter_error_set(greeter_error* err, int32_t code, + const uint8_t* message_ptr, size_t message_len) { + if (err == NULL) return; + greeter_error_clear(err); + err->code = code; + if (message_ptr != NULL && message_len != 0) { + err->message_ptr = run_copy(message_ptr, message_len); + err->message_len = err->message_ptr != NULL ? message_len : 0; + } } void greeter_error_set_payload(greeter_error* err, const uint8_t* ptr, size_t len) { + if (err == NULL) return; free((void*)err->payload_ptr); err->payload_ptr = NULL; err->payload_len = 0; - if (ptr && len) { - uint8_t* copy = malloc(len); - memcpy(copy, ptr, len); - err->payload_ptr = copy; - err->payload_len = len; + if (ptr != NULL && len != 0) { + err->payload_ptr = run_copy(ptr, len); + err->payload_len = err->payload_ptr != NULL ? len : 0; } } -void greeter_error_free(greeter_error* err) { - if (err) { greeter_error_clear(err); free(err); } +void greeter_error_clear(greeter_error* err) { + if (err == NULL) return; + free((void*)err->message_ptr); + free((void*)err->payload_ptr); + memset(err, 0, sizeof *err); } -/* One allocator for every byte run, returned or consumer-allocated. */ -uint8_t* greeter_alloc(size_t len) { return len ? calloc(len, 1) : NULL; } -void greeter_free_bytes(uint8_t* ptr, size_t len) { (void)len; free(ptr); } +void greeter_error_free(greeter_error* err) { + greeter_error_clear(err); + free(err); +} /* Cancel tokens (used by async functions; this API has none, but the runtime surface is the same for every library). */ -struct greeter_cancel_token { atomic_bool cancelled; }; +struct greeter_cancel_token { atomic_int refs; atomic_bool cancelled; }; greeter_cancel_token* greeter_cancel_token_create(void) { - return calloc(1, sizeof(greeter_cancel_token)); + greeter_cancel_token* t = malloc(sizeof *t); + if (t != NULL) { atomic_init(&t->refs, 1); atomic_init(&t->cancelled, false); } + return t; } void greeter_cancel_token_cancel(greeter_cancel_token* token) { - if (token) atomic_store(&token->cancelled, true); + if (token != NULL) atomic_store(&token->cancelled, true); } bool greeter_cancel_token_is_cancelled(const greeter_cancel_token* token) { - return token && atomic_load(&((greeter_cancel_token*)token)->cancelled); + return token != NULL && atomic_load(&((greeter_cancel_token*)token)->cancelled); +} +void greeter_cancel_token_destroy(greeter_cancel_token* token) { + if (token != NULL && atomic_fetch_sub(&token->refs, 1) == 1) free(token); } -void greeter_cancel_token_destroy(greeter_cancel_token* token) { free(token); } /* Leak counters: a producer that doesn't count returns 0 for every kind. */ uint64_t greeter_debug_live(int32_t kind) { (void)kind; return 0; } ``` The library must export every runtime symbol the header declares, each -top-level module's contract function, and the API itself; the +top-level module's contract function, and the API itself. Every run it hands +out is 8-aligned and released by its own `greeter_free_bytes`, and a +callback vtable it's given may carry the `GREETER_VTABLE_THREAD_AFFINE` +flag, which it must honor. The [C ABI contract](reference/abi.md#runtime-surface) lists them, and [`conformance/c/producer.c`](https://github.com/weavefoundry/weaveffi/blob/main/conformance/c/producer.c) is a complete hand-written producer to copy from. Build it as @@ -362,5 +382,5 @@ GREETER_LIBRARY=$PWD/libgreeter.so python3 -c "import greeter; print(greeter.add - [Samples](samples.md): three complete producers, from the minimal `calculator` to the feature-complete `kvstore`. - [Generators](generators/README.md): what each language gets. -- Gate CI on `weaveffi diff --check` so committed bindings can't drift; see +- Gate CI on `weaveffi generate --check` so committed bindings can't drift; see [Stability and Versioning](stability.md#ci-workflow). diff --git a/docs/src/guides/async.md b/docs/src/guides/async.md index f4bfb1c2..cc570bbe 100644 --- a/docs/src/guides/async.md +++ b/docs/src/guides/async.md @@ -32,20 +32,20 @@ In an IDL, set `async: true` and optionally `cancellable: true` on a function, method, or static: ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: kv errors: - name: KvError - codes: - - { name: InvalidPath, code: 1004, message: "invalid path" } + - name: KvError + codes: + - { name: InvalidPath, code: 1004, message: "invalid path" } interfaces: - name: Store constructors: - name: open params: - { name: path, type: string } - throws: true + throws: KvError methods: - name: compact params: @@ -53,6 +53,11 @@ modules: return: u32 async: true cancellable: true + - name: version_of + params: + - { name: key, type: string } + return: u32? + async: true ``` `async` composes with `throws`, with object and buffered results, and with @@ -80,11 +85,22 @@ void kvstore_kv_Store_compact( void* context); ``` -The result slots are nothing (a `void` function), one direct value or object -pointer named `result`, or `const uint8_t* result_ptr, size_t result_len` for -strings, bytes, and value buffers. The launcher has no `out_err`: every -failure, including a marshalling failure of an input, arrives through the -callback. +The result slots follow the value's family: + +| Result | Completion slots after `context, err` | +|--------|----------------------------------------| +| none (`void`) | none | +| Direct (scalar, `bool`, C-style enum) | `T result` | +| OptDirect (`u32?`, `bool?`, `Color?`) | `bool has_result, T result` | +| Slice (`[i32]`, `[f64]`, ...) | `const T* result_ptr, size_t result_len` (element count) | +| string, bytes, value buffer | `const uint8_t* result_ptr, size_t result_len` | +| object | `{tag}* result` (one strong reference) | + +`version_of` above completes through +`void (*)(void* context, kvstore_error* err, bool has_result, uint32_t +result)`. On failure every result slot is zero (`false`, `0`, `NULL`). The +launcher has no `out_err`: every failure, including a marshalling failure of +an input, arrives through the callback. ## The completion contract @@ -101,7 +117,9 @@ The runtime guarantees these for every launch: 3. **Owned results.** `err` is `NULL` on success; otherwise it's heap-boxed, owned by the consumer, and released with `{prefix}_error_free`. A string, bytes, or buffered result is owned by the consumer and released with - `{prefix}_free_bytes(result_ptr, result_len)` after decoding. An object + `{prefix}_free_bytes(result_ptr, result_len)` after decoding; a typed + array with `{prefix}_free_bytes((uint8_t*)result_ptr, result_len * + sizeof(T))`. An OptDirect or Direct result is copied, and an object result transfers one strong reference. 4. **Dropped futures complete.** If the executor drops a future before it finishes (a runtime shutting down, say), the drop guard completes the call @@ -163,30 +181,35 @@ language page. Something has to drive each future between launch and completion. The runtime routes every future to the process-wide executor: -- **Default.** A fixed pool of worker threads, one per available core and - at least two, started lazily on the first launch. The workers share one - run queue, and a future's waker puts it back on the queue, so a launch - never costs a thread and a future woken from any thread resumes on the - pool. The pool has no reactor, so a future that awaits Tokio's I/O or - timers never wakes, and because its workers are few, a future that blocks - one for a long time (synchronous I/O, a long computation) holds up every - other async call. Run such work on a runtime of its own: enable the - `tokio` feature or install a spawner. -- **The `tokio` feature.** With the `weaveffi` crate's `tokio` feature on, - the default executor is Tokio: the current runtime when the launcher is - called from inside one, otherwise a multi-thread runtime the library - creates on first use (its threads are named `weaveffi-async`). That - runtime enables every Tokio driver the build compiles in, so turn on the - Tokio features your futures need (`time`, `net`, and so on) in your own - dependency on `tokio`. Use `tokio::task::spawn_blocking` for synchronous - work inside a future. +- **Tokio (the default).** The `weaveffi` crate's `tokio` feature is on by + default, and with it the executor is Tokio: the current runtime when the + launcher is called from inside one, otherwise a multi-thread runtime the + library creates on first use (its threads are named `weaveffi-async`). + That runtime enables every Tokio driver the build compiles in, so turn on + the Tokio features your futures need (`time`, `net`, and so on) in your + own dependency on `tokio`. Use `tokio::task::spawn_blocking` for + synchronous work inside a future. ```toml [dependencies] - weaveffi = { version = "0.24", features = ["tokio"] } + weaveffi = "0.25" tokio = { version = "1", features = ["time"] } ``` +- **A thread per call.** With `default-features = false`, there's no Tokio + dependency, and each async call runs on a thread of its own (also named + `weaveffi-async`), which drives the future with a minimal blocking + executor and exits when the call completes. A future woken from another + thread resumes on its own thread. There's no reactor, so a future that + awaits Tokio's I/O or timers never wakes; use this for futures that wait + only on channels, other threads, or nothing at all. A thread that can't + start completes the call with `-1`. + + ```toml + [dependencies] + weaveffi = { version = "0.25", default-features = false } + ``` + - **Custom.** Call `weaveffi::set_spawner` once at startup, before the first async launch; it overrides both defaults. It accepts any `Fn(BoxFuture) + Send + Sync + 'static`: @@ -204,30 +227,31 @@ runtime routes every future to the process-wide executor: thread. Every future it receives is already wrapped so a panic becomes a `-2` completion, a spawner that drops a future still produces a `-5` completion, and a spawner that panics completes the call with `-1`. -- **`wasm32`.** There are no threads, so the default polls the future - inline before the launcher returns. A future that finishes after bounded - work is fine. A future that's still pending once nothing can wake it (it - awaits a timer or I/O) is dropped, and the call completes with `-1` and - the message "async function suspended with no executor on wasm32" rather - than hanging the module. A call usually completes before an `AbortSignal` - can fire, so on this target cancellation mostly matters for a signal - that's already aborted when the call starts. +- **`wasm32`.** There are no threads (Tokio is never compiled in there, so + the `tokio` feature has no effect), and the future is polled inline before + the launcher returns. A future that finishes after bounded work is fine. A + future that's still pending once nothing can wake it (it awaits a timer or + I/O) is dropped, and the call completes with `-1` and the message "async + function suspended with no executor on wasm32" rather than hanging the + module. A call usually completes before an `AbortSignal` can fire, so on + this target cancellation mostly matters for a signal that's already + aborted when the call starts. ## Pitfalls -- **Awaiting Tokio under the default executor.** The pool has no reactor, - so the future never wakes and the callback never fires; enable the `tokio` - feature or install a Tokio spawner. -- **Blocking a pool worker.** A future that blocks (synchronous I/O, a long - computation) stalls every other call queued behind it on the default - pool; move the work to a runtime of its own. +- **Awaiting Tokio without the `tokio` feature.** The thread-per-call + executor has no reactor, so the future never wakes and the callback never + fires; keep the default feature or install a Tokio spawner. +- **Blocking a Tokio worker.** A future that blocks (synchronous I/O, a + long computation) holds a runtime thread; wrap the work in + `tokio::task::spawn_blocking`. - **Expecting the token to stop CPU-bound work.** Cancellation drops the future at a suspension point. Long synchronous loops should yield or check `is_cancelled()`. - **Clearing an async error.** `err` is boxed; release it with `{prefix}_error_free`, not `error_clear`. - **Leaking a result buffer.** A raw C callback that only copies the result - must still free it. + must still free it (a typed array's length times `sizeof(T)`). - **Async functions with no return value.** Valid, but `weaveffi validate --warn` flags an async free function that returns nothing (`AsyncVoidFunction`), since it's usually a missing return type. diff --git a/docs/src/guides/config.md b/docs/src/guides/config.md index c98223da..f6f089ab 100644 --- a/docs/src/guides/config.md +++ b/docs/src/guides/config.md @@ -45,9 +45,9 @@ Without any file, every setting takes its default. | Key | Default | Meaning | |-----|---------|---------| -| `input` | none | The API definition: a Rust producer crate (its directory, usually `"."`, or its `Cargo.toml`), whose API is read from its built library (see [Library Mode](extract.md)), or a `.yml`, `.yaml`, `.json`, or `.toml` IDL | -| `out` | `bindings` | Output directory for `generate` and `diff` | -| `targets` | all eleven | Targets to generate when `--target` isn't given | +| `input` | none | The API definition: a Rust producer crate (its directory, usually `"."`, or its `Cargo.toml`), whose API is read from its built library (see [Library Mode](extract.md)), or a `.yml`, `.yaml`, or `.json` IDL (TOML is only for this file) | +| `out` | `bindings` | Output directory for `generate` and `dev` | +| `targets` | all eleven | Targets to generate when `--target` isn't given (`weaveffi init` writes `["c", "python"]`) | Paths are relative to the directory holding `weaveffi.toml`. Command-line arguments (`input`, `-o`, `--target`) override the table. @@ -74,7 +74,7 @@ The rules depend on the input: |-|-----------------------|-----| | `name` | `[package] name`, else the crate's Cargo package name | `[package] name`, else the input file stem | | `prefix` | the crate's library name (`[lib] name`, else the package name with `-` mapped to `_`) | `c_prefix`, else snake-case `name` | -| `library` | same as `prefix` (the cdylib Cargo builds) | `library`, else snake-case `name` | +| `library` | same as `prefix` (the `cdylib` WeaveFFI builds) | `library`, else snake-case `name` | | metadata | `[package]`, falling back to `cargo metadata` (workspace-inherited `version.workspace = true` included) | `[package]` | A Rust producer's prefix is what the macro compiled into its symbols, so it @@ -84,12 +84,15 @@ an error. Renaming the published package with `name` is fine. ## `[build]` How `weaveffi build` and `weaveffi package` compile a Rust producer (see -[Packaging](packaging.md)): +[Packaging](packaging.md)). The commands that build a producer's host +library to read its API (`generate`, `dev`, `validate`, `extract`) use the +`dev` profile unless `--profile` names another; `[build] profile` doesn't +apply to them. | Key | Default | Meaning | |-----|---------|---------| | `platforms` | the host | Platform ids to build when `--platforms` isn't given | -| `release` | `true` | Build with the release profile (`--debug` overrides it) | +| `profile` | `"release"` | The Cargo profile to build with (`--profile` overrides it) | | `macos_deployment_target` | `"11.0"` | `MACOSX_DEPLOYMENT_TARGET`, and the macOS version wheel tags carry | | `ios_deployment_target` | `"13.0"` | `IPHONEOS_DEPLOYMENT_TARGET` | | `android_api` | `21` | The minimum Android API level, which picks the NDK compiler | @@ -122,6 +125,7 @@ The C target has no package name. A few targets name a second thing: | `[generators.cpp]` | `header_name` | `{library}.hpp` | | `[generators.python]` | `import_name` (the import package) | `{prefix}` | | `[generators.ruby]` | `module_name` (the top-level Ruby module) | `PascalCase(name)` | +| `[generators.go]` | `package` (the Go package name) | `{prefix}` with underscores removed (`kitchen_sink` gives `kitchensink`) | The constants that go into package manifests are options too: @@ -130,15 +134,15 @@ The constants that go into package manifests are options too: | `[generators.swift]` | `min_macos`, `min_ios` (the `platforms:` of `Package.swift`) | `"11.0"`, `"13.0"` | | `[generators.swift]` | `xcframework_url` (where the packaged binary target downloads from; `{version}` and `{file}` are substituted) | a placeholder | | `[generators.kotlin]` | `min_sdk`, `compile_sdk` | `21`, `35` | -| `[generators.python]` | `requires_python` | `">=3.9"` | +| `[generators.python]` | `requires_python` | `">=3.10"` | | `[generators.node]` | `node_engine` (`engines.node`) | `">=18"` | | `[generators.dart]` | `sdk` (the pubspec SDK constraint) | `">=3.10.0 <4.0.0"` | Keep `min_macos`, `min_ios`, and `min_sdk` at or above the `[build]` deployment targets and API level, or the toolchains warn that the library is newer than the package claims. The remaining options (C's -`buffer_helpers`, C++'s `standard`, Kotlin's `flavor`, and Wasm's -`emscripten`) are documented on the target's page +`buffer_helpers`, C++'s `standard`, and Kotlin's `flavor`) are documented on +the target's page under [Generators](../generators/README.md). No target has its own C prefix: the prefix belongs to the library. @@ -155,20 +159,25 @@ orchestrator does every write. Every run renders every selected target, then: deleted. Files you added under the output directory (a `node_modules/`, a build directory) are never touched. -`--dry-run` validates and prints the files that would be written. +`--dry-run` validates and prints every file the targets render, without +writing. ## CI -`weaveffi diff --check` regenerates in memory and compares with the output +`weaveffi generate --check` renders in memory and compares with the output directory without writing anything: ```bash -weaveffi diff --check # uses [project] -weaveffi diff path/to/crate -o bindings --target c,swift --check +weaveffi generate --check # uses [project] +weaveffi generate path/to/crate -o bindings --target c,swift --check ``` -It exits `0` when the directory is up to date, `2` when files would change, -and `3` when files would be added or removed (stale files count as removed). +It lists every file that would change (`+` added, `~` modified, `-` +removed, where a stale file from the previous run counts as removed) and +exits `0` when the directory is up to date and `1` otherwise. `weaveffi +generate --diff` prints the same changes as a unified diff, also without +writing (it exits `0`; add `--check` to exit `1` on changes). `--dry-run` +can't be combined with either. `weaveffi validate --format json` prints one JSON object with `ok`, counts, and any errors (each with a `code` matching the [error catalog](../reference/idl.md#error-catalog)); `--warn` adds advisory diff --git a/docs/src/guides/errors-and-memory.md b/docs/src/guides/errors-and-memory.md index f2a1c2bb..26463b7c 100644 --- a/docs/src/guides/errors-and-memory.md +++ b/docs/src/guides/errors-and-memory.md @@ -4,15 +4,18 @@ This guide states who owns every value that crosses the C ABI and how failures travel back to the caller. The generated bindings follow these rules for you; they matter when you consume the C header directly, implement it by hand, or audit a binding. The [C ABI contract](../reference/abi.md) is the -normative text. Examples use the `kvstore` sample, whose prefix is `kvstore`. +normative text. Examples use the `kvstore` and `calculator` samples, whose +prefixes are `kvstore` and `calculator`. ## Ownership at a glance | Family | Parameter | Return, async result, iterator element | Release | |--------|-----------|----------------------------------------|---------| -| direct (scalars, `bool`, C-style enums) | copied | copied | nothing | +| Direct (scalars, `bool`, C-style enums) | copied | copied | nothing | +| OptDirect (`T?` of a Direct type) | copied (`has_x`, `x`) | copied (presence plus value) | nothing | +| Slice (`[i32]`, `[f64]`, and the other numeric lists) | borrowed `(ptr, count)` | owned `(ptr, count)` | `{prefix}_free_bytes(ptr, count * sizeof(T))` | | `string`, `bytes` | borrowed `(ptr, len)` | owned `(ptr, len)` | `{prefix}_free_bytes(ptr, len)` | -| value buffers (records, rich enums, `T?`, `[T]`, `{K:V}`) | borrowed `(ptr, len)` | owned `(ptr, len)` | `{prefix}_free_bytes(ptr, len)` | +| value buffers (records, rich enums, other `T?`, `[T]`, `{K:V}`) | borrowed `(ptr, len)` | owned `(ptr, len)` | `{prefix}_free_bytes(ptr, len)` | | objects (interfaces) | borrowed pointer | one strong reference | `{Type}_destroy` | | callback interfaces | `ctx` plus vtable, owned by the consumer | never returned | the producer calls `free(ctx)` once | @@ -24,25 +27,32 @@ which the producer adopts (see [Callback interfaces](#callback-interfaces)). ## Byte runs -Every string, bytes, and value buffer the producer returns is a **byte run** -it allocated, and the producer owns the allocator. Two runtime functions -manage runs: +Every string, bytes, value buffer, and typed array the producer returns, and +every error message and payload, is a **byte run** it allocated, and the +producer owns the allocator. Two runtime functions manage runs: ```c -uint8_t* kvstore_alloc(size_t len); /* a zero-filled run, NULL for 0 */ +uint8_t* kvstore_alloc(size_t len); /* a zero-filled, 8-aligned run; NULL for 0 */ void kvstore_free_bytes(uint8_t* ptr, size_t len); /* returned runs and alloc runs */ ``` +Every run is allocated with alignment 8, so a consumer can read a returned +run in place as any element type (a typed array of `double`, say) without +copying it first. + A consumer releases a returned run with `{prefix}_free_bytes(ptr, len)`, -passing the exact length it received. It allocates a run with +passing the exact length in **bytes** it received (for a typed array, the +element count times the element size). It allocates a run with `{prefix}_alloc(len)` only when it hands bytes *to* the producer: a callback -method's string, bytes, or buffer return, which the producer adopts and -frees, or (on `wasm32`) an argument staged in the module's linear memory, -which the consumer frees itself with `{prefix}_free_bytes`. Null with length -`0` is the empty run everywhere: `{prefix}_alloc(0)` returns null, and -`{prefix}_free_bytes(ptr, 0)` is a no-op. Never free a run with the -language's own allocator, and never hand the producer memory that didn't come -from `{prefix}_alloc`. +method's string, bytes, buffer, or typed-array return, which the producer +adopts and frees, or (on `wasm32`) an argument staged in the module's linear +memory, which the consumer frees itself with `{prefix}_free_bytes`. Null +with length `0` is the empty run everywhere: `{prefix}_alloc(0)` returns +null, and `{prefix}_free_bytes(ptr, 0)` is a no-op. Never free a run with +the language's own allocator, and never hand the producer memory that didn't +come from `{prefix}_alloc`. An error's message and payload are released with +the error (`{prefix}_error_clear` or `{prefix}_error_free`), never with +`{prefix}_free_bytes`. ## Strings and bytes @@ -70,16 +80,59 @@ if (err.code == 0) { } ``` +## Optional scalars and typed arrays + +Two families cross without a value buffer at a call boundary. + +**OptDirect.** An optional integer, float, `bool`, or C-style enum (`i64?`, +`Color?`) is a presence flag plus the value. A parameter is `bool has_x, T +x`, and `x` is ignored (pass `0`) when `has_x` is false; a present value is +still range-checked, so an undeclared enum value fails with `-3`. A return +is the C return `bool` (present) with the value written through a trailing +`T* out_value`; on failure the function returns false. Nothing is +allocated, so nothing is freed. + +```c +/* kvstore's `Store.expires_at(key: string) -> i64?`. */ +int64_t at = 0; +if (kvstore_kv_Store_expires_at(store, (const uint8_t*)"a", 1, &at, &err)) { + printf("expires at %lld\n", (long long)at); +} else if (err.code != 0) { + kvstore_error_clear(&err); /* failed */ +} /* else: no expiry */ +``` + +**Slice.** A list of `i8`, `i16`, `i32`, `i64`, `u16`, `u32`, `u64`, `f32`, +or `f64` is a typed array. A parameter is a borrowed `const T* x_ptr, +size_t x_len`, where `x_len` counts **elements**; the pointer must be +aligned for `T` (a misaligned array fails with `-3`) and may be null only +when the length is `0`. A return is the C return `T*` with the element count +in a trailing `size_t* out_len`, released with +`{prefix}_free_bytes((uint8_t*)ptr, len * sizeof(T))`: + +```c +/* calculator's `running_total(values: [i32]) -> [i32]`. */ +const int32_t xs[] = {1, 2, 3}; +size_t n = 0; +int32_t* totals = calculator_calculator_running_total(xs, 3, &n, &err); +/* totals[0..n] == {1, 3, 6} */ +calculator_free_bytes((uint8_t*)totals, n * sizeof(int32_t)); +``` + +An empty result may come back as `NULL` with count `0`. Inside a record, +variant, or error payload, both families use the ordinary +[value-buffer](../reference/value-buffers.md) encoding. + ## Value buffers -Records, rich enums, optionals (except `Interface?`), lists, and maps cross -as one serialized [value buffer](../reference/value-buffers.md), however deep -the nesting. A parameter is a borrowed `(ptr, len)` the caller encodes and -frees itself. A return is one producer allocation the consumer decodes and -then releases once with `{prefix}_free_bytes`; the strings and records it -decodes are copies, so nothing inside is freed separately. The exception is -an object token inside a buffer, which carries one strong reference the -reader adopts. +Records, rich enums, the remaining optionals (except `Interface?`), lists, +and maps cross as one serialized [value buffer](../reference/value-buffers.md), +however deep the nesting. A parameter is a borrowed `(ptr, len)` the caller +encodes and frees itself. A return is one producer allocation the consumer +decodes and then releases once with `{prefix}_free_bytes`; the strings and +records it decodes are copies, so nothing inside is freed separately. The +exception is an object token inside a buffer, which carries one strong +reference the reader adopts. ## Objects @@ -120,39 +173,61 @@ The [capability matrix](../generators/README.md) lists each target's idiom. A callback-interface parameter lowers to `void* {name}_ctx` plus `const {vtable}* {name}_vtable`. The consumer owns `ctx` (a generated binding uses a key into a table that keeps the implementation alive) and a static -vtable per interface, whose header records its `size` and `free` hook. The -producer may call any entry, any number of times, from any thread, until it +vtable per interface, whose header records its `size`, its `flags`, and its +`free` hook. The producer may call any entry, any number of times, from any +thread (subject to [thread affinity](#thread-affine-callbacks)), until it calls `free(ctx)` exactly once; `free` may also run on any producer thread. An optional callback parameter (`Cb?`) passes a null vtable for none. Arguments to a callback method follow the parameter rules above, seen from -the consumer: strings, bytes, and buffers are borrowed for the duration of -the call and must be copied or decoded before returning; objects transfer one -reference the consumer adopts. +the consumer: strings, bytes, typed arrays, and buffers are borrowed for the +duration of the call and must be copied or decoded before returning (a +zero-length run may have a non-null pointer that must not be dereferenced); +objects transfer one reference the consumer adopts. A callback method's return transfers to the producer: | Return | Slot | Ownership | |--------|------|-----------| | direct value | the C return | copied | +| optional scalar (`T?`) | C return `bool` (present), value in a trailing `T* out_value` | copied | | object (`I`, `I?`) | the C return, `{prefix}_{path}_{I}*` | one strong reference (a fresh `_clone`) the producer adopts; `I` must not be null | -| `string`, `bytes`, value buffer | trailing `uint8_t** out_ptr, size_t* out_len` | a run the consumer allocated with `{prefix}_alloc`, which the producer adopts and frees | +| `string`, `bytes`, value buffer | trailing `uint8_t** out_ptr, size_t* out_len` | a run the consumer allocated with `{prefix}_alloc(len)`, which the producer adopts and frees | +| typed array (`[T]`) | trailing `T** out_ptr, size_t* out_len` (element count) | a run from `{prefix}_alloc(len * sizeof(T))`, adopted the same way | The producer adopts whatever the slots hold even when the method fails, so a consumer that fails after allocating doesn't leak. A return the producer can't accept (text that isn't UTF-8, a null `I`, an out-of-range enum value, -a malformed buffer) fails the method with `-3`. +a malformed buffer, a null or misaligned array) fails the method with `-3`. + +### Thread-affine callbacks + +Some runtimes can only run a value-returning callback on one thread: a Dart +callback that returns a value must run on its isolate's thread. Such a +binding sets bit 0 of the vtable's `flags`, `{PREFIX}_VTABLE_THREAD_AFFINE`. +The producer then records the thread that passed it the vtable, and a method +that returns a value (a non-`void` C return or any out slot), called from +any other thread, fails with `-4` and the message `callback called off its +thread` without reaching the consumer. `void` methods and `free` may still +run on any thread, and the binding forwards them to its own thread. In a +Rust producer the failure arrives as a `ForeignError` converted into the +method's error type, like any other callback failure. Only the Dart target +sets the flag; every other target passes `0`. ## Iterators An `iter` return is an opaque handle. `{Iter}_next` writes one element (returning `1`) or reports the end (returning `0`); each element is owned by -the consumer under the return rules above. `{Iter}_destroy` is called exactly -once, either on exhaustion or when the consumer abandons the iteration early. -The generated wrappers do both. An iterator is advanced by one caller at a -time: a `_next` that arrives while another is in progress on the same -iterator (from another thread, or re-entrantly from a callback the producer's -`next` calls) fails with `-3` rather than blocking. +the consumer under the return rules above (an optional-scalar element is a +`bool* out_has_item` plus `T* out_item`, a typed-array element a `T** +out_item` plus an element count). `_next` checks that every out slot is +non-null before it pulls an element, so a bad call never loses one. +`{Iter}_destroy` is called exactly once, either on exhaustion or when the +consumer abandons the iteration early. The generated wrappers do both. An +iterator is advanced by one caller at a time: a `_next` that arrives while +another is in progress on the same iterator (from another thread, or +re-entrantly from a callback the producer's `next` calls) fails with `-3` +rather than blocking. ## Errors @@ -163,24 +238,30 @@ Every synchronous symbol except `_clone`, `_destroy`, and the iterator's ```c typedef struct kvstore_error { - int32_t code; /* 0 success, >0 domain code, <0 runtime code */ - const char* message; /* NUL-terminated UTF-8, producer-owned */ - const uint8_t* payload_ptr; /* the code's fields as a value buffer, or NULL */ + int32_t code; /* 0 success, >0 domain code, <0 runtime code */ + const uint8_t* message_ptr; /* UTF-8, NOT NUL-terminated, producer-owned */ + size_t message_len; + const uint8_t* payload_ptr; /* the code's fields as a value buffer, or NULL */ size_t payload_len; } kvstore_error; ``` -The caller owns the struct and zero-initializes it. The producer writes it -only on failure. After a failure the caller reads `code`, `message`, and the -payload, then calls `{prefix}_error_clear(&err)`, which frees the message and -payload and resets `code` to `0`. On failure the function's return value is a -zero sentinel (`0`, `false`, `NULL`) that must not be used or freed. +The caller owns the struct and zero-initializes it. After a failure the +caller reads `code`, the message, and the payload, then calls +`{prefix}_error_clear(&err)`, which frees the message and payload runs and +resets `code` to `0`. On failure the function's return value is a zero +sentinel (`0`, `false`, `NULL`) that must not be used or freed. + +The message is `message_len` bytes of UTF-8 at `message_ptr`, with no NUL +terminator, so print it with a length (`%.*s`) or copy it. An empty message +is a null `message_ptr` with length `0`, even when `code` is non-zero. ```c kvstore_error err = {0}; -int64_t n = kvstore_kv_Store_count(store, &err); +uint64_t n = kvstore_kv_Store_count(store, &err); if (err.code != 0) { - fprintf(stderr, "count failed (%d): %s\n", err.code, err.message); + fprintf(stderr, "count failed (%d): %.*s\n", + err.code, (int)err.message_len, (const char*)err.message_ptr); kvstore_error_clear(&err); } ``` @@ -193,67 +274,113 @@ success), released with `{prefix}_error_free`, which also frees the box. | Code | Meaning | |------|---------| | `0` | success | -| `> 0` | a declared code of the module's [error domain](../reference/idl.md#error-domains) | -| `-1` | generic: an untyped producer error (`Result`, an error type without a domain code), or an async call the executor couldn't start | +| `> 0` | a code of the [error domain](../reference/idl.md#error-domains) the callable throws | +| `-1` | untyped: the failure of a `throws: any` callable (a Rust `Result`, `Result`, or any error type that isn't a declared domain), or an async call the executor couldn't start | | `-2` | the producer panicked; the message carries the panic text | -| `-3` | marshalling failure: a null or invalid argument, non-UTF-8 text, an out-of-range enum value, a malformed value buffer (including a map with a repeated key), a callback vtable smaller than the producer's, an iterator advanced concurrently | -| `-4` | a consumer's callback-interface implementation failed; the message is the consumer's | +| `-3` | marshalling failure: a null or invalid argument, non-UTF-8 text, an out-of-range enum value or integer conversion (a `usize` that doesn't fit, a `char` that isn't one scalar), a misaligned typed array, a custom type's `lift` rejecting its input, a malformed value buffer (including a map with a repeated key), a callback vtable smaller than the producer's, an iterator advanced concurrently | +| `-4` | a consumer's callback-interface implementation failed, or a thread-affine callback was called off its thread; the message is the consumer's | | `-5` | cancelled: an async call's cancel token fired, or its executor dropped it (async completions only) | -Domain codes are validated positive, so the two ranges never collide. The C -header declares each domain as an enum (`kvstore_kv_KvError_KeyNotFound = -1001`). +Domain codes are validated positive, so the two ranges never collide. Code +values are unique only within a domain (the calculator sample's +`CalcError.DivisionByZero` and `ParseError.NotANumber` are both `1`), so a +binding interprets a positive code by the domain of the callable that +reported it. The C header declares each domain as a `typedef int32_t` with +its codes as constants (`kvstore_kv_KvError_KeyNotFound = 1001`). ### Domain errors and the trap channel -How a binding surfaces a non-zero code depends on the callable's `throws` -flag, and every target applies the same rule: - -- **`throws: true`.** A positive code is a typed domain error: an exception - subclass, a Swift `Error` case, a Go `error` value, carrying the code, the - producer's message, and the payload fields as properties. A negative code - surfaces as the package's root error type with that code. -- **No `throws`.** The function can't report a domain error, so any non-zero - code is a bug, and the binding traps on it (see +Every callable has one error strategy, recorded on its binding in the +model, and every target applies it the same way: + +- **Domain** (`throws: KvError`). A positive code is a typed domain error: + an exception subclass, a Swift `Error` case, a Go `error` value, carrying + the code, the producer's message, and the payload fields as properties. + **Domains are open**: a positive code the binding wasn't generated with + (the producer gained a code since) surfaces as the domain's base error + type (Swift's `unknown` case, Go's `Unknown{Domain}`), with the code and + message preserved. A negative code surfaces as the package's root error + type (Swift's `{Module}RuntimeError`) with that code. +- **Untyped** (`throws: any`). Failure is `-1` with a message, and surfaces + as the package's root error type carrying the message. There is no code + to match on. Negative runtime codes surface the same way. +- **Trap** (no `throws`). The function can't report an error, so any + non-zero code is a bug, and the binding traps on it (see [The trap policy](#the-trap-policy)). It never dresses the code up as a domain error. -- **`-5` on an async call** always surfaces as the language's own - cancellation error (Swift's `CancellationError`, Kotlin's - `CancellationException`, .NET's `OperationCanceledException`, Python's - `asyncio.CancelledError`, and so on; see - [Async and Cancellation](async.md#per-target-surface)). -Error types are named after the package and the IDL, never after WeaveFFI; -each [language page](../generators/README.md) lists its names. - -A Rust producer's domain error message is its `Display` output. An IDL -`message:` (or a Rust variant's doc comment) is the documented default that -generated docs and consumer-side constructors use. +Under every strategy, `-5` on an async call surfaces as the language's own +cancellation error (Swift's `CancellationError`, Kotlin's +`CancellationException`, .NET's `OperationCanceledException`, Python's +`asyncio.CancelledError`, and so on; see +[Async and Cancellation](async.md#per-target-surface)). + +Error types are named after the package and the IDL, never after WeaveFFI. +Every target derives a domain's type name from its IDL name with one shared +rule: a name already ending in `Error` or `Errors` (or `Exception`) keeps +its stem, so `KvError` stays `KvError` (or `KvException`) and +`KitchenErrors` becomes `KitchenError`, while `Failure` becomes +`FailureError`. Each [language page](../generators/README.md) lists its +names. + +A Rust producer's domain error message is its `Display` output, generated +from the variant's `#[weaveffi(message = "...")]` template or doc comment. +An IDL `message:` (a Rust variant's doc comment) is the documented default +that generated docs and consumer-side constructors use. ### The trap policy -A failure of a call that isn't `throws` is a producer bug (a panic, an +A failure of a call that can't report one is a producer bug (a panic, an argument the producer couldn't lift, a callback failure it let through a call that can't report one), and every binding treats it the same way: it -raises the language's unchecked error rather than a declared one, naming the -runtime code and the producer's message so the bug can be diagnosed from the -report. +raises the language's unchecked error rather than a declared one, naming +the runtime code and the producer's message so the bug can be diagnosed +from the report. -| Target | What a failed non-throwing call raises | -|--------|-----------------------------------------| +| Target | What a failed non-throwing call does | +|--------|-------------------------------------| | C | nothing; the caller reads `err.code` itself | -| C++ | `{namespace}::InternalError` (a `std::runtime_error`) | +| C++ | throws `InternalError` (outside the domain hierarchy, under the root `Error`) | +| Kotlin | throws `NativeBugException`, an unchecked `IllegalStateException` | +| .NET | throws `NativeBugException`, an `InvalidOperationException` | +| Python | raises `InternalError`, a `RuntimeError` outside the `Error` hierarchy | +| Ruby | raises `NativeBugError` | +| Node.js, WebAssembly | throws the package's root error class (`KvstoreError`) with the code; JavaScript has no unchecked errors | | Swift | `fatalError`, since Swift has no unchecked errors (as in UniFFI) | -| Kotlin | `NativeBugException` (an `IllegalStateException`) | -| Node.js, WebAssembly | the package's root error class, `{PascalCase(name)}Error` (`KvstoreError`), with the negative `code` | -| Python | `InternalError` (a `RuntimeError`) | -| .NET | `NativeBugException` (an `InvalidOperationException`) | -| Dart | `NativeError` (an `Error`, not an `Exception`) | -| Go | a `panic` whose value is the package's `*Error` | -| Ruby | `NativeBugError` (a subclass of the gem's root `Error < StandardError`) | - -Cancellation (`-5`) isn't a bug and always surfaces as the language's -cancellation error, as above. +| Dart | throws `NativeError`, an `Error` (not an `Exception`) | +| Go | panics with the package's `*Error` value | + +Each language page names its exact type. Cancellation (`-5`) isn't a bug +and always surfaces as the language's cancellation error, as above. + +### Load-time checks + +Before the first call, every binding checks the library's ABI revision and +each top-level module's [contract table](../reference/abi.md#load-time-checks). +A failed check is a **catchable** error, never a crash, so an application +can report a stale or mismatched library and carry on: + +- Python raises `LibraryLoadError`, an `ImportError` subclass, from the + import, and Ruby raises its `LoadError` (a `::LoadError` subclass) from + `require`. +- Kotlin's `NativeLibrary.load()` throws an `UnsatisfiedLinkError`, which + the first use of the bindings also throws. .NET throws + `NativeLoadException` from `{Name}Library.Check()` or from the first call, + and again from every later one. Dart throws `NativeLibraryException` from + the first call (and retries on the next). C++ throws `LoadError` from + `check_library()`, which every entry point also runs first. +- Node.js throws a plain `Error` while the package's module is evaluated, + so catch it around a dynamic `await import()`. WebAssembly's `init()` + rejects with one (and can be retried). +- Swift and Go expose a check function (`{Module}Library.check()` in + Swift, `Check() error` in Go) that reports the failure as a thrown error + or a returned `error`. A call made without a successful check still traps + (Swift) or panics (Go) with the same error. +- C consumers call `{prefix}_{module}_contract_check()` and compare + `{prefix}_abi_version()` themselves. + +The message names the declaration that's missing or changed ("`kv.Store.get` +changed since these bindings were generated"). ### Payloads @@ -270,45 +397,53 @@ binding catches the exception in its trampoline and reports it through the vtable entry's `out_err` with the runtime surface: ```c -void kvstore_error_set(kvstore_error* err, int32_t code, const char* message); +void kvstore_error_set(kvstore_error* err, int32_t code, + const uint8_t* message_ptr, size_t message_len); void kvstore_error_set_payload(kvstore_error* err, const uint8_t* ptr, size_t len); ``` Both copy their arguments with the producer's allocator; a consumer never -stores its own allocation in `message`. What the producer receives depends -on the method: - -- A method declared `throws` (it needs an error domain in scope for its - module) may report a positive code of that domain, attaching the code's - fields encoded as a value buffer with `{prefix}_error_set_payload`. The - fields may not include objects. The producer receives the code, message, - and payload as written. -- Every other failure, including a code the domain doesn't declare or a - positive code from a method without `throws`, reaches the producer as `-4` - with the consumer's message, so a consumer bug can't impersonate a domain - error. - -The producer decides what the failure means for the call in progress. Nothing -unwinds and nothing is deferred: in a Rust producer every callback trait -method returns `Result`, and the failure arrives -as an `Err`: +stores its own allocation in the struct. What the consumer reports follows +the method's strategy: + +- A method that throws a domain (`throws: KvError`) may report a positive + code of that domain, attaching the code's fields encoded as a value buffer + with `{prefix}_error_set_payload`. Any + other failure it reports as `-1` with a message. +- A method that throws `any` reports `-1` with a message. +- A method that declares no errors reports `-4` with a message. (In Swift + and Go such a method can't fail: its signature doesn't throw or return an + `error`. Go reports a panic in any callback method as `-4`.) + +On the producer's side, a declared code whose payload decodes arrives as +that typed error. Everything else (`-1`, a code the domain doesn't declare, +any failure of a method without a domain) reaches the producer as a callback +failure, `-4` with the consumer's message, so a consumer bug can't +impersonate a domain error. + +The producer decides what the failure means for the call in progress. +Nothing unwinds and nothing is deferred: in a Rust producer every callback +trait method returns `Result` with `E: From`, +and the failure arrives as an `Err`. When `E` is the method's domain, a +typed code arrives as its variant (with the message re-rendered from its +fields by the domain's `Display`), and any other failure is converted from a +`ForeignError`: ```rust pub struct ForeignError { - pub code: i32, // -4, a declared domain code, or -3 for a bad return + pub code: i32, // -4, or -3 for a return the runtime couldn't accept pub message: String, // the consumer's message pub payload: Vec, // a domain code's fields, else empty } ``` -`ForeignError::domain::()` decodes a declared domain code and its payload -into the module's `#[weaveffi::error]` enum, returning `None` for anything -else. A producer function that returns `Result` (and is -therefore `throws`, so its module needs an error domain in scope) can -propagate the failure with `?`, and the original caller then sees the -consumer's code, message, and payload. A function that handles the failure -itself returns whatever it likes. See the -[producer macro guide](producer-macro.md#callback-interfaces) for examples. +A producer function propagates the failure with `?` into its own error type. +Mapping `ForeignError` into a variant of the function's domain (the +`kvstore` sample's `KvError::CallbackFailed`) keeps the call typed for the +original caller; returning `ForeignError` itself makes the function +`throws: any`, so the original caller sees `-1` with the consumer's message. +See the [producer macro guide](producer-macro.md#callback-interfaces) for +examples. ### Panics @@ -322,11 +457,13 @@ the completion callback. `{prefix}_debug_live(kind)` reports how many resources are live, so a test harness can assert that a consumer released everything: `0` objects, `1` foreign callbacks, `2` iterators, `3` cancel tokens, and `4` byte runs -(returned runs and `{prefix}_alloc` runs not yet freed or adopted). Kind `-1` -returns `1` when the producer counts at all and `0` when it doesn't, so a -harness can tell "nothing is live" from "nothing is counted". A Rust producer -counts only with the `weaveffi` crate's `leak-check` feature; without it -every kind returns `0`. +(returned runs, error messages and payloads not yet cleared, and +`{prefix}_alloc` runs not yet freed or adopted). Kind `-1` returns `1` when +the producer counts at all and `0` when it doesn't, so a harness can tell +"nothing is live" from "nothing is counted". A Rust producer counts only +with the `weaveffi` crate's `leak-check` feature; without it every kind +returns `0`. A consumer that keeps a failed error around without clearing it +shows up as a live run. ## Thread safety @@ -334,14 +471,21 @@ Every `#[weaveffi::interface]` type is `Send + Sync` (the macro asserts it), so a macro-built producer may be called from any thread, and the bindings add no thread restriction of their own. Callback methods and async completions may arrive on any producer thread; each binding hops to its own scheduler where -the language requires it. A hand-written producer with thread-affine state -should document it. +the language requires it, and the Dart binding marks its vtables +[thread-affine](#thread-affine-callbacks). A hand-written producer with +thread-affine state should document it, and must honor the vtable flag. ## Pitfalls -- **Wrong length to `free_bytes`.** Always pass the exact `out_len` you got. +- **Wrong length to `free_bytes`.** Always pass the exact length you got, in + bytes: `out_len` for a string, bytes, or buffer, and `out_len * + sizeof(T)` for a typed array. +- **Reading the message as a C string.** It isn't NUL-terminated and may be + null; use `message_len`. - **Freeing borrowed inputs.** Parameters are the caller's; never pass them to `free_bytes` or `_destroy`. +- **Misaligned typed arrays.** A `const T*` parameter must be aligned for + `T`; slicing a byte buffer at an odd offset fails the call with `-3`. - **Encoding an object without `_clone`.** The token hands away a reference your wrapper still needs; the later `_destroy` over-releases. - **Decoding an object-carrying buffer twice.** The second decode adopts @@ -349,9 +493,9 @@ should document it. - **Not clearing the error.** The message and payload leak, and a stale non-zero `code` confuses the next check. Start every slot at `{0}`. - **`error_clear` on an async error.** It leaks the box; use `error_free`. -- **Freeing a callback's returned run.** A string, bytes, or buffer a - callback method returns belongs to the producer once it's written to the - out slots; allocate it with `{prefix}_alloc` and don't free it. +- **Freeing a callback's returned run.** A string, bytes, buffer, or typed + array a callback method returns belongs to the producer once it's written + to the out slots; allocate it with `{prefix}_alloc` and don't free it. - **Holding a lock across a callback call.** The consumer's implementation may call back into the producer, which then waits on the lock you hold; snapshot state, release the lock, then call. diff --git a/docs/src/guides/extract.md b/docs/src/guides/extract.md index 5d2233b1..512fb0e7 100644 --- a/docs/src/guides/extract.md +++ b/docs/src/guides/extract.md @@ -3,24 +3,24 @@ The CLI never reads Rust source. A Rust producer's API is read from the library the crate builds: `#[weaveffi::module]` embeds a description of every declaration it exports in the compiled library, and `weaveffi generate`, -`diff`, `validate`, `extract`, `dev`, `build`, and `package` read it back -out. The bindings therefore describe exactly what the library was compiled +`validate`, `extract`, `dev`, `build`, and `package` read it back out. The bindings therefore describe exactly what the library was compiled with, `#[cfg]` and type aliases resolved, macro expansion and all. A Rust project's input is the crate: its directory (`[project] input = "."`, which `weaveffi init` writes) or its `Cargo.toml`. ```bash -weaveffi generate # build the crate (debug), read it, generate -weaveffi generate --release # read a release build instead +weaveffi generate # build the crate (dev profile), read it, generate +weaveffi generate --profile release # read a release build instead weaveffi generate --library target/x86_64-unknown-linux-gnu/release/libkvstore.so ``` Without `--library`, the CLI resolves the crate with `cargo metadata` (its -name, version, library name, and target directory), runs -`cargo build --lib --message-format=json-render-diagnostics` (Cargo's errors -and warnings print as usual), and reads the crate's `cdylib`, or its -`staticlib` when it has no `cdylib`. `--library` skips the build and reads +name, version, library name, and target directory), runs `cargo rustc --lib +--crate-type cdylib --profile --message-format=json-render-diagnostics` +(Cargo's errors and warnings print as usual), and reads the `cdylib` it +built, so the crate needs no `crate-type` of its own. `--profile` defaults +to `dev`. `--library` skips the build and reads the given file: a shared library, a static archive, or a `.wasm` module, for any platform (an Android `.so` reads fine on a Mac). With no crate at all (no input argument and no `[project] input`), `--library` reads the library on @@ -37,16 +37,16 @@ next to the crate, or handing the API to a non-Rust implementation. ```bash weaveffi extract # the project's crate, YAML to stdout weaveffi extract path/to/crate -o api.yml -weaveffi extract -f json -o api.json # JSON (or -f toml) +weaveffi extract -f json -o api.json # JSON weaveffi extract --library libkvstore.dylib ``` | Flag | Default | Meaning | |------|---------|---------| | `-o`, `--output` | stdout | Write to a file | -| `-f`, `--format` | `yaml` | `yaml`, `json`, or `toml` | +| `-f`, `--format` | `yaml` | `yaml` or `json` | | `--library` | build the crate | Read this library instead | -| `--release` | off | Build the crate in release mode | +| `--profile` | `dev` | The Cargo profile to build the crate with | | `--config` | discovered | The `weaveffi.toml` to use | The document is validated before it's printed, so what `extract` prints @@ -62,8 +62,9 @@ the generated bindings must load the Rust library. ## How the API is embedded -For every declaration (a module, its error domain, a function, an interface -and each of its members, a record, an enum, a callback interface) the macro +For every declaration (a module, each of its error domains, a function, an +interface and each of its members, a record, an enum, a callback interface) +the macro emits an exported static: ```rust,ignore @@ -108,13 +109,14 @@ pub mod shop { `weaveffi extract` lists `discount` only for a build with the `extra` feature (`cargo build --features extra`, then `--library`). -A library with no frames for the crate (the API isn't annotated, or the -crate builds no `cdylib` or `staticlib`) is an error naming what's missing: +A library with no frames for the crate (the API isn't annotated) is an +error naming what's missing: `#[weaveffi::module]` on the API and one `weaveffi::export_runtime!()` call. ## Iterating: `weaveffi dev` -`weaveffi dev` builds the crate's debug library, generates, and makes the +`weaveffi dev` builds the crate's library (the `dev` profile unless +`--profile` names another), generates, and makes the generated packages find that library without any environment variables where a package looks for a bundled copy (when the crate builds a shared library; a static-only build gets the environment variable instead): @@ -138,28 +140,36 @@ follow later builds. ## Generating from a build script The CLI is also a library. `weaveffi_cli::project::Project` locates a -project the way the `weaveffi` command does and generates it, so a crate -that ships bindings can generate them from its `build.rs`: +project the way the `weaveffi` command does, and the +`weaveffi_cli::codegen::Orchestrator` generates it, so a crate that ships +bindings can generate them from its `build.rs`: ```rust,ignore // build.rs +use weaveffi_cli::codegen::Orchestrator; +use weaveffi_cli::project::Project; + fn main() -> miette::Result<()> { println!("cargo::rerun-if-changed=weaveffi.toml"); println!("cargo::rerun-if-changed=api.yml"); - weaveffi_cli::project::Project::discover(env!("CARGO_MANIFEST_DIR"))?.generate()?; + let project = Project::discover(env!("CARGO_MANIFEST_DIR"))?; + let targets = project.config.targets(None)?; + Orchestrator::new() + .with_targets(targets.iter().map(AsRef::as_ref)) + .run(&project.model()?, &project.config.out_dir(None))?; Ok(()) } ``` ```toml [build-dependencies] -weaveffi-cli = "0.24" +weaveffi-cli = "0.25" miette = "7" ``` `Project::discover` finds the nearest `weaveffi.toml` at or above the -directory, and `generate` writes the `[project] targets` into `[project] -out`, rewriting only changed files. +directory, and the orchestrator writes the `[project] targets` into +`[project] out`, rewriting only changed files. A build script runs before its crate is compiled, so it can't read its own crate's library: from a `build.rs`, the project's input must be an IDL, or @@ -173,6 +183,8 @@ instead: `weaveffi generate` in CI or a `cargo xtask`. | Rust | IDL | |------|-----| | `i8`..`i64`, `u8`..`u64`, `f32`, `f64`, `bool` | same | +| `usize`, `isize` | `u64`, `i64` | +| `char` | `string` | | `String`, `&str` | `string` | | `Vec`, `&[u8]` | `bytes` | | `Vec`, `&[T]` | `[T]` | @@ -182,24 +194,28 @@ instead: `weaveffi generate` in CI or a `cargo xtask`. | `&T`, `Arc` (interface), `Arc` | `T` | | `Arc` (callback interface) | `Trait` | | `weaveffi::CancelToken` | removed (`#[weaveffi::cancellable]` sets `cancellable: true`) | -| `Result` | return `T`, sets `throws: true` | +| `Result`, `E` a `#[weaveffi::error]` domain of the tree | return `T`, `throws: E` | +| `Result`, any other `E` | return `T`, `throws: any` | +| a `#[weaveffi::custom]` alias | its repr | | a type alias declared in the tree | its target | | any other name | that name, resolved by the validator | Compositions map recursively: `Option>` is `[i32]?` and -`Vec>` is `[Gadget]`. An `async fn` sets `async: true`, `()` and -`Result<(), E>` returns are no return, and a callback method's -`Result` is a `T` return that throws only with -`#[weaveffi::throws]`. A type is always referenced by its bare name -(`Store`, wherever it's declared), because type names are global; a path -such as `super::kv::Store` contributes only its last segment. +`Vec>` is `[Gadget]`. An `async fn` sets `async: true`, and `()` +and `Result<(), E>` returns are no return. A callback method's `Result` follows the same rule as a function's: a domain `E` throws that domain, +anything else (`ForeignError` included) throws `any`. A type is always +referenced by its bare name (`Store`, wherever it's declared), because type +names are global; a path such as `super::kv::Store` contributes only its +last segment. ## Limits - **Error messages.** A `#[weaveffi::error]` variant's doc comment becomes - the code's `doc:`, and its first line the code's `message:` (the variant - name when it has no doc), so a Rust producer can't give a code a message - that differs from its doc's first line. + the code's `doc:`, and the code's `message:` is the doc's first line, else + the variant's `#[weaveffi(message = "...")]` template as written, else the + variant name. The runtime message is the generated `Display` (the + template filled from the fields), which the IDL can't express. - **Deprecation versions.** `#[deprecated(since = "...")]` keeps only the note; the IDL has no `since` field. - **Parameter docs.** Rust doesn't allow doc comments on function diff --git a/docs/src/guides/packaging.md b/docs/src/guides/packaging.md index 63b05c2f..0cfded40 100644 --- a/docs/src/guides/packaging.md +++ b/docs/src/guides/packaging.md @@ -21,7 +21,8 @@ identity's `library` field. `weaveffi build` finds the producer crate with `cargo metadata` (the project's crate, `[build] manifest`, or `--manifest-path`) and runs `cargo rustc --lib` once per platform with the artifact kinds that platform -needs, so the crate's own `crate-type` doesn't matter. A Rust producer's API +needs, so the crate's own `crate-type` doesn't matter. It builds with the +`release` profile unless `--profile` (or `[build] profile`) names another. A Rust producer's API is then read from the first library it built (see [Library Mode](extract.md)), so nothing is compiled twice: @@ -62,10 +63,17 @@ library so packages ship it prebuilt: `windows-x64`, where the generated CMake project builds it. Glue a host can't compile is skipped with a warning, and the package falls -back to compiling it. Every platform is checked before anything compiles, so -a missing rustup target (`rustup target add ...`), a missing NDK, or a -platform this host can't build (Apple platforms need macOS, Windows needs -Windows) fails at once with every problem listed. +back to compiling it. Every platform is checked before anything compiles; a +platform this machine can't build (a missing rustup target, a missing NDK, +or a host that can't build it: Apple platforms need macOS, Windows needs +Windows) is skipped with a warning naming the fix, and the run fails only +when no requested platform can be built. + +`build` and `package` follow one policy for a missing external tool (a C +compiler, Node.js headers, a JDK, the NDK, Xcode, the .NET SDK): they warn, +skip the artifact that needs it, finish everything else, and list every +skipped artifact at the end. The run still exits `0`; pass `--strict` to +exit `1` when anything was skipped (as CI should). ## Package @@ -140,14 +148,15 @@ To publish, upload the archive to that URL and commit `swift/{Module}/` to the repository consumers depend on. For local use, unzip the archive next to the packaged `Package.swift`; the manifest prefers a local `C{Module}.xcframework` over the URL. This step runs `xcodebuild`, so it -needs macOS. +needs macOS with Xcode; elsewhere the Swift package is skipped. ## What still needs the ecosystem's tools - **Publishing.** There's no `weaveffi publish`; run each ecosystem's own upload command on the artifacts. - **NuGet.** `package` runs `dotnet pack`, so the .NET SDK must be - installed; without it the run fails after writing everything else. + installed; without it the `.nupkg` is skipped (and `--strict` fails the + run) after everything else is written. - **Kotlin.** The output is a Gradle project, not an `.aar` or `.jar`: building one needs the Kotlin compiler, so run `gradle assembleRelease` (Android) or `gradle jar` (JVM) on it. With prebuilt shims present, Gradle @@ -190,7 +199,7 @@ jobs: targets: ${{ matrix.targets }} - uses: actions/setup-node@v4 # for the prebuilt Node.js addon - run: cargo install weaveffi-cli - - run: weaveffi build --platforms ${{ matrix.platforms }} + - run: weaveffi build --platforms ${{ matrix.platforms }} --strict - uses: actions/upload-artifact@v4 with: name: weaveffi-${{ matrix.runner }} @@ -207,7 +216,7 @@ jobs: path: prebuilt merge-multiple: true - run: cargo install weaveffi-cli - - run: weaveffi package --binaries prebuilt + - run: weaveffi package --binaries prebuilt --strict ``` The Linux runner finds the NDK through `ANDROID_NDK_HOME` (or the SDK diff --git a/docs/src/guides/producer-macro.md b/docs/src/guides/producer-macro.md index ad5aa671..a1c214a0 100644 --- a/docs/src/guides/producer-macro.md +++ b/docs/src/guides/producer-macro.md @@ -12,13 +12,16 @@ bindings describe exactly what the library was compiled with. ## Setup ```toml -[lib] -crate-type = ["cdylib"] # add "staticlib" for iOS, "rlib" for in-crate tests - [dependencies] -weaveffi = "0.24" +weaveffi = "0.25" ``` +The crate needs no `crate-type`: the CLI builds the C dynamic library itself +with `cargo rustc --lib --crate-type cdylib`, and `weaveffi build` adds the +`staticlib` an iOS build needs. Add `crate-type = ["cdylib", "rlib"]` only +if you also want `cargo build` to produce the dynamic library, or in-crate +tests to link it (as the samples do). + **The prefix is the crate name.** Every C symbol starts with the crate's library name (`[lib] name`, else the package name with `-` mapped to `_`), which the macro reads from `CARGO_CRATE_NAME` at expansion time. The CLI @@ -32,68 +35,107 @@ on, and builds `libkvstore.so`. arguments and exports the [runtime surface](../reference/abi.md#runtime-surface) under the crate's prefix: `abi_version`, the error helpers (`error_set`, `error_set_payload`, `error_clear`, `error_free`), `alloc`, `free_bytes`, the -cancel-token functions, and `debug_live`. +cancel-token functions, and `debug_live`. It also defines a hidden +`crate::__weaveffi_runtime` item that every `#[weaveffi::module]` refers to, +so a crate that forgets the call fails to compile at the module's name: + +```text +error[E0432]: unresolved import `crate` + --> src/lib.rs:2:9 + | +2 | pub mod api { + | ^^^ no `__weaveffi_runtime` in the root +``` **Each top-level module exports its contract table,** `{prefix}_{module}_contract()`: one `{id, hash}` entry per declaration in the -module tree, which generated bindings check at load time (see +module tree (including each error code and each callback method), which +generated bindings check at load time (see [load-time checks](../reference/abi.md#load-time-checks)). The macro computes the table with the same function the CLI uses for the bindings (`weaveffi_model::contract::entries`), so a library and bindings generated -from it always agree, and adding a declaration never breaks bindings -generated before it. +from it always agree, and adding a declaration, an error code, or a callback +method never breaks bindings generated before it. **Each declaration is described in the library,** as an exported static named `{PREFIX}_META_{HASH}` holding a small JSON frame (on `wasm32`, in the -`weaveffi_meta` custom section). `weaveffi generate`, `diff`, `validate`, -`extract`, and every other command that reads a Rust producer's API read -these frames out of the built library; see [Library Mode](extract.md). +`weaveffi_meta` custom section). `weaveffi generate`, `validate`, `extract`, +and every other command that reads a Rust producer's API read these frames +out of the built library; see [Library Mode](extract.md). ## The attributes | Attribute | On | Effect | |-----------|----|--------| | `#[weaveffi::module]` | inline `mod name { ... }` | An exported namespace and the driver of the expansion. Nested `#[weaveffi::module]` modules become IDL submodules. | -| `#[weaveffi::export]` | `fn`, `async fn` | Exports a free function. `Result` makes it throwing; `async fn` makes it async. | +| `#[weaveffi::export]` | `fn`, `async fn` | Exports a free function. `Result` makes it fallible; `async fn` makes it async. | | `#[weaveffi::record]` | struct with named fields | A by-value record, serialized as a value buffer. | | `#[weaveffi::enumeration]` | `#[repr(i32)]` enum, or enum with named-field variants | A C-style enum (explicit `= N` on every variant) or a rich enum. | -| `#[weaveffi::interface]` | struct plus its inherent `impl` | A reference-counted object type; the `impl`'s `pub fn`s become constructors, methods, and statics. | -| `#[weaveffi::callback_interface]` | `trait Name: Send + Sync` | Methods the consumer implements, each returning `Result`; accepted as `Arc` or `Option>`. | -| `#[weaveffi::throws]` | callback-interface method | The consumer may fail with a code of the error domain in scope. | -| `#[weaveffi::error]` | enum with explicit discriminants | The module's error domain. Must implement `Display`. | +| `#[weaveffi::interface]` | struct plus its inherent `impl` blocks | A reference-counted object type; the `pub fn`s become constructors, methods, and statics. | +| `#[weaveffi::skip]` | `pub fn` in an interface's `impl` | Leaves that function out of the interface. | +| `#[weaveffi::callback_interface]` | `trait Name: Send + Sync` | Methods the consumer implements, each returning `Result` with `E: From`; accepted as `Arc` or `Option>`. | +| `#[weaveffi::error]` | enum with explicit discriminants | An error domain. Generates `Display` and `std::error::Error`; `#[weaveffi::error(no_display)]` leaves both to you. A module may declare several. | +| `#[weaveffi::custom(repr = R, lift = f, lower = g)]` | `pub type Name = T;` | A custom type that crosses the ABI as `R`. | | `#[weaveffi::cancellable]` | exported `async fn` or async method | Takes a `weaveffi::CancelToken` as its last parameter. | Only tagged items are exported; private helpers, `use` items, and state are left alone. Doc comments flow into the IDL and every binding, and `#[deprecated(note = "...")]` becomes the IDL's `deprecated:` text. +The marker attributes are always written with the `weaveffi::` path, and +they mean something only inside a `#[weaveffi::module]`, which reads and +removes them. One that expands on its own (on an item outside any module) is +a compile error naming the fix: + +```text +error: `#[weaveffi::export]` only works on an item inside a `#[weaveffi::module]`; put the item in an inline module annotated `#[weaveffi::module]` (`#[weaveffi::module] pub mod api { ... }`) +``` + +Because a bare `#[export]` or `#[error]` is never read as a marker, the +macro coexists with other derives' helper attributes, such as thiserror's +`#[error("...")]`. + The macro validates the module tree with the same validator the CLI applies -to an IDL (duplicate names, C symbol collisions, a `Result` with no error -domain in scope, iterators out of place, and so on), and each error is a -compile error on the offending item, member, or type. +to an IDL (duplicate names, C symbol and slot collisions, iterators out of +place, and so on), and each error is a compile error on the offending item, +member, or type. ## Types | Rust | IDL | Crosses the ABI as | |------|-----|--------------------| | `i8`..`i64`, `u8`..`u64`, `f32`, `f64`, `bool` | same | one value | -| `usize`, `isize`, `u128`, `i128`, `char` | none (rejected) | use `u64`, `i64`, or `String` | +| `usize`, `isize` | `u64`, `i64` | one value, checked on the way in (out of range is `-3`) | +| `char` | `string` | a UTF-8 run holding exactly one Unicode scalar value (anything else is `-3`) | | `String`, `&str` | `string` | UTF-8 `(ptr, len)` | | `Vec`, `&[u8]` | `bytes` | `(ptr, len)` | -| `#[weaveffi::record]` struct, rich enum | the type | value buffer | | `#[repr(i32)]` enum | the enum | `int32_t` | -| `Option`, `Vec` or `&[T]`, `HashMap`, `BTreeMap` | `T?`, `[T]`, `{K:V}` | value buffer | +| `Option` of a scalar, `bool`, or C-style enum | `T?` | a presence flag plus the value (OptDirect) | +| `Vec` or `&[T]` of `i8`, `i16`, `i32`, `i64`, `u16`, `u32`, `u64`, `f32`, `f64` (or `usize`, `isize`) | `[T]` | a typed array (Slice) | +| `#[weaveffi::record]` struct, rich enum | the type | value buffer | +| every other `Option`, `Vec` or `&[T]`, `HashMap`, `BTreeMap` | `T?`, `[T]`, `{K:V}` | value buffer | | `&T`, `Arc`, `Option>` (interface `T`); `Self` or `T` as a return | `T`, `T?` | object pointer | | `Arc`, `Option>` (callback interface) | `Trait`, `Trait?` | `ctx` plus vtable (parameters only) | | `weaveffi::Iter` | `iter` | iterator handle (returns only) | +| a `#[weaveffi::custom]` alias | its repr | as its repr | | `weaveffi::CancelToken` | none | the launcher's cancel-token slot | -A reference (`&str`, `&[u8]`, `&Contact`, `&Store`) is a calling convention, -not an IDL distinction: the thunk lends the lifted argument for the call. -Objects compose with every buffered shape, so `Vec>`, -`Option>`, an `Arc` record field, and `weaveffi::Iter>` all -work. [Library Mode](extract.md#type-mapping) has the full -mapping. +A reference (`&str`, `&[u8]`, `&[f64]`, `&Contact`, `&Store`) is a calling +convention, not an IDL distinction: the thunk lends the lifted argument for +the call. A `&[P]` of a slice element type reads the caller's array in +place, without copying. How a type crosses never changes its IDL spelling or +its surface type in a binding (`i64?` is an optional integer and `[f64]` a +list of doubles in every language); only the transport differs, and inside a +value buffer every type uses the buffer encoding. Objects compose with every +buffered shape, so `Vec>`, `Option>`, an `Arc` record field, +and `weaveffi::Iter>` all work. [Library Mode](extract.md#type-mapping) +has the full mapping. + +`usize` and `isize` are for sizes and counts that are natural in Rust; the +IDL, the contract, and every binding see `u64` and `i64`, so a 32-bit +producer rejects an incoming value that doesn't fit with `-3` instead of +truncating it. `u128` and `i128` have no IDL type (declare a custom type +with a `String` repr for wider integers). ## Functions and errors @@ -104,53 +146,106 @@ pub mod calculator { #[weaveffi::error] #[derive(Debug)] pub enum CalcError { - /// Division by zero. + /// division by zero DivisionByZero = 1, } - impl std::fmt::Display for CalcError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("division by zero") - } + /// Divide `a` by `b`, failing on a zero divisor. + #[weaveffi::export] + pub fn divide(a: i32, b: i32) -> Result { + a.checked_div(b).ok_or(CalcError::DivisionByZero) } - /// Divide two integers, failing on a zero divisor. + /// The square root of `x`, failing for a negative `x`. #[weaveffi::export] - pub fn div(a: i32, b: i32) -> Result { - a.checked_div(b).ok_or(CalcError::DivisionByZero) + pub fn sqrt(x: f64) -> Result { + if x < 0.0 { + return Err(format!("cannot take the square root of {x}")); + } + Ok(x.sqrt()) } } -``` -A `Result` return is `throws: true` in the IDL with return type `T`; -`()` and `Result<(), E>` return nothing. The `#[weaveffi::error]` enum's -discriminants are the codes (positive and unique), and its runtime message -is the enum's `Display` output; a variant's doc comment is the documented -default message that appears in the IDL and in generated docs. Without a -`Display` impl the macro fails with a trait-bound error naming the enum. +weaveffi::export_runtime!(); +``` -A variant may carry named fields, which travel as the error's structured -payload (this needs a primitive repr): +A `Result` return makes the callable fallible with return type `T` +(`()` and `Result<(), E>` return nothing), and `E` decides what it throws: + +- **A declared domain.** When `E` is a `#[weaveffi::error]` enum declared in + the same module tree (matched by its last path segment), the callable is + `throws: E` in the IDL. Every binding raises a typed error for each code, + with the code's fields as properties. `divide` above is `throws: + CalcError`. +- **Anything else.** For any other `E` (a `String`, `std::io::Error`, + `anyhow::Error`, `Box`, a domain from a sibling + root tree), the callable is `throws: any`: a failure reports the runtime + code `-1` with `E`'s `Display` output as the message, and bindings raise + the library's base error type. `E` must implement `Display`; if it + doesn't, the error is spanned on `E`. `sqrt` above is `throws: any`. + +A function that returns a plain `T` can't fail; a panic in producer code is +caught at the boundary and reported as code `-2`, which bindings treat as a +bug (see [Errors and Memory](errors-and-memory.md#the-trap-policy)). + +### Error domains + +An error domain's discriminants are its stable codes (positive and unique +within the domain). The macro generates `Display` and `std::error::Error` +for it, so the enum must derive `Debug`. Each variant's message is its +`#[weaveffi(message = "...")]` template, which may interpolate the +variant's fields by name (`{key}`, or `{key:?}` for `Debug`), else the first +line of its doc comment, else the variant name. A variant may carry named +fields, which travel as the error's structured payload; an enum with such a +variant needs `#[repr(i32)]`: ```rust #[weaveffi::error] #[derive(Debug)] #[repr(i32)] -pub enum QuotaError { - /// Quota exceeded. - Exceeded { limit: i64, used: i64 } = 3001, - /// Quota service unavailable. - Unavailable = 3002, +pub enum KvError { + /// key not found + #[weaveffi(message = "key not found: {key}")] + KeyNotFound { + /// The key that was looked up. + key: String, + } = 1001, + /// store is full + #[weaveffi(message = "store is full ({capacity} entries)")] + StoreFull { capacity: u32 } = 1003, + /// invalid path + InvalidPath = 1004, +} +``` + +`KvError::KeyNotFound { key: "a".into() }.to_string()` is `key not found: +a`, which is the message every binding's error carries. The variant's doc +comment is the code's documented default message in the IDL (its +`message:`) and in generated docs. + +To write `Display` yourself (with thiserror, say), opt out with +`#[weaveffi::error(no_display)]`; the enum must then implement `Display`, +and the macro generates neither impl. thiserror's `#[error("...")]` +attributes pass through untouched: + +```rust +#[weaveffi::error(no_display)] +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + /// not a number + #[error("not a number")] + NotANumber = 1, } ``` -A module declares at most one domain; it's in scope for that module and every -module nested in it, and a `Result` with no domain in scope is a compile -error, whatever its error type. With a domain in scope, `Result` -compiles too (it reports the generic code `-1`), and any type implementing -`weaveffi::ErrorReport` can be an error type. A panic in producer code is -caught at the boundary and reported as code `-2`. -See [Errors and Memory](errors-and-memory.md). +A module may declare any number of domains, and any callable in the module +tree may throw any of them. Code values only need to be unique within their +domain (the calculator sample's `CalcError::DivisionByZero` and +`ParseError::NotANumber` are both `1`), but code names must be unique across +the whole API, because several targets flatten them into one namespace. +Domains are open: adding a code later never breaks a deployed binding, which +reports an unknown code as the domain's base error with its code and +message. ## Records and enums @@ -183,10 +278,56 @@ pub enum Shape { ``` Records and rich enums get a generated `weaveffi::abi::BufferValue` -implementation, and a C-style enum implements `weaveffi::abi::CEnum`; none -of them exports a C function. Rich-enum variants use named fields (tuple -variants are rejected), and tags follow declaration order unless the variant -declares a discriminant. +implementation, and a C-style enum implements `weaveffi::abi::Scalar` (its +ABI value is an `i32`); none of them exports a C function. Rich-enum +variants use named fields (tuple variants are rejected), and tags follow +declaration order unless the variant declares a discriminant. A field may +be any value type, including `usize`, `char`, a custom type, and an object. + +## Custom types + +A custom type crosses the ABI as another type, its *repr*, and converts at +the boundary with two functions you name. Declare it on a type alias inside +the module tree: + +```rust +#[weaveffi::module] +pub mod codec { + /// A `u32` written in lowercase hexadecimal. + #[weaveffi::custom(repr = String, lift = parse_hex, lower = format_hex)] + pub type Hex = u32; + + fn parse_hex(text: String) -> Result { + u32::from_str_radix(&text, 16) + } + + fn format_hex(value: &u32) -> String { + format!("{value:x}") + } + + /// Return `value` normalized. + #[weaveffi::export] + pub fn echo_hex(value: Hex) -> Hex { + value + } +} +``` + +- `repr` is a type the macro can export on its own (`String`, an integer, + `Vec`, ...). The IDL, the contract, and every binding see the repr, + so `echo_hex` is `echo_hex(value: string) -> string`. +- `lift: fn(Repr) -> Result` with `E: Display` runs on the way in. A + failure fails the call with the marshalling code `-3` and the message + `"{param}: {error}"` (`value: invalid digit found in string`). +- `lower: fn(&T) -> Repr` runs on the way out. +- `lift` and `lower` are paths (or any expression) resolved in the module + that declares the alias; the aliased type may live anywhere + (`pub type Id = uuid::Uuid;`). + +A custom type works in every position: parameters, returns, async results, +iterator items, callback parameters and returns, record, rich-enum, and +error-payload fields, and nested in `Option`, `Vec`, and maps. Bindings +expose the repr; mapping it to a language's own type is up to the consumer. ## Interfaces @@ -195,22 +336,24 @@ collections, and in-flight async calls), so the type must be `Send + Sync` and keeps mutable state behind interior mutability. ```rust +use std::sync::{Arc, Mutex}; + #[weaveffi::interface] pub struct Shelf { - titles: std::sync::Mutex>, + titles: Mutex>, } impl Shelf { /// Constructor: no receiver, returns `Self` or `Arc`. pub fn new() -> Self { - Self { titles: Default::default() } + Self { titles: Mutex::new(Vec::new()) } } /// Fallible constructor. pub fn open(name: String) -> Result, LibraryError> { /* ... */ } /// Method on `&self`. - pub fn count(&self) -> i64 { /* ... */ } + pub fn count(&self) -> usize { /* ... */ } /// Method on `self: Arc`, returning another reference to itself. pub fn share(self: Arc) -> Arc { self } @@ -222,10 +365,16 @@ impl Shelf { pub async fn duplicate(self: Arc) -> Arc { /* ... */ } /// Static: no receiver, returns something other than `Self`. - pub fn capacity() -> i64 { 10_000 } + pub fn capacity() -> u32 { 10_000 } /// Async static factory: an `async fn` returning `Self` is a static. pub async fn load(name: String) -> Result, LibraryError> { /* ... */ } + + /// A Rust-only helper: `pub` for the crate, but not exported. + #[weaveffi::skip] + pub fn titles_mut(&self) -> std::sync::MutexGuard<'_, Vec> { + self.titles.lock().unwrap() + } } ``` @@ -240,6 +389,10 @@ impl Shelf { `Option>`) is a new strong reference the thunk took for you, so you may store it. A by-value `T` parameter is rejected. - An object you return transfers one strong reference to the consumer. +- Every `pub fn` of the type's inherent `impl` blocks is exported unless + it's marked `#[weaveffi::skip]`, so a public helper whose signature can't + cross the ABI (a guard, a closure, a borrowed iterator) stays usable from + Rust. Private functions are never exported. The macro also emits `{prefix}_{path}_{Type}_clone` and `_destroy`. Every binding wraps the reference in a class that releases it exactly once. @@ -251,118 +404,134 @@ it and the producer calls it, from any thread, for as long as it holds the `Arc`. When the last clone drops, the consumer's release hook runs exactly once (on whichever thread drops it). -Every method returns `Result`, because the -consumer's implementation can fail and the `Err` is how its failure reaches -you; `T` maps to the IDL return. These are two of the `kvstore` sample's -callback interfaces: +Every method returns `Result`, because the consumer's implementation +can fail and the `Err` is how its failure reaches you. `T` maps to the IDL +return, and `E` must implement `From`, which carries +any failure the method can't express as `E` directly. These are the +`kvstore` sample's `Policy` and `Scorer`: ```rust use std::sync::Arc; use weaveffi::ForeignError; -/// Retained by `Store::subscribe` and told about every change. -#[weaveffi::callback_interface] -pub trait Listener: Send + Sync { - /// A direct return. - fn accepts(&self, key: &str) -> Result; - /// No return; the change (a rich enum) is lent for the call. - fn on_change(&self, change: &Change) -> Result<(), ForeignError>; -} - /// Consulted by every `put` while installed. #[weaveffi::callback_interface] pub trait Policy: Send + Sync { - /// A record return. The consumer may fail with a `KvError` code. - #[weaveffi::throws] - fn admit(&self, entry: &Entry) -> Result; + /// An optional scalar in and out. The consumer may fail with a `KvError`. + fn ttl_for(&self, key: &str, requested: Option) -> Result, KvError>; + /// A record return. The consumer may fail with a `KvError`. + fn admit(&self, entry: &Entry) -> Result; /// The consumer adopts `home` and returns a reference you adopt. fn route(&self, key: &str, home: Arc) -> Result, ForeignError>; } + +/// Scores entries for `Store::rank`. +#[weaveffi::callback_interface] +pub trait Scorer: Send + Sync { + /// A typed array in and out. + fn scores(&self, sizes: &[u64]) -> Result, ForeignError>; +} ``` The rules: - The receiver is `&self`. Methods are synchronous. -- Parameters may be any IDL type except a callback interface or an iterator. - Strings, bytes, and buffered values are lent to the consumer for the call; - an object parameter transfers one strong reference to the consumer, so - it's spelled `Arc` (or `Option>`), never `&T`. -- The return is `Result`, where `T` is `()` or any IDL type - except an iterator or a callback interface: a direct value, a `String`, a - `Vec`, a record, rich enum, `Option`, `Vec`, or map, or an object - (`Arc`, or `Option>` when the consumer may return none). A plain - `T` is a compile error. The consumer allocates a returned string, bytes, - or buffer with `{prefix}_alloc`, and the runtime adopts it for you. +- Parameters may be any value type (not a callback interface or an + iterator). Strings, bytes, typed arrays, and buffered values are lent to + the consumer for the call; an object parameter transfers one strong + reference to the consumer, so it's spelled `Arc` (or `Option>`), + never `&T`. +- The return is `Result`, where `T` is `()` or any value type: a + scalar, an optional scalar, a `String`, a `Vec`, a typed array, a + record, rich enum, `Option`, `Vec`, or map, or an object (`Arc`, or + `Option>` when the consumer may return none). A plain `T` is a + compile error. The consumer allocates a returned run with + `{prefix}_alloc`, and the runtime adopts it for you. - A callback interface may appear only as a top-level parameter of a function or an interface member, either bare (`Arc`) or optional (`Option>`, where `None` is a null vtable), never in a return, a record, a collection, or another callback. -**When the consumer fails,** the method returns -`Err(ForeignError { code, message, payload })`. Nothing unwinds and nothing -is deferred; the failure is a value you handle or propagate. `code` is `-4` -for any consumer failure, except that a method marked -`#[weaveffi::throws]` keeps a code of the error domain in scope, and it's -`-3` when the consumer returned a value the runtime couldn't accept (text -that isn't UTF-8, a null object, an undeclared enum value, a malformed -buffer). - -To propagate the failure, return an error type that reports a -`ForeignError`'s own code, message, and payload, and use `?`. That's -`ForeignError` itself (its `ErrorReport` implementation does exactly this), -or your own enum implementing `weaveffi::ErrorReport` that wraps either a -domain error or a `ForeignError`, as the `kvstore` sample's `StoreError` does -for `Store::put`. The function is `throws`, so its module needs an error -domain in scope. The original caller sees the consumer's code, message, and -payload, so a typed domain error from a `throws` method reaches it typed. - -**Typed callback errors.** A method marked `#[weaveffi::throws]` is `throws` -in the IDL, which needs an error domain in scope for its module. Its -consumer may fail with one of the domain's codes, fields included (but no -object fields), and `ForeignError::domain::()` decodes that code back -into your `#[weaveffi::error]` enum, returning `None` for any other failure: +**The error type decides what the method throws.** When `E` is a +`#[weaveffi::error]` domain of the module tree, the method is `throws: E`: +the consumer may fail with one of `E`'s codes (fields included, but no +object fields), and it arrives as that typed variant, its message rendered +by your `Display` from the fields. Any other consumer failure (an untyped +exception, a code `E` doesn't declare) reaches `E` through +`From`. When `E` is anything else (`ForeignError` itself, or +an `anyhow`-style type implementing `From`), the method is +`throws: any`, and every consumer failure goes through `From`. + +The `kvstore` sample maps foreign failures into its own domain, so `put` +can use `?` on store errors and callback errors alike: ```rust #[weaveffi::error] #[derive(Debug)] #[repr(i32)] -pub enum LookupError { - /// No such key. - Missing { key: String } = 1, - /// The source is busy. - Busy = 2, +pub enum KvError { + /// write rejected by policy + #[weaveffi(message = "write to {key} rejected: {reason}")] + Rejected { key: String, reason: String } = 1005, + /// a consumer callback failed + #[weaveffi(message = "{message}")] + CallbackFailed { message: String } = 1006, } -#[weaveffi::callback_interface] -pub trait Source: Send + Sync { - /// Look up a key, failing with a `LookupError`. - #[weaveffi::throws] - fn lookup(&self, key: &str) -> Result; +impl From for KvError { + fn from(e: ForeignError) -> Self { + Self::CallbackFailed { message: e.message } + } } -/// Describe the outcome of a lookup without failing. -#[weaveffi::export] -pub fn lookup_via(source: Arc, key: String) -> String { - match source.lookup(&key) { - Ok(v) => format!("ok {v}"), - Err(e) => match e.domain::() { - Some(LookupError::Missing { key }) => format!("missing {key}"), - Some(LookupError::Busy) => "busy".to_string(), - None => format!("foreign {}: {}", e.code, e.message), - }, +impl Store { + pub fn put(self: Arc, key: String, value: Vec) -> Result { + let policy = self.policy(); + let mut entry = Entry::new(&key, value); + if let Some(policy) = &policy { + entry = policy.admit(&entry)?; // a consumer's Rejected arrives typed + let home = policy.route(&key, Arc::clone(&self))?; // ForeignError -> CallbackFailed + return home.insert(entry); + } + self.insert(entry) } } ``` -(`LookupError` needs a `Display` impl, omitted here.) +A consumer that rejects a write with `Rejected { key: "k", reason: +"read-only" }` makes `put` fail with that same typed error, and the original +caller sees code `1005`, its fields, and the message `write to k rejected: +read-only`. + +**`ForeignError`** is the failure the runtime hands to `From`: + +```rust +pub struct ForeignError { + pub code: i32, // -4 for a consumer failure, -3 for a return the runtime couldn't accept + pub message: String, // the consumer's message + pub payload: Vec, // a domain code's fields as a value buffer, else empty +} +``` + +`code` is `-4` when the consumer's implementation failed (or, for a +[thread-affine](errors-and-memory.md#thread-affine-callbacks) vtable, when +a value-returning method was called off the consumer's thread, with the +message `callback called off its thread`), and `-3` when the consumer +returned a value the runtime couldn't accept (text that isn't UTF-8, a +`char` that isn't one scalar, a null object, an undeclared enum value, a +malformed buffer, a misaligned array). An empty consumer message becomes +"callback interface implementation failed". `ForeignError` implements +`Display`, so it's a valid error type for an exported function too (which is +then `throws: any`), and `ForeignError::domain::()` still decodes a +declared domain code by hand when you need it. **Optional callbacks.** Accept `Option>` when the consumer may pass none: ```rust #[weaveffi::export] -pub fn has_source(source: Option>) -> bool { - source.is_some() +pub fn rank_with(scorer: Option>) -> bool { + scorer.is_some() } ``` @@ -375,10 +544,11 @@ Snapshot the state you need, release the lock, then call out, as Return `weaveffi::Iter` (built with `Iter::new` from any `Send + 'static` iterator) when the consumer should pull elements lazily instead of receiving -a materialized list. It may be wrapped in a `Result`, and `T` may be any IDL -type, including an object. Iterators are returns of synchronous callables -only; an `Iter` parameter, a nested `Iter`, or an async function returning one -is rejected. +a materialized list. It may be wrapped in a `Result`, and `T` may be any +value type, including an object, an optional scalar, or a typed array +(`weaveffi::Iter>`, `weaveffi::Iter>`). Iterators are +returns of synchronous callables only; an `Iter` parameter, a nested `Iter`, +or an async function returning one is rejected. ## Async functions and cancellation @@ -389,9 +559,9 @@ returns immediately and a completion callback that fires exactly once. ```rust #[weaveffi::export] #[weaveffi::cancellable] -pub async fn wait(timeout_ms: i64, cancel: weaveffi::CancelToken) -> i64 { +pub async fn wait(timeout_ms: u64, cancel: weaveffi::CancelToken) -> u64 { let _ = cancel; // the runtime drops this future when the token fires - Delay::new(timeout_ms as u64).await; // any future the executor can drive + tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)).await; timeout_ms } ``` @@ -399,19 +569,20 @@ pub async fn wait(timeout_ms: i64, cancel: weaveffi::CancelToken) -> i64 { When the consumer cancels, the runtime drops your future at its next suspension point and completes the call with code `-5`; you don't need to poll the token. Poll `cancel.is_cancelled()` only for cooperative cleanup, -such as work running on another thread. Futures run on a small pool of -worker threads by default, on Tokio with the `weaveffi` crate's `tokio` -feature, or on the executor installed with `weaveffi::set_spawner`; a future -the executor drops without finishing also completes with `-5`. See +such as work running on another thread. Futures run on Tokio by default +(the `weaveffi` crate's default `tokio` feature), on a thread per call with +`default-features = false`, or on the executor installed with +`weaveffi::set_spawner`; a future the executor drops without finishing also +completes with `-5`. See [Async and Cancellation](async.md#choosing-an-executor). ## Modules and cross-module references Each `#[weaveffi::module]` at the top level of the crate is a root; nested `#[weaveffi::module]` modules are its submodules, and their symbols carry the -joined path (`kvstore_kv_stats_summarize`). Inside one root tree, -any module may use any declaration in the tree: records, enums, interfaces, -callback interfaces, and the error domains of its ancestors. +joined path (`kvstore_kv_stats_summarize`). Inside one root tree, any module +may use any declaration in the tree: records, enums, interfaces, callback +interfaces, custom types, and error domains. Names are global across every root: type, free-function, and error-code names must each be unique in the whole crate's API (see @@ -432,20 +603,22 @@ error[E0277]: `Color` is not a WeaveFFI record or rich enum; declare C-style enu = note: nest the modules under one `#[weaveffi::module]` root (as inner `mod`s) so the macro can see the declaration ``` -The fix is in the message: put the modules under one root. The `kvstore` -sample shows both shapes: a nested `kv::stats` that uses the parent's -interface and enum and reports the parent's error domain, and a sibling -`report` root that shares only the `Entry` record. +The fix is in the message: put the modules under one root. For the same +reason, an error domain declared in a sibling root isn't a domain to the +macro, so a `Result` using it is `throws: any`. The `kvstore` sample shows +both shapes: a nested `kv::stats` that uses the parent's interface and enum +and throws the parent's error domain, and a sibling `report` root that +shares only the `Entry` record and declares its own domain. ## Conditional compilation and type aliases A `#[cfg]` on an exported item (a function, an `impl` block, a record, -enum, error domain, interface struct, callback trait, or nested +enum, error domain, interface struct, callback trait, custom type, or nested `#[weaveffi::module]`) applies to everything the macro generates for it: its thunks and its contract table entries. An item the build compiles out has no symbols, no entry, and no metadata, so the library never claims a -declaration it doesn't have. To make a single interface member conditional, move it into -its own `impl` block and put the `#[cfg]` on that block: +declaration it doesn't have. To make a single interface member conditional, +move it into its own `impl` block and put the `#[cfg]` on that block: ```rust #[weaveffi::interface] @@ -489,19 +662,30 @@ pub mod tree { } ``` +## Hygiene + +The thunks name every parameter, lifted argument, and pattern binding with a +`__wv_` prefix (`__wv_out_err`, `__wv_p_key`, `__wv_f_message`), so nothing +the producer declares can shadow them: a module may define a constant or +static called `out_err`, `out_len`, `callback`, `context`, or `message` +without breaking the expansion. The C slot names themselves come from the +IDL parameter names; when two slots of one function would collide in C (a +parameter `name` next to one called `name_ptr`, or a parameter called +`out_err`), validation reports `SlotCollision` on the function. + ## Leak checks The `leak-check` cargo feature (on `weaveffi`) counts live objects, foreign -callbacks, iterators, cancel tokens, and returned allocations. -`{prefix}_debug_live(kind)` reports them (`0` objects, `1` callbacks, -`2` iterators, `3` tokens, `4` byte runs), and kind `-1` returns `1` to say -the library counts. Without the feature the symbol still exists and returns -`0` for every kind. Every sample enables it, and every conformance consumer -asserts all five counts are zero at exit. +callbacks, iterators, cancel tokens, and byte runs (including error +messages). `{prefix}_debug_live(kind)` reports them (`0` objects, +`1` callbacks, `2` iterators, `3` tokens, `4` byte runs), and kind `-1` +returns `1` to say the library counts. Without the feature the symbol still +exists and returns `0` for every kind. Every sample enables it, and every +conformance consumer asserts all five counts are zero at exit. ```toml [dependencies] -weaveffi = { version = "0.24", features = ["leak-check"] } +weaveffi = { version = "0.25", features = ["leak-check"] } ``` ## What the macro rejects @@ -511,17 +695,21 @@ Each rejection is a spanned compile error, most of them pinned by a | Source | Why | |--------|-----| +| a marker attribute outside a `#[weaveffi::module]` | Put the item in an inline module annotated `#[weaveffi::module]`. | +| a crate without `weaveffi::export_runtime!()` | Every library exports the runtime once (`no __weaveffi_runtime in the root`). | | `*const T`, `*mut T` | Declare the pointee as an interface and pass `&T` or `Arc`. | -| `usize`, `isize`, `u128`, `i128`, `char` | Use `u64` or `i64` (the same width everywhere), or `String`. | +| `u128`, `i128` | No IDL type; use `u64`, `i64`, `usize`, `isize`, or a custom type with a `String` repr. | | interface parameter by value (`T`) | Take `&T` to borrow it or `Arc` to keep it. | | `Box`, `Rc`, `Box` | Objects and callbacks are shared: use `Arc`, `Arc`. | | `&mut T` parameter | Take `&T` or a value and return the result. | | interface method on `self` or `&mut self` | Use `&self` or `self: Arc` with interior mutability. | | interface that isn't `Send + Sync` | The compiler names the offending field. | | callback method on `&mut self` | Take `&self`; the producer may call it from any thread. | -| callback method returning a plain `T` | Return `Result`. | -| `Result` with no error domain in scope | Declare a `#[weaveffi::error]` enum in the module or an ancestor. | -| `#[weaveffi::error]` enum without `Display` | `Display` supplies the runtime message. | +| callback method returning a plain `T` | Return `Result` with `E: From`. | +| callback method error type without `From` | The conversion carries every failure that isn't one of `E`'s codes. | +| `Result` (not a domain) where `E` isn't `Display` | A `throws: any` failure's message is `E`'s `Display` output. | +| `#[weaveffi::error(no_display)]` enum without `Display` | You opted out of the generated impl. | +| `#[weaveffi::custom]` without `repr`, `lift`, or `lower` | All three are required. | | C-style enum without `#[repr(i32)]` | The discriminant crosses as `int32_t`. | | `Iter` parameter or nested `Iter` | Iterators are outermost returns only. | | non-value type from another root | See [cross-module references](#modules-and-cross-module-references). | @@ -529,10 +717,10 @@ Each rejection is a spanned compile error, most of them pinned by a | out-of-line submodule (`mod x;`) | The macro reads the tree from its tokens; write the module inline. | Also rejected: tuple-style rich-enum variants, error variants without an -explicit discriminant, two `#[weaveffi::error]` enums in one module, an -`Arc` naming more than one trait, and anything else the validator -rejects in the tree. The macro validates one root tree at a time, so the -rules that span roots (names unique across the whole crate's API, and +explicit discriminant, an error domain named `any`, an `Arc` +naming more than one trait, and anything else the validator rejects in the +tree. The macro validates one root tree at a time, so the rules that span +roots (names unique across the whole crate's API, and [C symbol collisions](../reference/idl.md#validation) between roots) are checked by the CLI; run `weaveffi validate` or `weaveffi generate` in CI. diff --git a/docs/src/intro.md b/docs/src/intro.md index 31c6a0c8..768a5e9b 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -2,7 +2,7 @@ **WeaveFFI generates idiomatic bindings for 11 languages from one API definition, over a stable C ABI.** Write the API as safe Rust annotated with -`#[weaveffi::module]`, or as a YAML, JSON, or TOML IDL implemented by any +`#[weaveffi::module]`, or as a YAML or JSON IDL implemented by any library that can export C symbols (C, C++, Zig, and so on). WeaveFFI emits packages for C, C++, Swift, Kotlin, Node.js, WebAssembly, Python, .NET, Dart, Go, and Ruby that all call the same C functions. @@ -24,24 +24,30 @@ native `library` base name. For a Rust producer all three come from the crate or NuGet package named `kvstore`). Nothing a generator emits is named after WeaveFFI, so any number of WeaveFFI-built libraries can live in one process. -**A small, explicit C ABI.** [Revision 4](reference/abi.md) has five value -families: direct scalars, UTF-8 strings and bytes as `(ptr, len)` runs, -serialized [value buffers](reference/value-buffers.md) for records, enums, -optionals, lists, and maps, reference-counted objects, and +**A small, explicit C ABI.** [Revision 5](reference/abi.md) passes every +value in one of a few families: direct scalars, optional scalars as a +presence flag plus the value, numeric lists as typed arrays, UTF-8 strings +and bytes as `(ptr, len)` runs, serialized +[value buffers](reference/value-buffers.md) for records, rich enums, and the +remaining optionals, lists, and maps, and reference-counted objects, plus consumer-implemented callback vtables. Every fallible call reports through a -`{prefix}_error` struct. Async functions complete through a callback that -fires exactly once, and cancellation is a first-class runtime code. Every -consumer checks the ABI revision and each module's contract table (a hash of -every declaration's signature) when it loads the library, so a stale binding -fails loudly, naming the declaration that changed, instead of misreading -memory. +`{prefix}_error` struct carrying a code, a length-delimited message, and the +code's fields. Async functions complete through a callback that fires +exactly once, and cancellation is a first-class runtime code. Every consumer +checks the ABI revision and each module's contract table (a hash of every +declaration's signature, error code, and callback method) when it loads the +library, so a stale binding fails loudly, naming the declaration that +changed, instead of misreading memory, while a library that only added +declarations, codes, or callback methods keeps loading. **Idiomatic generators.** Each target maps the model onto its own idioms: objects become classes with deterministic disposal and a garbage-collector backstop, async functions become `async`/`await`, coroutines, promises, or tasks with native cancellation, iterators stay lazy, and error domains become typed exceptions or error values. The -[capability matrix](generators/README.md) summarizes all eleven. +[capability matrix](generators/README.md) summarizes all eleven: C, C++, +Swift, Kotlin, Python, Node.js, and .NET are Tier 1, and Go, Ruby, Dart, and +WebAssembly are Tier 2 (see [Stability](stability.md#target-tiers)). **Standalone output.** Generated packages are self-contained: helper code (the value-buffer codec, error types, object wrappers, loaders) is generated @@ -59,4 +65,4 @@ bundles prebuilt native libraries into publishable packages per ecosystem. - [Comparison](comparison.md) and [FAQ](faq.md): how WeaveFFI relates to UniFFI, Diplomat, cbindgen, and the single-language bridges. - [Stability and Versioning](stability.md): what the version numbers - promise, and how to migrate to schema 0.11 and ABI revision 4. + promise, and how to migrate to schema 0.12 and ABI revision 5. diff --git a/docs/src/reference/abi.md b/docs/src/reference/abi.md index 8970d50b..2e7535f0 100644 --- a/docs/src/reference/abi.md +++ b/docs/src/reference/abi.md @@ -1,6 +1,6 @@ # C ABI Contract -This page is the normative description of the WeaveFFI C ABI, **revision 4**. +This page is the normative description of the WeaveFFI C ABI, **revision 5**. The `#[weaveffi::module]` macro implements it on the producer side, the C generator renders it as a header, and every other generator consumes it. Where another page disagrees with this one, this one wins. @@ -15,6 +15,9 @@ Where another page disagrees with this one, this one wins. the same prefix uppercased, used for macros (`KVSTORE`). - **`{path}`**: a module path joined with `_` (`kv`, `kv_stats`). - **Slot**: one C parameter. An IDL parameter lowers to one or more slots. +- **Run**: a contiguous allocation of bytes that crosses the boundary + (a string, bytes, a value buffer, a typed array, an error message or + payload). Every identifier the ABI defines starts with `{p}_` (functions, types, constants) or `{P}_` (macros). There are no fixed `weaveffi_*` symbols, so any @@ -25,17 +28,33 @@ dynamically. A consumer makes two checks once, when it loads the library, before any other call, and refuses to load if either fails (with the language's load or -import error, or a trap where loading can't throw, as in Swift and Go). +import error, or a catchable check function where loading itself can't +fail, as in Swift and Go). **The revision.** `{P}_ABI_VERSION` in the header and `{p}_abi_version()` in the library must be equal. The revision changes only when the runtime surface, the value-buffer encoding, or the object, callback, async, or iterator conventions change incompatibly; it's independent of the crate -version and the IDL schema version. Revision 4 replaced revision 3's -per-module checksums with the per-declaration contract tables below, gave -callback vtables their size-checked header and every return family (with -`throws` and optional callback parameters), exported `{p}_alloc` on every -target (removing `{p}_dealloc`), and added `{p}_error_set_payload`. +version and the IDL schema version. + +Revision 5 changed, relative to revision 4: + +- enum, rich-enum tag, and error-code types are `typedef int32_t` with + anonymous-enum constants, never `typedef enum`; +- the error struct's message is a length-delimited UTF-8 run + (`message_ptr`, `message_len`), and `{p}_error_set` takes one; +- optional scalars (the OptDirect family) and numeric lists (the Slice + family) cross directly instead of in value buffers; +- every run the producer hands out, and every `{p}_alloc` run, is 8-aligned; +- the contract tables exclude parameter and field names, name the error + domain a callable throws, and give every error code and every callback + method an entry of its own, so error domains and callback interfaces are + open; +- callback vtables define the `{P}_VTABLE_THREAD_AFFINE` flag. + +Revision 5 is designed to grow without a revision 6: new declarations, new +callback methods (a vtable's `size` says how many it has), and new error +codes are additive. **The contract.** Every top-level module `m` has a contract table: one entry per declaration in `m` and its submodules. @@ -47,52 +66,72 @@ const {p}_contract_entry* {p}_{m}_contract(size_t* out_len); /* static, sorted A declaration is a function, an interface, each interface member (constructor, method, or static), a record, an enum, a callback interface, -or an error domain. Its entry is: +each callback method, an error domain, or each error code. Its entry is: - **`id`**: the 64-bit FNV-1a hash of its dotted path, the module path then - the name (`kv.open_store`, `kv.Store`, `kv.Store.get`, `kv.KvError`, + the name (`kv.open_store`, `kv.Store`, `kv.Store.get`, `kv.Listener`, + `kv.Listener.on_put`, `kv.KvError`, `kv.KvError.NotFound`, `kv.stats.Stats`); - **`hash`**: the 64-bit FNV-1a hash of its canonical signature string. The canonical signature is computed from the resolved model, never from source spelling: type aliases are substituted, `[u8]` is `bytes`, and each type is written in its IDL spelling (`i32`, `string`, `[Contact]`, -`{string:i64}`, `Store?`, `iter`). Documentation, deprecation text, -and error messages are excluded, and so is the order of sibling -declarations, because each entry stands alone. +`{string:i64}`, `Store?`, `Listener?`, `iter`). Parameter names, field +names, documentation, deprecation text, and error messages are excluded, +and so is the order of sibling declarations, because each entry stands +alone. | Declaration | Canonical signature | |---|---| -| function or interface member | `{kind} {name}({param}: {type}, ...) -> {type}`, where `kind` is `function`, `constructor`, `method`, or `static` and a missing return is `void`, followed by ` throws`, ` async`, and ` cancellable` in that order when set (a constructor's return is its interface) | -| record | `record {name} {{field}: {type}, ...}` | -| enum | `enum {name} {{Variant} = {value}, ...}`, a variant with fields followed by ` {{field}: {type}, ...}` | -| callback interface | `callback {name} {{method}({param}: {type}, ...) -> {type}; ...}`, a method that throws followed by ` throws` | -| error domain | `errors {name} {{Code} = {value}, ...}`, a code with fields followed by ` {{field}: {type}, ...}` | +| function or interface member | `{kind} {name}({type}, ...) -> {type}`, where `kind` is `function`, `constructor`, `method`, or `static` and a missing return is `void`, followed by ` throws {Domain}` or ` throws any`, then ` async`, then ` cancellable`, each when set (a constructor's return is its interface) | | interface | `interface {name}` (each member has its own entry) | +| record | `record {name} {{type}, ...}`: field types only, in order | +| enum | `enum {name} {{Variant} = {value}, ...}`, a variant with fields followed by ` {{type}, ...}` | +| callback interface | `callback {name}` (each method has its own entry) | +| callback method | `callback_method #{slot} {name}({type}, ...) -> {type}`, where `slot` is the method's zero-based position in the vtable and a missing return is `void`, followed by ` throws {Domain}` or ` throws any` when set | +| error domain | `errors {name}` (each code has its own entry) | +| error code | `code {Code} = {value}`, a code with fields followed by ` {{type}, ...}` | + +Variant and code names stay in their signatures because they're values a +consumer sees; parameter and field names don't affect the encoding, so +renaming one is ABI-neutral. The flip side is that swapping two parameters or +fields of the same type passes the check too, and the values then reach the +swapped names; treat that as a breaking change even though the check can't +see it. + +A callback method's slot is part of its signature because the vtable is laid +out in declaration order: reordering methods, or inserting one before +another, moves slots and fails the check for every method that moved, while +appending a method leaves every existing slot, and hash, unchanged. For example, the `kvstore` sample's `fn get(&self, key: String) -> Result` on interface `Store` in module `kv` has the id `fnv1a64("kv.Store.get")` (`0x3bf340d3781ddb6d`) and the hash -`fnv1a64("method get(key: string) -> Entry throws")` -(`0xd86b757fadf495cb`). FNV-1a 64 starts from `0xcbf29ce484222325` and, for +`fnv1a64("method get(string) -> Entry throws KvError")` +(`0x1898683461a88009`). FNV-1a 64 starts from `0xcbf29ce484222325` and, for each byte, XORs the byte in and multiplies by `0x100000001b3`, wrapping. -The header defines the entries the bindings were generated with, and a -checker: +The header defines the entries the bindings were generated with, each +commented with its path and canonical signature, and a checker: ```c #define {P}_{M}_CONTRACT_LEN n -#define {P}_{M}_CONTRACT { {0x...ull, 0x...ull}, /* kv.Store.get */ ... } +#define {P}_{M}_CONTRACT { \ + {0x3bf340d3781ddb6dull, 0x1898683461a88009ull}, /* kv.Store.get: method get(string) -> Entry throws KvError */ \ + ... } static inline uint64_t {p}_{m}_contract_check(void); /* first failing id, or 0 */ ``` A consumer passes when **every entry it was generated with** is present in the producer's table with an equal hash. Entries the producer has and the consumer doesn't are fine, so adding a declaration (a function, a method, a -record) never breaks a deployed binding. A missing entry fails with -"`kv.Store.get` is missing from the library"; a different hash fails with -"`kv.Store.get` changed since these bindings were generated". `{M}` is the module name -uppercased. The macro and the CLI compute the table with the same function +record, an error code, or a callback method at the end of its interface) +never breaks a deployed binding. +A missing entry fails with "`kv.Store.get` is missing from the library"; a +different hash fails with "`kv.Store.get` changed since these bindings were +generated". `{M}` is the module name uppercased. The macro and the CLI +compute the table with the same function (`weaveffi_model::contract::entries`) over the same model, and a Rust producer's table lists only the declarations its build compiles in (an item's `#[cfg]` applies to its entry). @@ -103,21 +142,23 @@ Every producer exports these symbols (`weaveffi::export_runtime!()` emits them for a Rust producer): ```c -#define {P}_ABI_VERSION 4u +#define {P}_ABI_VERSION 5u uint32_t {p}_abi_version(void); typedef struct {p}_error { - int32_t code; /* 0 ok; >0 domain code; <0 runtime code */ - const char* message; /* NUL-terminated UTF-8, producer-owned */ - const uint8_t* payload_ptr; /* the code's fields as a value buffer, or NULL */ + int32_t code; /* 0 ok; >0 domain code; <0 runtime code */ + const uint8_t* message_ptr; /* UTF-8, NOT NUL-terminated, producer-owned */ + size_t message_len; + const uint8_t* payload_ptr; /* the code's fields as a value buffer, or NULL */ size_t payload_len; } {p}_error; -void {p}_error_set({p}_error* err, int32_t code, const char* message); /* copies */ -void {p}_error_set_payload({p}_error* err, const uint8_t* ptr, size_t len); /* copies */ +void {p}_error_set({p}_error* err, int32_t code, + const uint8_t* message_ptr, size_t message_len); /* copies */ +void {p}_error_set_payload({p}_error* err, const uint8_t* ptr, size_t len); /* copies */ void {p}_error_clear({p}_error* err); /* frees message and payload; code = 0 */ void {p}_error_free({p}_error* err); /* clear, then free a heap-boxed error */ -uint8_t* {p}_alloc(size_t len); /* a zero-filled run, NULL for 0 */ +uint8_t* {p}_alloc(size_t len); /* a zero-filled 8-aligned run, NULL for 0 */ void {p}_free_bytes(uint8_t* ptr, size_t len); /* returned runs and {p}_alloc runs */ typedef struct {p}_cancel_token {p}_cancel_token; @@ -126,6 +167,8 @@ void {p}_cancel_token_cancel({p}_cancel_token* token); /* idempotent */ bool {p}_cancel_token_is_cancelled(const {p}_cancel_token* token); void {p}_cancel_token_destroy({p}_cancel_token* token); /* drops one reference */ +#define {P}_VTABLE_THREAD_AFFINE 1u /* a callback vtable flag; see below */ + uint64_t {p}_debug_live(int32_t kind); /* leak counters */ typedef struct {p}_contract_entry { uint64_t id; uint64_t hash; } {p}_contract_entry; @@ -134,25 +177,33 @@ typedef struct {p}_contract_entry { uint64_t id; uint64_t hash; } {p}_contract_e Every function accepts null where a pointer is optional (`error_set`, `error_set_payload`, `error_clear`, `error_free`, `free_bytes`, the token functions, `_clone`, `_destroy`) and treats it as a no-op; a null token reads -as never cancelled. - -`{p}_alloc` and `{p}_free_bytes` manage **byte runs**. Every string, bytes, -or value buffer the producer returns is a run the consumer releases with -`{p}_free_bytes(ptr, len)`, passing the exact length it received. A consumer -allocates a run with `{p}_alloc(len)` when it hands bytes *to* the producer -(a callback method's string, bytes, or buffer return, which the producer -adopts) or, on `wasm32`, when it stages an argument in the module's linear -memory (released with `{p}_free_bytes`). `{p}_alloc(0)` returns null, and -null with length `0` is the empty run everywhere; `{p}_free_bytes(ptr, 0)` -is a no-op. +as never cancelled. `{p}_error_set` with a null `message_ptr` (whatever the +length) sets an empty message, and a producer never fails on message bytes +that aren't UTF-8 (the Rust runtime replaces them with U+FFFD), so +reporting a failure never fails. + +`{p}_alloc` and `{p}_free_bytes` manage **runs**. Every run the producer +hands out (a string, bytes, value buffer, or typed-array return; an error's +message and payload) and every `{p}_alloc(len)` run is allocated with +alignment 8, so a run can be read in place as any element type. The +consumer releases a returned run with `{p}_free_bytes(ptr, len)`, passing +its exact length in **bytes** (`count * sizeof(T)` for a typed array); it +never frees a run with its own allocator. A consumer allocates a run with +`{p}_alloc(len)` when it hands memory *to* the producer (a callback method's +string, bytes, buffer, or typed-array return, which the producer adopts) or, +on `wasm32`, when it stages an argument in the module's linear memory +(released with `{p}_free_bytes`). `{p}_alloc(0)` returns null, and null with +length `0` is the empty run everywhere; `{p}_free_bytes(ptr, 0)` is a no-op. +An error's message and payload belong to the error struct and are released +by `{p}_error_clear` or `{p}_error_free`, never by `{p}_free_bytes`. `{p}_debug_live(kind)` reports live resources for leak checks: `0` objects, -`1` foreign callbacks, `2` iterators, `3` cancel tokens, `4` byte runs -(returned and allocated ones not yet freed or adopted). Kind `-1` returns `1` -when the producer counts at all and `0` when it doesn't, so a harness can -tell "nothing is live" from "nothing is counted"; an unknown kind returns -`0`. A Rust producer counts only with the `leak-check` feature; without it -every kind but `-1` returns `0`. +`1` foreign callbacks, `2` iterators, `3` cancel tokens, `4` runs (returned +and allocated ones not yet freed or adopted). Kind `-1` returns `1` when the +producer counts at all and `0` when it doesn't, so a harness can tell +"nothing is live" from "nothing is counted"; an unknown kind returns `0`. A +Rust producer counts only with the `leak-check` feature; without it every +kind but `-1` returns `0`. ## Errors @@ -166,44 +217,98 @@ success it clears it (releasing anything an earlier failure left there), so one struct can serve many calls. The caller releases a failure's message and payload with `{p}_error_clear`. +The message is `message_len` bytes of UTF-8 at `message_ptr`, without a NUL +terminator (a consumer that needs a C string copies it). An empty message is +a null `message_ptr` with length `0`, even on failure. + | Code | Meaning | |------|---------| -| `0` | success; every pointer is null | -| `> 0` | a declared code of the callable's error domain; `payload_ptr`/`payload_len` hold the code's fields, if any | -| `-1` | generic: an untyped producer error, or an async call the executor couldn't start | +| `0` | success; every pointer is null and every length `0` | +| `> 0` | a code of the error domain the callable throws; `payload_ptr`/`payload_len` hold the code's fields, if any | +| `-1` | untyped: the failure of a `throws: any` callable, or an async call the executor couldn't start | | `-2` | the producer panicked | -| `-3` | marshalling failure: an argument couldn't be lifted (null with a non-zero length, a null `self`, object, or vtable where one is required, invalid UTF-8, an out-of-range enum value, a malformed buffer, a map with a repeated key, a callback vtable smaller than the producer's, an iterator advanced concurrently) | -| `-4` | a consumer's callback-interface implementation failed | +| `-3` | marshalling failure: an argument couldn't be lifted (null with a non-zero length, a null `self`, object, or vtable where one is required, invalid UTF-8, an out-of-range enum value or integer conversion, a misaligned typed array, a malformed buffer, a map with a repeated key, a callback vtable smaller than the producer's, an iterator advanced concurrently) | +| `-4` | a consumer's callback-interface implementation failed, or was called off its thread (see [Callback interfaces](#callback-interfaces)) | | `-5` | cancelled (async completions only) | +What a callable may report depends on its `throws`: + +- **No `throws`.** Only runtime codes. A non-zero code is a producer bug or + a failed callback; consumers trap on it rather than report a typed error + (see [Errors and memory](../guides/errors-and-memory.md)). +- **`throws: {Domain}`.** The domain's positive codes plus runtime codes. + **Domains are open**: a consumer that receives a positive code it wasn't + generated with (the producer gained a code since) maps it to the domain's + base error type with the code and message preserved, never to a crash or + a generic failure. It discards that code's payload without decoding it, + which is why an error code's fields never carry an object (validation + reports `ObjectInErrorPayload`): an undecoded object token would never be + released. +- **`throws: any`.** Code `-1` with a message, plus runtime codes; never a + positive code. + A producer lifts **every** argument of a call before it reports the first one that failed, so the inputs it adopts (a callback context, the object tokens in a value buffer) are adopted and released even when another argument, or a null `self`, fails. -A non-zero code on a callable without `throws` is a producer bug or a failed -callback; consumers trap on it rather than report a domain error (see -[Errors and memory](../guides/errors-and-memory.md)). - ## Families and slots -Every resolved type belongs to one family, which decides how it crosses: - -| Family | IDL types | Parameter slots | Return | -|--------|-----------|-----------------|--------| -| Direct | integers, `f32`, `f64`, `bool`, C-style enums | one by value (`bool` is C `bool`; an enum is its header type `{p}_{path}_{E}`, an `int`-sized value that crosses as `int32_t`) | by value | -| String | `string` | `const uint8_t* {n}_ptr, size_t {n}_len`: UTF-8, not NUL-terminated, borrowed | `const uint8_t*` plus trailing `size_t* out_len` | -| Bytes | `bytes` | same as String | same as String | -| Buffer | structs, rich enums, `T?` (except `Interface?` and `Cb?`), `[T]`, `{K:V}` | `const uint8_t* {n}_ptr, size_t {n}_len` holding a [value buffer](value-buffers.md), borrowed | same as String | -| Object | interfaces, `Interface?` | `const {p}_{path}_{I}* {n}`, borrowed; null means none | `{p}_{path}_{I}*`, one strong reference; null means none | -| Callback | callback interfaces, `Cb?` | `void* {n}_ctx, const {p}_{path}_{Cb}_vtable* {n}_vtable`; for `Cb?` a null vtable means none | not allowed | -| Iterator | `iter` | not allowed | `{Iter}*` (see [Iterators](#iterators)) | - -A parameter `ptr` may be null when its `len` is `0`. A String, Bytes, or -Buffer return is a producer allocation the consumer releases with -`{p}_free_bytes(ptr, len)` using the exact length written to `out_len`; null -with length `0` is the empty value. Out-parameters for a return precede -`out_err`. Methods take the receiver as a leading `const {p}_{path}_{I}* self`. +Every value type belongs to one family, which decides how it crosses a call +boundary. (Inside a [value buffer](value-buffers.md), every type uses the +buffer encoding regardless of its family.) + +| Family | IDL types | +|--------|-----------| +| Direct | integers, `f32`, `f64`, `bool`, C-style enums | +| OptDirect | `T?` where `T` is Direct | +| Slice | `[i8]`, `[i16]`, `[i32]`, `[i64]`, `[u16]`, `[u32]`, `[u64]`, `[f32]`, `[f64]` (not `[bool]`; `[u8]` is `bytes`) | +| String | `string` | +| Bytes | `bytes` | +| Buffer | records, rich enums, every other `T?` (except `Interface?`), every other `[T]`, `{K:V}` | +| Object | interfaces, `Interface?` | + +Two more parameter and return kinds aren't value types: a **callback** +parameter (a callback interface `Cb` or `Cb?`, legal only as a parameter of +a function or interface member) and an **iterator** return (`iter`, +legal only as a return). + +`T` below is the scalar C type: `bool`, `int8_t` ... `uint64_t`, `float`, +`double`, or an enum's header type `{p}_{path}_{E}` (`typedef int32_t`). +`{tag}` is an interface's `{p}_{path}_{I}`. `{n}` is the parameter name. + +| Family | Parameter | Return (before `out_err`) | +|--------|-----------|---------------------------| +| Direct | `T {n}` | C return `T` | +| OptDirect | `bool has_{n}, T {n}` (`{n}` is ignored, pass `0`, when `has_{n}` is false) | C return `bool` (present); trailing `T* out_value`. On failure, returns false | +| Slice | `const T* {n}_ptr, size_t {n}_len` (`len` counts elements; borrowed; aligned for `T`; null allowed when `len` is `0`) | C return `T*`; trailing `size_t* out_len` (element count). Released with `{p}_free_bytes((uint8_t*)ptr, len * sizeof(T))` | +| String, Bytes, Buffer | `const uint8_t* {n}_ptr, size_t {n}_len` (strings are UTF-8 without a NUL terminator; buffers hold a [value buffer](value-buffers.md); borrowed) | C return `const uint8_t*`; trailing `size_t* out_len`. Released with `{p}_free_bytes(ptr, len)` | +| Object | `const {tag}* {n}`, borrowed; null means none for `I?` | C return `{tag}*`, one strong reference; null means none for `I?` | +| Callback | `void* {n}_ctx, const {p}_{path}_{Cb}_vtable* {n}_vtable`; for `Cb?` a null vtable means none | not allowed | +| Iterator | not allowed | C return `{Iter}*` (see [Iterators](#iterators)) | + +A parameter `ptr` may be null when its `len` is `0`, and null with length +`0` is the empty value of a returned run. Methods take the receiver as a +leading `const {tag}* self`. A full synchronous signature is `[self]`, the +parameter slots in declaration order, the return's out slots, then +`out_err`. + +The same families apply in every other position; the [async +result](#async-functions), [iterator item](#iterators), and [callback +method](#callback-interfaces) sections give their slots: + +| Family | Async result | Iterator item | Callback method parameter | Callback method return | +|---|---|---|---|---| +| Direct | `T result` | `T* out_item` | `T {n}` | C return `T` | +| OptDirect | `bool has_result, T result` | `bool* out_has_item, T* out_item` | `bool has_{n}, T {n}` | C return `bool` (present); trailing `T* out_value` | +| Slice | `const T* result_ptr, size_t result_len` | `T** out_item, size_t* out_len` | `const T* {n}_ptr, size_t {n}_len` | `void`; trailing `T** out_ptr, size_t* out_len` | +| String, Bytes, Buffer | `const uint8_t* result_ptr, size_t result_len` | `const uint8_t** out_item, size_t* out_len` | `const uint8_t* {n}_ptr, size_t {n}_len` | `void`; trailing `uint8_t** out_ptr, size_t* out_len` | +| Object | `{tag}* result` | `{tag}** out_item` | `{tag}* {n}` (adopted) | C return `{tag}*` | + +A slot name that two parameters would share (a parameter `name` next to +one called `name_ptr`, or a parameter named `out_err`, `out_len`, +`out_value`, `callback`, `context`, `cancel_token`, or `has_x` beside `x`) +fails validation with `SlotCollision`. ## Symbol names @@ -217,14 +322,23 @@ produce the same identifier. | interface `I` | type `{p}_{path}_I`; members `{p}_{path}_I_{member}`; `{p}_{path}_I_clone`, `{p}_{path}_I_destroy` | | async function `f` | launcher `{p}_{path}_f` (no suffix); completion type `{p}_{path}_f_callback` | | iterator returned by `f` | type `{p}_{path}_{Owner_}{PascalF}Iterator`, plus `{Iter}_next` and `{Iter}_destroy` | -| C-style enum `E`, variant `V` | type `{p}_{path}_E`, constant `{p}_{path}_E_V` | -| rich enum `E`, variant `V` | tag type `{p}_{path}_E_Tag`, constant `{p}_{path}_E_V`; struct `{p}_{path}_E` and its codecs in the buffer header | +| C-style enum `E`, variant `V` | type `{p}_{path}_E` (`typedef int32_t`), constant `{p}_{path}_E_V` | +| rich enum `E`, variant `V` | tag type `{p}_{path}_E_Tag` (`typedef int32_t`), constant `{p}_{path}_E_V`; struct `{p}_{path}_E` and its codecs in the buffer header | | record `S` | struct `{p}_{path}_S` and its codecs in the buffer header | -| error domain `D`, code `C` | type `{p}_{path}_D`, constant `{p}_{path}_D_C`; for a code with fields, struct `{p}_{path}_D_C_payload` and its codecs in the buffer header | +| error domain `D`, code `C` | type `{p}_{path}_D` (`typedef int32_t`), constant `{p}_{path}_D_C`; for a code with fields, struct `{p}_{path}_D_C_payload` and its codecs in the buffer header | | callback interface `Cb` | vtable type `{p}_{path}_Cb_vtable` | | top-level module `m` | `{p}_{m}_contract`, `{p}_{m}_contract_check`, `{P}_{M}_CONTRACT`, `{P}_{M}_CONTRACT_LEN` | | runtime | the names in [Runtime surface](#runtime-surface), plus the buffer header's `{p}_str`, `{p}_bytes`, `{p}_reader`, and `{p}_writer` (reserved) | +Enum, tag, and error-code types are `typedef int32_t` with their constants +in an anonymous `enum`, never `typedef enum`: a C enum's size is the +compiler's choice, and these values always cross as `int32_t`. + +```c +typedef int32_t kvstore_kv_Kind; +enum { kvstore_kv_Kind_Hot = 0, kvstore_kv_Kind_Cold = 1 }; +``` + `{Owner_}` is empty for a free function and the interface name plus `_` for a member, and the function name is converted to PascalCase: `stream_items` in `kitchen` is `kitchen_sink_kitchen_StreamItemsIterator`, and @@ -233,15 +347,16 @@ enums export no functions (they cross as buffers), but the C generator's value-buffer helper header (`{library}_buffer.h`) declares a struct for each, and for each error code with fields, plus four codecs per struct `T` (`T_write`, `T_read`, `T_decode`, and `T_free`), so those names are reserved -like the rest. That header also -reserves the identifier families `{p}_str_*`, `{p}_bytes_*`, `{p}_reader_*`, -`{p}_writer_*`, `{p}_list_*`, `{p}_map_*`, and `{p}_opt_*`: a declaration -whose symbol falls in one (anything in a top-level module named `list`, say) -is a `SymbolCollision`. IDL spellings are never re-cased in C. +like the rest. That header also reserves the identifier families +`{p}_str_*`, `{p}_bytes_*`, `{p}_reader_*`, `{p}_writer_*`, `{p}_list_*`, +`{p}_map_*`, and `{p}_opt_*`: a declaration whose symbol falls in one +(anything in a top-level module named `list`, say) is a `SymbolCollision`. +IDL spellings are never re-cased in C. -The header's macros are `{P}_ABI_VERSION`, the per-module contract macros, -`{P}_API` (symbol visibility; define `{P}_BUILD` when building the producer -on Windows), `{P}_DEPRECATED(msg)`, and the include guard `{P}_H`. +The header's macros are `{P}_ABI_VERSION`, `{P}_VTABLE_THREAD_AFFINE`, the +per-module contract macros, `{P}_API` (symbol visibility; define +`{P}_BUILD` when building the producer on Windows), `{P}_DEPRECATED(msg)`, +and the include guard `{P}_H`. ## Objects @@ -268,7 +383,7 @@ wrapper while a call is pending is safe. ```c typedef struct {p}_{path}_{Cb}_vtable { uint32_t size; /* sizeof the vtable, as the consumer compiled it */ - uint32_t flags; /* reserved; 0 */ + uint32_t flags; /* 0, or {P}_VTABLE_THREAD_AFFINE */ void (*free)(void* ctx); /* always first, at a fixed offset */ /* one entry per method, in declaration order: */ (*{method})(void* ctx, , , {p}_error* out_err); @@ -278,31 +393,49 @@ typedef struct {p}_{path}_{Cb}_vtable { A callback interface has no exported symbols; it lowers to a vtable type. The consumer passes a `ctx` it owns and a pointer to a vtable that outlives the producer's use of it (in practice, one static vtable per interface), -with `size` set to `sizeof` the vtable and `flags` to `0`. The producer -rejects a vtable whose `size` is smaller than the vtable it was built with, -failing the call with `-3` (after calling `free(ctx)`, when the header itself -is complete); a larger `size` is accepted, so a consumer generated from a -newer contract may carry methods this producer doesn't call. For an optional +with `size` set to `sizeof` the vtable. The producer rejects a vtable whose +`size` is smaller than the vtable it was built with, failing the call with +`-3` (after calling `free(ctx)`, when the header itself is complete); a +larger `size` is accepted, so a consumer generated from a newer contract +may carry methods this producer doesn't call. Methods are only ever +appended, so a vtable may grow without a new revision. For an optional callback parameter (`Cb?`) a null vtable pointer means none. The producer may call any entry, any number of times, from any thread, until it calls `free(ctx)` exactly once, after which it never touches `ctx` again. `free` may also run on any producer thread. +**Thread affinity.** Bit 0 of `flags` is `{P}_VTABLE_THREAD_AFFINE` +(`1u`); every other bit is reserved and `0`. With the flag set, a method +that returns a value (a non-`void` C return or any out slot) may only be +called on the thread that passed the vtable to the producer. The producer +records that thread when it adopts the vtable and fails such a call from +any other thread with `-4` and the message `callback called off its thread`, +without calling the method. `void` methods and `free` may still be called +from any thread. The Dart target sets the flag (a Dart callback that +returns a value can only run on its isolate's thread); every other target +passes `0`. + **Parameters** are borrowed for the duration of the call (strings, bytes, -and buffers as `ptr`/`len`), except objects, which transfer one strong -reference the consumer adopts. +buffers, and typed arrays as `ptr`/`len`), except objects, which transfer +one strong reference the consumer adopts. -**Returns** of any family but iterators and callback interfaces: +**Returns** of any family: - A Direct value is the C return (``). +- An OptDirect value makes `` `bool` (present) and adds a trailing + `T* out_value` before `out_err`, which the consumer writes when it + returns true. - An object (`I` or `I?`) is the C return, `{p}_{path}_{I}*`: one strong reference (a fresh `_clone`) the producer adopts. `I?` may return null; `I` must not. - A string, bytes, or buffer return makes `` `void` and adds two trailing slots before `out_err`: `uint8_t** out_ptr, size_t* out_len`. - The consumer allocates the run with `{p}_alloc`, writes it there, and the - producer adopts and frees it. Null with length `0` is the empty value. + The consumer allocates the run with `{p}_alloc(len)`, writes it there, and + the producer adopts and frees it. Null with length `0` is the empty value. +- A typed-array (Slice) return is the same with `T** out_ptr, size_t* + out_len`: `out_len` is the element count, and the run comes from + `{p}_alloc(len * sizeof(T))`. The producer adopts whatever the return and out slots hold whether or not the method failed, so a consumer that fails after allocating doesn't leak. A @@ -310,17 +443,27 @@ return it can't accept (a string that isn't UTF-8, a null `I`, an out-of-range enum value, a malformed buffer) fails the method with `-3`. **Errors.** A consumer implementation that fails reports it with -`{p}_error_set(out_err, code, message)`, which copies a borrowed message with -the producer's allocator; it never writes `message` itself. A method declared -`throws` may report a positive code of the error domain in scope for its -module, attaching the code's fields encoded as a value buffer with -`{p}_error_set_payload` (they may not include objects). Any other non-zero -code, a code the domain doesn't declare or whose payload doesn't decode, or -a positive code on a method without `throws` reaches the producer as `-4`, -so a consumer bug can't masquerade as a domain error. The producer decides -what a failure means for the call in progress: a Rust producer's method -returns `Result`, and propagating the `Err` reports its -code, message, and payload to the original caller. +`{p}_error_set(out_err, code, message_ptr, message_len)`, which copies a +borrowed message with the producer's allocator; it never writes the +message itself. What it reports follows the method's `throws`: + +- `throws: {Domain}`: a positive code of that domain, attaching the code's + fields encoded as a value buffer with `{p}_error_set_payload`, which + reaches the producer as that typed error; or + `-1` with a message for any other failure. +- `throws: any`: `-1` with a message. +- No `throws`: `-4` with a message (any non-zero code is accepted). + +Anything but a declared code with a payload that decodes (`-1`, a code the +domain doesn't declare, any failure of a method without `throws`) reaches +the producer as a callback failure, `-4` with the consumer's message, so a +consumer bug can't masquerade as a domain error. The producer decides what +a failure means for the call in progress: a Rust producer's method returns +`Result`, its `E` built from a typed code or, for a callback failure, +with `From`, and propagating the `Err` reports its +code, message, and payload to the original caller. (A Rust producer +renders a typed error's message from its own fields, so the consumer's +message for a domain code isn't kept.) ## Async functions @@ -330,20 +473,24 @@ void {p}_{path}_{f}(, , [{p}_cancel_token* cancel_token,] {p}_{path}_{f}_callback callback, void* context); ``` -Result slots are: none for a `void` function; `result` for a Direct value or -an object (adopted); or `const uint8_t* result_ptr, size_t result_len` for a -String, Bytes, or Buffer result, released with `{p}_free_bytes`. `err` is -`NULL` on success; otherwise it's heap-boxed, owned by the consumer, and -released with `{p}_error_free`. The launcher has no `out_err`; every failure, -including an input marshalling failure, is delivered through the callback. +Result slots follow the [position table](#families-and-slots): none for a +`void` function; `T result` for a Direct value; `bool has_result, T result` +for an OptDirect value; `const T* result_ptr, size_t result_len` for a +typed array; `const uint8_t* result_ptr, size_t result_len` for a String, +Bytes, or Buffer result; `{tag}* result` for an object (adopted). Runs are +released with `{p}_free_bytes` (a typed array's length times +`sizeof(T)`). `err` is `NULL` on success; otherwise it's heap-boxed, owned +by the consumer, and released with `{p}_error_free`. The launcher has no +`out_err`; every failure, including an input marshalling failure, is +delivered through the callback. The runtime guarantees: - The callback fires **exactly once**, from any thread, possibly before the launcher returns. - The launcher takes everything it needs before returning (it copies - strings, bytes, and buffers and retains objects and `self`), so the - consumer may release its arguments as soon as it returns. + strings, bytes, buffers, and typed arrays and retains objects and + `self`), so the consumer may release its arguments as soon as it returns. - If the executor can't take the call (it failed to start, or a custom executor panicked), the completion fires with `-1` and a message. - If the executor drops the future before it completes, the completion @@ -365,19 +512,26 @@ int32_t {Iter}_next({Iter}* iter, , {p}_error* out_err); /* 1 i void {Iter}_destroy({Iter}* iter); ``` -Item out slots are `T* out_item` for a Direct or object element and +Item out slots follow the [position table](#families-and-slots): `T* +out_item` for a Direct element, `bool* out_has_item, T* out_item` for an +OptDirect element, `T** out_item, size_t* out_len` for a typed array, `const uint8_t** out_item, size_t* out_len` for a String, Bytes, or Buffer -element. Each element follows the return rules of its family. `_next` never -blocks on another `_next`: a call that arrives while the same iterator is -being advanced (from another thread, or re-entrantly from inside the -producer's own `next`) fails with `-3`. The consumer calls `_destroy` exactly -once, after exhaustion or to abandon the iteration. +element, and `{tag}** out_item` for an object. Each element follows the +return rules of its family. `_next` checks that every out slot is non-null +before it pulls an element (failing with `-3` otherwise), so a bad call +never loses one. `_next` never blocks on another `_next`: a call +that arrives while the same iterator is being advanced (from another +thread, or re-entrantly from inside the producer's own `next`) fails with +`-3`. The consumer calls `_destroy` exactly once, after exhaustion or to +abandon the iteration. ## Value buffers Buffers have one encoding, specified in [Value Buffers](value-buffers.md): little-endian, packed, `u32` lengths and counts, object tokens as `u64`. -Error payloads use the same encoding. +Error payloads use the same encoding, and so do OptDirect and Slice values +nested inside a buffer (a record's `i64?` field is a presence byte and a +value; its `[f64]` field is a count and packed elements). ## Producers in other languages @@ -400,6 +554,7 @@ const calculator_contract_entry* calculator_calculator_contract(size_t* out_len) The producer owns every allocation it hands out and adopts every run a consumer allocates, so `error_clear`, `error_free`, `free_bytes`, and the adoption of a callback's returned run must all use the allocator `alloc` -uses. When building the library, define `{P}_BUILD` so the header's `{P}_API` -macro exports symbols on Windows. `conformance/c/producer.c` is a complete -example. +uses, with alignment 8. It honors `{P}_VTABLE_THREAD_AFFINE` on every +vtable it adopts. When building the library, define `{P}_BUILD` so the +header's `{P}_API` macro exports symbols on Windows. +`conformance/c/producer.c` is a complete example. diff --git a/docs/src/reference/idl.md b/docs/src/reference/idl.md index 8c7ac720..5cd0362f 100644 --- a/docs/src/reference/idl.md +++ b/docs/src/reference/idl.md @@ -1,8 +1,11 @@ # IDL Schema -An IDL document describes an API: its modules, types, and functions. YAML, -JSON, and TOML are all accepted; this page uses YAML. It documents schema -version `0.11.0`, the only version the current tools accept. A Rust producer +An IDL document describes an API: its modules, types, and functions. YAML +and JSON are accepted (TOML is only for [`weaveffi.toml`](../guides/config.md), +never for an IDL); this page uses YAML, read as YAML 1.2, so a name YAML +reads as null (`null`, `~`) has to be quoted (`name: "null"`). It +documents schema version +`0.12.0`, the only version the current tools accept. A Rust producer never writes one by hand (the macro embeds the API in the built library, and [`weaveffi extract`](../guides/extract.md) prints it as an IDL), but the schema is the same either way. @@ -25,7 +28,7 @@ Editors using the YAML Language Server pick it up from a header comment: ## Document structure ```text -version: "0.11.0" +version: "0.12.0" modules: - name: my_module doc: "..." @@ -34,7 +37,7 @@ modules: interfaces: [...] callback_interfaces: [...] functions: [...] - errors: { ... } + errors: [...] modules: [...] ``` @@ -44,7 +47,7 @@ keys, so a misspelled field fails to parse instead of being ignored. | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `version` | string | yes | Must be `"0.11.0"` | +| `version` | string | yes | Must be `"0.12.0"` | | `modules` | array of Module | yes | Top-level modules | ### Module @@ -58,7 +61,7 @@ keys, so a misspelled field fails to parse instead of being ignored. | `enums` | array of Enum | C-style or rich enums | | `interfaces` | array of Interface | Reference-counted object types | | `callback_interfaces` | array of CallbackInterface | Method sets the consumer implements | -| `errors` | ErrorDomain | The module's error domain | +| `errors` | array of ErrorDomain | The module's error domains | | `modules` | array of Module | Nested submodules | ### Function @@ -73,7 +76,7 @@ their sections. | `params` | array of Param | Ordered parameters (default none) | | `return` | TypeRef | Return type; omit for none | | `doc` | string | Documentation | -| `throws` | bool | Report typed domain errors (default `false`) | +| `throws` | string | The error domain the callable reports (`KvError`), or `any` for an untyped error; omit when it can't fail (see [Error domains](#error-domains)) | | `async` | bool | Complete asynchronously (default `false`) | | `cancellable` | bool | Accept a cancel token; requires `async: true` | | `deprecated` | string | Deprecation message propagated to every binding | @@ -109,13 +112,23 @@ The parser reads outside in: `[Contact?]` is a list of optional contacts and YAML would otherwise interpret (`"string?"`, `"[i32]"`, `"{string:i32}"`). The Node and Wasm targets surface `i64` and `u64` as `BigInt`. +How a type is spelled never depends on how it crosses, but two shapes +cross faster than the rest at a call boundary: an optional scalar or C-style +enum (`i32?`, `bool?`, `Color?`, the OptDirect family) is a presence flag +plus the value, and a list of a fixed-width number (`[i32]`, `[f64]`, every +integer and float type except `u8` and `bool`, the Slice family) is a typed +array. Every binding still surfaces them as its ordinary optional and list +types; inside a record, variant, or error payload they use the value-buffer +encoding like any other type. See the +[C ABI contract](abi.md#families-and-slots). + **Map keys** must be an integer, `bool`, `string`, or a C-style enum, so every target can use its native dictionary with exact equality. Floats, `bytes`, composites, and objects are rejected: ```yaml # rejected: InvalidMapKey -version: "0.11.0" +version: "0.12.0" modules: - name: maps functions: @@ -132,7 +145,7 @@ and as the return of an async function (`AsyncIteratorReturn`): ```yaml # rejected: IteratorInInvalidPosition -version: "0.11.0" +version: "0.12.0" modules: - name: streaming functions: @@ -174,7 +187,7 @@ class, struct, or record type; no per-struct C functions exist. | `fields` | array of Field (required, non-empty) | Each with `name`, `type`, optional `doc` | ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: geometry structs: @@ -211,7 +224,7 @@ fields, and bindings map it to a Swift enum with associated values, a sealed class hierarchy, a tagged union, and so on. Unit and data variants may mix. ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: shapes enums: @@ -263,23 +276,23 @@ constructor where the target has one (Swift `init`, Python `__init__`); others become static factories. For an async factory, declare an `async` static that returns the interface. -An interface may appear in every position except a map key: parameters, -returns, optionals, lists, map values, record and variant fields, error -payloads, iterator elements, async results, and callback parameters and -returns. A map keyed by an interface reports both `InvalidMapKey` and +An interface may appear in every position except a map key and an error +payload: parameters, returns, optionals, lists, map values, record and +variant fields, iterator elements, async results, and callback parameters +and returns. A map keyed by an interface reports both `InvalidMapKey` and `InterfaceInInvalidPosition`. `Store?` at the top level of a parameter or return is a nullable pointer. Every position transfers or borrows references by the rules in [Errors and Memory](../guides/errors-and-memory.md#objects). ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: kv errors: - name: KvError - codes: - - { name: KeyNotFound, code: 1001, message: "key not found" } + - name: KvError + codes: + - { name: KeyNotFound, code: 1001, message: "key not found" } structs: - name: StoreInfo fields: @@ -291,13 +304,13 @@ modules: - name: open params: - { name: path, type: string } - throws: true + throws: KvError methods: - name: get params: - { name: key, type: string } return: bytes - throws: true + throws: KvError - name: share return: Store - name: larger @@ -313,12 +326,12 @@ modules: params: - { name: paths, type: "[string]" } return: "[Store]" - throws: true + throws: KvError ``` ```yaml # rejected: InvalidMapKey, InterfaceInInvalidPosition -version: "0.11.0" +version: "0.12.0" modules: - name: kv interfaces: @@ -369,12 +382,15 @@ Methods are restricted: (`CallbackInterfaceInInvalidPosition`); an object parameter transfers one reference to the consumer. -A method may set `throws: true` when an error domain is in scope for its -module (otherwise it's `ThrowsWithoutErrorDomain`). Its consumer may then -fail with one of the domain's codes, with the code's fields (which can't -include objects), and the producer receives the typed error. Any other -consumer failure, and every failure of a method without `throws`, reaches -the producer as the runtime code `-4`. +A method may declare `throws` like any callable. With `throws: KvError` its +consumer may fail with one of the domain's codes, with the code's fields +(which can't include objects), and the producer receives the typed error; +with `throws: any` a consumer failure reaches the producer as an untyped +error with its message. Every failure of a method without `throws`, and a +code its domain doesn't declare, reaches the producer as the runtime code +`-4`. Each callback method is its own contract entry, so appending a method +to a callback interface doesn't break a deployed binding; reordering methods +or inserting one before others moves vtable slots and does. A callback interface is valid only as a top-level parameter of a function, constructor, method, or static, passed bare (`Listener`) or optional @@ -383,7 +399,7 @@ constructor, method, or static, passed bare (`Listener`) or optional ```yaml # rejected: CallbackInterfaceInInvalidPosition -version: "0.11.0" +version: "0.12.0" modules: - name: events callback_interfaces: @@ -396,7 +412,7 @@ modules: ``` ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: events enums: @@ -433,11 +449,11 @@ modules: ``` A callback interface lowers to a vtable type that starts with a header (the -vtable's `size`, reserved `flags`, and the `free` release hook) followed by -one entry per method in declaration order; a parameter lowers to a `ctx` -pointer plus the vtable. From the `kitchen_sink` fixture, whose `label` -returns a `string` and throws, `latest` returns an `Item?`, and `favorite` -returns a `Gadget`: +vtable's `size`, its `flags`, and the `free` release hook) followed by one +entry per method in declaration order; a parameter lowers to a `ctx` pointer +plus the vtable. From the `kitchen_sink` fixture, whose `label` returns a +`string` and throws, `latest` returns an `Item?`, and `favorite` returns a +`Gadget`: ```c typedef struct kitchen_sink_kitchen_ReadyListener_vtable { @@ -457,10 +473,10 @@ bool kitchen_sink_kitchen_maybe_subscribe(void* listener_ctx, const kitchen_sink ## Error domains -A module may declare one error domain: named, stable codes its throwing -callables report. Each binding generates a typed error (an exception -hierarchy, a Swift `Error` enum, a Go error type) so consumers match on the -names you declared. +An error domain is a named set of stable codes that callables report. Each +binding generates a typed error (an exception hierarchy, a Swift `Error` +enum, a Go error type) so consumers match on the names you declared. A +module may declare any number of domains in its `errors:` list. | Field | Type | Meaning | |-------|------|---------| @@ -470,42 +486,68 @@ names you declared. Each code has a required `name` (PascalCase by convention), positive `code`, and default `message`, plus an optional `doc` and optional `fields`: a structured payload with the same shape as struct fields, delivered as -properties of the raised error. +properties of the raised error. Payload fields are plain data: a field may +not reach an interface, directly or through a record or rich enum +(`ObjectInErrorPayload`), because a consumer generated before a code existed +discards its payload without decoding it. ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: kv errors: - name: KvError - codes: - - name: KeyNotFound - code: 1001 - message: key not found - fields: - - { name: key, type: string } - - { name: StoreFull, code: 1003, message: store has reached capacity } + - name: KvError + codes: + - name: KeyNotFound + code: 1001 + message: key not found + fields: + - { name: key, type: string } + - { name: StoreFull, code: 1003, message: store has reached capacity } + - name: ParseError + codes: + - { name: BadSyntax, code: 1, message: bad syntax } functions: - name: lookup params: - { name: key, type: string } return: bytes - throws: true + throws: KvError + - name: parse_key + params: + - { name: text, type: string } + return: string + throws: ParseError + - name: load_file + params: + - { name: path, type: string } + return: bytes + throws: any - name: count return: i64 ``` -Declaring a domain reserves the codes; a function, constructor, method, or -static opts in with `throws: true`. A callable without `throws` (`count` -above) has a plain signature and can't report a domain error; a failure there -is a producer bug and traps (see -[Errors and Memory](../guides/errors-and-memory.md#domain-errors-and-the-trap-channel)). -A domain is in scope for its module and every module nested in it, and -`throws: true` with no domain in scope is rejected: +A function, constructor, method, static, or callback method says how it can +fail with `throws`: + +- **`throws: {Domain}`** names the one domain it reports (`lookup` above). + The name is global, so the domain can be declared in any module. +- **`throws: any`** reports an untyped error: a message, with no code a + consumer can match on (`load_file`). Bindings surface it as the library's + base error type. A Rust producer's `Result` becomes `throws: any` + whenever `E` isn't a declared domain (an `anyhow::Error`, a + `std::io::Error`, a `String`). +- **No `throws`** (`count`) means a plain signature that can't report an + error; a failure there is a producer bug and traps (see + [Errors and Memory](../guides/errors-and-memory.md#domain-errors-and-the-trap-channel)). + +Boolean `throws: true` is gone; a document that still uses it fails to +parse with a message pointing at the new syntax. A `throws` that names +something other than an error domain is rejected: ```yaml -# rejected: ThrowsWithoutErrorDomain -version: "0.11.0" +# rejected: UnknownErrorDomain +version: "0.12.0" modules: - name: contacts functions: @@ -513,13 +555,21 @@ modules: params: - { name: id, type: i64 } return: string - throws: true + throws: ContactError ``` +**Domains are open.** Each code is its own contract entry, so a producer may +add codes to a domain without breaking a deployed binding. A binding that +receives a positive code it wasn't generated with reports it as the domain's +base error type with the code and message preserved. + Codes must be positive (`0` is success and negative codes belong to the runtime) and unique within their domain. Code names must be unique across every domain in the API, because several targets flatten them into one namespace; qualify one (`OrderNotFound`) when two domains need the same idea. +A domain name is a type name (unique with every other type), but it isn't a +value type: `type: KvError` is `ErrorDomainAsType`. `any` is reserved and +can't name a domain. ## Async, cancellable, and deprecated @@ -530,7 +580,7 @@ Constructors can't be async, and `cancellable` requires `async`: ```yaml # rejected: CancellableNotAsync -version: "0.11.0" +version: "0.12.0" modules: - name: net functions: @@ -544,7 +594,7 @@ deprecation marker. ## Modules and names Modules nest to any depth. A nested module shares the validation rules of a -top-level one, and an error domain declared on a parent serves the subtree. +top-level one. **Names are global.** Modules group declarations and namespace their C symbols, but they don't scope names: @@ -570,7 +620,7 @@ with underscores: a function `get_record` in module `app.data` is `{prefix}_app_data_Record`. ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: kv interfaces: @@ -594,7 +644,7 @@ modules: ```yaml # rejected: DuplicateFunctionName, QualifiedTypeRef -version: "0.11.0" +version: "0.12.0" modules: - name: kv interfaces: @@ -630,14 +680,16 @@ The rules in brief: - Names are identifiers (a letter or `_`, then ASCII letters, digits, and `_`) and not one of `if`, `else`, `for`, `while`, `loop`, `match`, `type`, `return`, `async`, `await`, `break`, `continue`, `fn`, `struct`, `enum`, - `mod`, `use`. Generators escape other languages' keywords themselves. + `mod`, `use` (and no error domain is named `any`). Generators escape other + languages' keywords themselves. - Type names, free-function names, and error code names are unique across the whole API; every other name is unique in its scope (see [Modules and names](#modules-and-names)). - Structs, enums, interfaces, and callback interfaces aren't empty. - Types are bare names that resolve, and appear only in the positions in the table above. -- `throws` needs a domain in scope; `cancellable` needs `async`. +- `throws` names an error domain (or is `any`); `cancellable` needs + `async`. - Codes are positive and their values unique within a domain; no free function shares a domain's name. - **No two declarations lower to the same C symbol.** Every symbol is the @@ -650,6 +702,11 @@ The rules in brief: reserve (`{prefix}_list_*`, `{prefix}_map_*`, and so on, which covers everything in a top-level module named `list`) collide too. The full symbol table is in the [C ABI contract](abi.md#symbol-names). +- **No two C slots of one callable share a name.** A parameter `name` lowers + to `name_ptr` and `name_len` (or `has_name` and `name`, or `name_ctx` and + `name_vtable`), so it collides with a parameter called `name_ptr` next to + it, and a parameter named `out_err`, `out_len`, `out_value`, `callback`, + `context`, or `cancel_token` can collide with a slot the lowering adds. A document with an unsupported `version` reports only `UnsupportedSchemaVersion`. @@ -658,12 +715,12 @@ A document with an unsupported `version` reports only | Code | Reported when | |------|---------------| -| `UnsupportedSchemaVersion` | `version` isn't `0.11.0` | +| `UnsupportedSchemaVersion` | `version` isn't `0.12.0` | | `NoModuleName` | a module has no name | | `InvalidModuleName` | a module name isn't an identifier or is reserved | | `DuplicateModuleName` | two sibling modules share a name | | `InvalidIdentifier` | a name isn't a valid identifier | -| `ReservedKeyword` | a name is a reserved word | +| `ReservedKeyword` | a name is a reserved word, or an error domain is named `any` | | `DuplicateFunctionName` | two free functions anywhere in the API share a name | | `DuplicateParamName` | two parameters of a callable share a name | | `NameCollisionWithErrorDomain` | a free function and an error domain share a name | @@ -678,6 +735,7 @@ A document with an unsupported `version` reports only | `ConstructorHasReturn` | a constructor declares `return` | | `AsyncConstructor` | a constructor is `async` | | `InterfaceInInvalidPosition` | an interface is used as a map key | +| `ObjectInErrorPayload` | an error code's field reaches an interface, directly or through a record or rich enum | | `EmptyCallbackInterface` | a callback interface has no methods | | `DuplicateCallbackMethod` | two methods of a callback interface share a name | | `InvalidCallbackMethod` | a callback method is async or cancellable, or returns an iterator | @@ -690,12 +748,14 @@ A document with an unsupported `version` reports only | `IteratorInInvalidPosition` | `iter` appears other than as an outermost return | | `AsyncIteratorReturn` | an async function returns `iter` | | `CancellableNotAsync` | a synchronous callable is `cancellable` | -| `ThrowsWithoutErrorDomain` | `throws: true` on a callable or callback method with no error domain in scope | +| `UnknownErrorDomain` | `throws` names something that isn't an error domain | +| `ErrorDomainAsType` | an error domain is used as a value type | | `ErrorDomainMissingName` | an error domain has no name | -| `DuplicateErrorCode` | two codes in a domain share a value | +| `DuplicateErrorCode` | two codes in one domain share a value | | `DuplicateErrorCodeName` | two codes anywhere in the API share a name | | `InvalidErrorCode` | a code is zero or negative | | `SymbolCollision` | two declarations lower to the same C identifier, or one falls in a reserved helper family | +| `SlotCollision` | two C slots of one callable's lowered signature share a name | ### Warnings @@ -715,15 +775,15 @@ An address book combining an enum, a record, an error domain, an interface, and a nested module that uses its parent's types: ```yaml -version: "0.11.0" +version: "0.12.0" modules: - name: contacts doc: An in-memory address book. errors: - name: ContactsError - codes: - - { name: InvalidName, code: 1, message: name must not be empty } - - { name: NotFound, code: 2, message: contact not found } + - name: ContactsError + codes: + - { name: InvalidName, code: 1, message: name must not be empty } + - { name: NotFound, code: 2, message: contact not found } enums: - name: ContactType variants: @@ -747,12 +807,12 @@ modules: - { name: email, type: "string?" } - { name: contact_type, type: ContactType } return: Contact - throws: true + throws: ContactsError - name: get params: - { name: id, type: i64 } return: Contact - throws: true + throws: ContactsError - name: list return: "[Contact]" modules: diff --git a/docs/src/reference/naming.md b/docs/src/reference/naming.md index c843cfe4..bfc35e2c 100644 --- a/docs/src/reference/naming.md +++ b/docs/src/reference/naming.md @@ -136,10 +136,15 @@ constructor's (kvstore's async `open_store` becomes `KvOpenStore` because **Errors.** Each target has a root error type named after the package and one type (or case) per error domain and code, named from the IDL with at -most one idiomatic suffix (`Error` or `Exception`). Write code names without -a suffix (`KeyNotFound`, not `KeyNotFoundError`) and let the generator add -it. Code names are global because several targets flatten them into one -namespace. +most one idiomatic suffix (`Error` or `Exception`). Every target applies one +shared rule (`weaveffi_model::errors::type_name`): convert the name to +PascalCase, strip one trailing `Exceptions`, `Exception`, `Errors`, or +`Error`, then add the target's suffix. So `KvError` stays `KvError` (or +becomes `KvException`), `KitchenErrors` becomes `KitchenError`, `Failure` +becomes `FailureError`, and a code `KEY_NOT_FOUND` becomes +`KeyNotFoundError`. Write code names without a suffix (`KeyNotFound`, not +`KeyNotFoundError`) and let the generator add it. Code names are global +because several targets flatten them into one namespace. **Reserved words.** A name that's a keyword in the target language gains a trailing `_` (`type` becomes `type_`), a rule that's stable under repetition. diff --git a/docs/src/reference/value-buffers.md b/docs/src/reference/value-buffers.md index b5708098..e87725e9 100644 --- a/docs/src/reference/value-buffers.md +++ b/docs/src/reference/value-buffers.md @@ -10,16 +10,23 @@ This page is the normative wire format every generator and the - a struct (record) - a rich enum (any variant has fields) -- `[T]`, `{K:V}` -- `T?`, except `Interface?`, which stays a nullable object pointer at the top - level of a parameter or return, and `Cb?`, an optional callback parameter - whose null vtable means none +- `{K:V}` +- `[T]`, except a list of a fixed-width number (`[i8]`, `[i16]`, `[i32]`, + `[i64]`, `[u16]`, `[u32]`, `[u64]`, `[f32]`, `[f64]`), which crosses as a + typed array (the Slice family) at the top level of a parameter, return, + async result, iterator item, or callback method +- `T?`, except an optional scalar or C-style enum (the OptDirect family, + `bool has_v, T v` at the top level), `Interface?`, which stays a nullable + object pointer, and `Cb?`, an optional callback parameter whose null + vtable means none Everything else keeps its own slot shape at the top level (see the [C ABI contract](abi.md#families-and-slots)): direct values by value, strings and bytes as `(ptr, len)`, objects as pointers. Any of them may appear -*inside* a buffer. Iterators and callback interfaces never do; validation -rejects them in buffered positions. +*inside* a buffer, and there they all use the encoding below: a record's +`i64?` field is a flag byte and a value, and its `[f64]` field is a count and +packed elements, exactly as before revision 5. Iterators and callback +interfaces never appear inside a buffer; validation rejects them there. ## Encoding @@ -95,14 +102,26 @@ exactly one strong reference, in either direction. A zero token is invalid in a non-optional position; `Interface?` inside a buffer uses the optional flag byte followed by the token. -A callback method's error payload can't carry object tokens: an error code -whose fields include an interface can't be reported from a callback, and a +An error payload never carries object tokens: validation rejects an error +code whose fields reach an interface (`ObjectInErrorPayload`), because a +consumer that doesn't know a code discards its payload undecoded, and a Rust producer decodes a callback's payload with a reader that refuses every token. That reader (`BufferReader::token_free`) is also why decoding is otherwise `unsafe` in the Rust runtime: only a reader that refuses tokens can safely decode arbitrary bytes, because no safe function can turn an arbitrary `u64` into an object reference. +## Runs + +A buffer the producer hands out (a return, an iterator element, an async +result, an error payload) is a run allocated with alignment 8, released +with `{p}_free_bytes(ptr, len)` (or, for a payload, with the error). A +buffer a consumer returns from a callback method is a run it allocates +with `{p}_alloc(len)`, which the producer adopts. A buffer a consumer +passes as a parameter is its own memory, borrowed for the call, with no +alignment requirement: values inside a buffer are packed and read +byte-wise. + ## Slots and ownership | Position | Slots | Owner | @@ -112,6 +131,7 @@ safely decode arbitrary bytes, because no safe function can turn an arbitrary | iterator element | `const uint8_t** out_item, size_t* out_len` | the consumer, per element | | async result | `const uint8_t* result_ptr, size_t result_len` | the consumer | | callback method argument | `const uint8_t* v_ptr, size_t v_len` | the producer; borrowed for the call | +| callback method return | `uint8_t** out_ptr, size_t* out_len` | a `{p}_alloc` run the producer adopts | | error payload | `payload_ptr`, `payload_len` in `{p}_error` | freed by `{p}_error_clear` or `{p}_error_free` | Object tokens inside any of these transfer their reference to whoever @@ -122,7 +142,9 @@ decodes them, including the consumer decoding a callback argument. Each binding ships a small private codec (a writer and a reader for the table above) plus one encode and one decode routine per record and rich enum, generated from the definition so field order is fixed at generation time. -Optionals, lists, and maps are handled generically. Lists of bytes and of +Optionals, lists, and maps nested in buffers each get one codec per distinct +shape, named by one canonical stem shared by every target (`list_i32`, +`opt_Item`, `map_string_list_i64`). Lists of bytes and of fixed-width numbers (every integer and float type, but not `bool`, whose bytes must be validated) encode and decode with a single copy where the platform is little-endian. The `codec` sample's conformance lane checks every diff --git a/docs/src/roadmap.md b/docs/src/roadmap.md index 591f3a8c..661e932e 100644 --- a/docs/src/roadmap.md +++ b/docs/src/roadmap.md @@ -1,15 +1,13 @@ # Roadmap -WeaveFFI is in active `0.x` development. Schema 0.11 and ABI revision 4 made -names global, replaced the per-module checksums with per-declaration contract -tables, gave callback methods rich returns, typed `throws`, optional callback -parameters, and a size-checked vtable header, made `{prefix}_alloc` the one -allocator contract on every target, moved the codec conformance lanes onto -shared test vectors, and taught the CLI to read a Rust producer's API from -its built library. This page lists what comes next. Items are **planned** -(the design is settled) or **exploring** (wanted, with open design -questions). Nothing carries a date; the -[changelog](https://github.com/weavefoundry/weaveffi/blob/main/CHANGELOG.md) +WeaveFFI is in active `0.x` development. Schema 0.12 and ABI revision 5 made +error domains and callback interfaces open (every error code and callback +method has its own contract entry), let callables throw any domain or an +untyped error, passed optional scalars and numeric lists directly, and gave +Rust producers generated error messages, custom types, and `usize`. This +page lists what comes next. Items are **planned** (the design is settled) or +**exploring** (wanted, with open design questions). Nothing carries a date; +the [changelog](https://github.com/weavefoundry/weaveffi/blob/main/CHANGELOG.md) records what shipped. ## Callback interfaces @@ -19,19 +17,22 @@ records what shipped. A callback method that returns a future on the consumer side needs a completion flowing the other way and a cancellation story when the producer drops the future. The vtable shape is simple (a completion function and -context per async method); the hard part is one producer working the same way +context per async method, which revision 5's growable vtables can add +without a new revision); the hard part is one producer working the same way whether the consumer runtime is an event loop, a thread pool, or the single-threaded Wasm host. -### Off-thread callbacks in Dart (exploring) +### Off-thread value callbacks in Dart (exploring) Dart can't run a value-returning callback method synchronously on a thread -that isn't a Dart isolate thread, so a producer that calls one from its own -worker thread aborts the process today (void methods are forwarded to the -isolate safely). Pure `dart:ffi` can't route that return, so the fix is -either a small native shim that hops to the isolate and waits, or a -per-vtable thread-affinity hint that lets the producer refuse the call with -`-4` instead. See the [Dart page](generators/dart.md#known-limitations). +that isn't its isolate's, so the Dart binding marks its vtables +thread-affine, and a producer that calls such a method from one of its own +threads gets `-4` (`callback called off its thread`) instead of a result. +That's safe (it used to abort the process), but it means a Dart callback +that returns a value only works when the producer calls it on the thread +that passed it in. Lifting the restriction needs a small native shim that +hops to the isolate and waits for the result. See the +[Dart page](generators/dart.md#threading). ### Synchronous calls that wait on Node.js callbacks (exploring) @@ -49,15 +50,8 @@ remove the heuristic. See the [Node.js page](generators/node.md#threading). An IDL API is one document. Large APIs want to split by module, and a monorepo wants to reference another package's types. The plan is an `imports:` list resolved at parse time, with bare type names still unique -across the merged API, and `diff`, `validate`, and contract tables covering -every imported file. - -### Generic and trait-object interfaces (exploring) - -WeaveFFI has a fixed set of generic shapes (`T?`, `[T]`, `{K:V}`, `iter`). -Under discussion are trait-object interfaces (one declared method set with -several producer implementations behind `Arc`) and, less likely, -parameterized interfaces monomorphized per instantiation. +across the merged API, and `validate`, `generate --check`, and contract +tables covering every imported file. ### Duration and timestamp primitives (exploring) @@ -65,35 +59,55 @@ Producers pass time as `i64` with a documented unit. `duration` and `timestamp` primitives mapped to each language's types would remove the ambiguity; the open question is the representation. -### Custom types (exploring) +### Consumer-side custom types (exploring) -A producer type that crosses as a builtin (a `Uuid` as `string`, a `Url` as -`string`, a fixed-point amount as `i64`) has to be converted by hand on both -sides today. A declared custom type with per-language conversion hooks would -let each binding expose the language's own type. +A Rust producer can declare a custom type (`#[weaveffi::custom]`) that +crosses as a builtin, such as a `Uuid` as a `string`, but every binding +still exposes the builtin. Per-language conversion hooks in `weaveffi.toml` +would let a binding expose the language's own type (`java.util.UUID`, +`Foundation.UUID`, `uuid.UUID`) instead; the open questions are how a hook +is declared and how a codec failure on the consumer side is reported. + +### Generic and trait-object interfaces (exploring) + +WeaveFFI has a fixed set of generic shapes (`T?`, `[T]`, `{K:V}`, `iter`). +Under discussion are trait-object interfaces (one declared method set with +several producer implementations behind `Arc`) and, less likely, +parameterized interfaces monomorphized per instantiation. ## Targets and runtime -### Kotlin Multiplatform (exploring) +### Kotlin Multiplatform and the JVM's FFM API (exploring) + +The Kotlin target reaches Android and the JVM through a generated JNI shim. +A Multiplatform flavor using Kotlin/Native `cinterop` for iOS and desktop is +the natural next step, and on JVMs with the Foreign Function and Memory API +(Java 22 and later) the shim could go away entirely. -The Kotlin target reaches Android and the JVM through JNI. A Multiplatform -flavor using Kotlin/Native `cinterop` for iOS and desktop is the natural next -step. +### Wasm threads and JSPI (exploring) -### Wasm threads (exploring) +On `wasm32-unknown-unknown` futures are polled inline and callbacks fire +only while a call is on the stack. A spawner that schedules on the JS event +loop, JavaScript Promise Integration (so a pending future can suspend the +module until a promise settles), or shared-memory builds with Web Workers +would lift those limits. -On `wasm32-unknown-unknown` futures run inline and callbacks fire only while -a call is on the stack. A spawner that schedules on the JS event loop, or -shared-memory builds with Web Workers, would lift both limits and let -Emscripten mode support async functions and callback interfaces. +### Replacing the Node.js addon transport (exploring) + +The Node.js target reaches the C ABI through a generated N-API addon, which +`weaveffi build` prebuilds per desktop platform and npm compiles as a +fallback (always, on Windows). A transport that needs no compiled glue would +remove that build step and the Windows gap; the open question is which one +offers the threading guarantees callbacks and async completions need. ### Zero-copy blittable records (exploring) Every record crosses as a serialized value buffer. A record of fixed-size scalars could instead cross as a `#[repr(C)]` struct passed by pointer, which -matters for hot paths that move many small records. The open questions are -how a record opts in, and how the contract hash and every target's layout -checks keep the two sides agreeing on the layout. +matters for hot paths that move many small records. (Revision 5 already +passes numeric lists as typed arrays.) The open questions are how a record +opts in, and how the contract hash and every target's layout checks keep the +two sides agreeing on the layout. ### Poll-based async (exploring) @@ -124,12 +138,15 @@ prebuilding the Node.js addon and JNI shim for `windows-x64`, which ## Toward 1.0 1.0 means the surfaces in [What 1.0 will cover](stability.md#what-10-will-cover) -stop changing without a major release. It needs: - -- ABI revision 4 stable for several releases, so 1.0 doesn't need - revision 5; -- every target passing the full conformance matrix on Linux, macOS, and - Windows; +stop changing without a major release. ABI revision 5 was designed so that +the ABI can keep growing after 1.0 without a new revision: new declarations, +callback methods, and error codes are additive, and error domains and +callback interfaces are open. 1.0 needs: + +- every Tier 1 target (C, C++, Swift, Kotlin, Python, Node.js, and .NET) + passing the full conformance matrix on Linux, macOS, and Windows, and the + Tier 2 targets (Go, Ruby, Dart, and WebAssembly) on at least Linux and + macOS (see [Target tiers](stability.md#target-tiers)); - multi-file IDL and the deprecation policy in place; - a schema migration tool and more than one accepted schema version; - the known limitations on each generator page resolved or documented as diff --git a/docs/src/samples.md b/docs/src/samples.md index 24e16a8b..c3bcf8d3 100644 --- a/docs/src/samples.md +++ b/docs/src/samples.md @@ -20,11 +20,22 @@ metadata. ## calculator The getting-started example, small enough to read in a minute: a -`calculator` module with `add` (wrapping on overflow), `divide`, which -throws the one-code `CalcError` domain on a zero divisor, and `greet`, a -string in and a string out. The `c-producer-exports` lane also implements -the calculator's generated header by hand in C (`conformance/c/producer.c`) -and checks that every required runtime and contract symbol is exported. +`calculator` module with + +- `add` (wrapping on overflow) and `greet`, a string in and a string out; +- `divide`, which throws the `CalcError` domain on a zero divisor; +- `parse`, which throws a second domain, `ParseError`, whose `NotANumber` + code carries the input as a field and renders its message from a + template (`not a number: 4x`); both domains use code `1`, so a binding + must interpret a code by the callable's domain; +- `sqrt`, a `throws: any` function returning `Result`, which + fails with code `-1` and the message `cannot take the square root of -4`; +- `mean` (`[f64]` in, `f64?` out) and `running_total` (`[i32]` in and out), + which cross as typed arrays and an optional scalar. + +The `c-producer-exports` lane also implements the calculator's generated +header by hand in C (`conformance/c/producer.c`) and checks that every +required runtime and contract symbol is exported. ## codec @@ -51,9 +62,16 @@ so each consumer also builds a few vectors from literals and checks those, and spot-checks decoded fields. The `echo_*` functions return their argument through the direct ABI families (scalars by value, strings and bytes as `(ptr, len)` runs, the C-style enum as an `int32_t`), and each consumer pushes -every primitive vector through the matching echo. A `Token` interface with -`sum_holder`, `primary_of`, and `same_primary` checks object identity and -reference counting through buffers. +every primitive vector through the matching echo. The revision 5 families +have echoes of their own: `echo_opt_i32`, `echo_opt_f64`, `echo_opt_bool`, +and `echo_opt_color` (optional scalars, including `-0.0`, NaN, and absent), +`echo_f64s`, `echo_i32s`, and `echo_u64s` (typed arrays, bit-exact at the +extremes), `echo_usize` (a Rust `usize` as `u64`), `echo_char` (a one-scalar +string; `"ab"` fails with `-3`), `echo_hex` (a custom type crossing as a +lowercase hex string; `"xyz"` fails with `-3`), and `chunks`, an iterator of +typed arrays. A `Token` interface with `sum_holder`, `primary_of`, and +`same_primary` checks object identity and reference counting through +buffers. ## kvstore @@ -65,21 +83,26 @@ concrete assertions. Time is a logical clock per store (`now`, advanced by | Feature | Where | |---|---| | Interface with constructors, methods, and statics | `Store` (`open`, `new`; `put`, `get`, ...; `open_many`, `default_capacity`, ...) | -| Typed errors with payload fields | `KvError`: `KeyNotFound { key }`, `Expired { key, expired_at }`, `StoreFull { capacity }`, `InvalidPath`, `Rejected { key, reason }` | +| Typed errors with payload fields and generated messages | `KvError`: `KeyNotFound { key }`, `Expired { key, expired_at }`, `StoreFull { capacity }`, `InvalidPath`, `Rejected { key, reason }`, `CallbackFailed { message }` | +| An untyped (`throws: any`) error | `import_lines` (a `Result`) | +| Optional scalars and typed arrays | `put`'s TTL (an `i64?` parameter), `expires_at` (an `i64?` return), `value_sizes` (a `[u64]` return), `expirations` (an iterator of `i64?`), `version_of` and `versions` (async `u32?` and `[u32]` results), and `Scorer::scores` (a `[u64]` parameter and `[f64]` return) | +| `usize` | `count` and `listener_count` (`u64` in the IDL) | | Records, optionals, lists, maps | `Entry` (bytes, an optional TTL, tags, metadata), `StoreInfo`, `Stats` (a map keyed by a C-style enum) | | C-style and rich enums | `EntryKind`; `Change` (`Put`, `Removed`, `Cleared`) | | Objects in every position | parameters, returns, `Store?` both ways (`larger`), lists (`open_many`, `total_count`), map values (`by_label`), record fields (`StoreInfo`), iterator elements (`partition`), an async result (`open_store`), and a callback's parameter and return (`Policy::route`) | -| Callback interfaces | `Listener` (retained by `subscribe`; void and direct returns), `Policy` (a record return, a throwing method whose typed error reaches the `put` caller, an object parameter and return), `Loader` (string, bytes, and optional-object returns; passed as an optional callback) | +| Callback interfaces | `Listener` (retained by `subscribe`; void and direct returns), `Policy` (`ttl_for`, an optional scalar in and out; `admit`, a record return whose typed `KvError` reaches the `put` caller; `route`, an object parameter and return), `Loader` (string, bytes, and optional-object returns; passed as an optional callback), `Scorer` (typed arrays in and out, used by `rank`) | | Callbacks from a producer thread | `compact` notifies listeners from the thread it runs on | | Lazy iterators | `keys` (strings, throwing), `entries` (records), `partition` (objects) | | Async and cancellation | `open_store` (an async free function returning an object), `compact` (cancellable; its background pause stops cooperatively, shown by `Store::active_jobs`), `get_many` (an async list), `summarize_all` | -| Nested modules | `kv.stats` uses the parent's `Store` and reports the parent's `KvError` | +| Nested modules | `kv.stats` uses the parent's `Store` and throws the parent's `KvError` | | Sibling roots | `report` shares the `Entry` record and has its own `ReportError` | | Deprecation | `Store::size` (use `count`) | A callback's failure surfaces according to its role: a `Listener` that fails -is unsubscribed while the store operation succeeds; a `Policy` or `Loader` -failure fails the call with the callback's code, message, and payload, so a -typed `KvError` a consumer raised reaches the original caller typed. The -store never holds a lock while a callback runs, so callbacks may call back -into it. +is unsubscribed while the store operation succeeds; a `Policy`, `Loader`, or +`Scorer` failure fails the call. A typed `KvError` a consumer raised (with +its fields) reaches the original caller typed, its message rendered from the +fields, and any other consumer failure becomes `KvError::CallbackFailed` +carrying the consumer's message, through the domain's +`From` implementation. The store never holds a lock while a +callback runs, so callbacks may call back into it. diff --git a/docs/src/stability.md b/docs/src/stability.md index 9a3fc167..d8978cc9 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -8,15 +8,19 @@ migrate to the current release. ## Three version numbers -- **The crate version** (`0.24.x`) is shared by every published crate - (`weaveffi`, `weaveffi-macros`, `weaveffi-model`, `weaveffi-cli`) and by - the CLI. Releases are cut from Conventional Commits by release-plz. -- **The IDL schema version** (`0.11.0`) is the `version:` an IDL document +- **The crate version** (`0.25.x` until the next release) is shared by + every published crate (`weaveffi`, `weaveffi-macros`, `weaveffi-model`, + `weaveffi-cli`) and by the CLI. Releases are cut from Conventional Commits + by release-plz, which bumps it. +- **The IDL schema version** (`0.12.0`) is the `version:` an IDL document declares. Pre-1.0, only the current version is accepted; a document declaring any other is rejected with `UnsupportedSchemaVersion`. -- **The C ABI revision** (`4`) is what `{prefix}_abi_version()` returns. It +- **The C ABI revision** (`5`) is what `{prefix}_abi_version()` returns. It changes only when the runtime surface or a calling convention changes - incompatibly, independently of the other two. + incompatibly, independently of the other two. Revision 5 is designed to + grow additively: new declarations, new callback methods, and new error + codes each get their own contract entry, so adding one never needs a + revision 6 and never breaks a deployed binding. ## What 1.0 will cover @@ -32,6 +36,19 @@ After 1.0.0, these surfaces change incompatibly only in a major release: - **The C ABI revision** and the [contract](reference/abi.md) it names. - **The public Rust API** of the published crates. +## Target tiers + +Every target passes the same conformance gate (real consumers against every +sample, with every leak counter at zero) before a release ships, but they +come in two tiers: + +- **Tier 1**: C, C++, Swift, Kotlin, Python, Node.js, and .NET. A change to + the ABI or the IDL lands in every Tier 1 target at once, and their + generated APIs are what 1.0 will freeze first. +- **Tier 2**: Go, Ruby, Dart, and WebAssembly. These are fully supported and + conformance-tested, but may lag Tier 1 on new ABI work and may change + shape more freely before 1.0. + Deprecation will precede removal: a deprecated feature warns for at least one minor release (the CLI prints a diagnostic, and generators emit each language's deprecation marker from the IDL's `deprecated:` field) and is @@ -49,25 +66,276 @@ the definition that triggers it. ## CI workflow Pin the CLI version and commit the generated output, so an upgrade is an -explicit, reviewable change. Then gate CI on `weaveffi diff --check`, which -regenerates in memory and compares with the committed directory without +explicit, reviewable change. Then gate CI on `weaveffi generate --check`, +which renders in memory and compares with the committed directory without writing anything: ```yaml - name: Bindings are up to date run: | - cargo install weaveffi-cli --locked --version =0.24.0 + cargo install weaveffi-cli --locked --version =0.25.0 weaveffi validate --warn - weaveffi diff --check + weaveffi generate --check ``` -`diff --check` exits `0` when the output matches, `2` when files would -change, and `3` when files would be added or removed. For a Rust producer, +`generate --check` lists the files that would change and exits `0` when +the output matches and `1` otherwise (`generate --diff` prints the diff). +For a Rust producer, both commands build the crate first (see [Library Mode](guides/extract.md)), so the job needs the Rust toolchain; pass `--library ` to reuse a library an earlier step built. -## Migrating to schema 0.11 and ABI 4 +## Migrating from 0.25 to the next release (schema 0.12, ABI 5) + +The next release moves to schema `0.12.0` and C ABI revision 5. It makes +error domains and callback interfaces open, lets a callable throw any +domain or an untyped error, passes optional scalars and numeric lists +directly, and reworks the Rust producer's error and callback API around +generated `Display` impls and ordinary `Result`s. Every consumer must be +regenerated, and every hand-written producer or consumer updated. There are +no compatibility shims. + +### C ABI (revision 4 to 5) + +- `{prefix}_abi_version()` returns `5`. +- **Enums are `int32_t`.** C-style enums, rich-enum tags, and error-domain + code types are `typedef int32_t {p}_{path}_{E};` with their constants in + an anonymous `enum`, never `typedef enum`, so their size no longer depends + on the compiler. +- **Error messages are length-delimited.** The error struct's `const char* + message` is replaced by `const uint8_t* message_ptr; size_t message_len;` + (UTF-8, not NUL-terminated; an empty message is null with length `0`, even + on failure), and `{prefix}_error_set(err, code, message_ptr, message_len)` + takes a run instead of a C string. The struct is now five fields. +- **Optional scalars cross directly (OptDirect).** A `T?` of an integer, + float, `bool`, or C-style enum is no longer a value buffer at a call + boundary: a parameter is `bool has_x, T x`; a return is a C `bool` + (present) plus a trailing `T* out_value`; an async result is `bool + has_result, T result`; an iterator item is `bool* out_has_item, T* + out_item`; callback parameters and returns follow the same shapes. +- **Numeric lists cross as typed arrays (Slice).** `[i8]`, `[i16]`, + `[i32]`, `[i64]`, `[u16]`, `[u32]`, `[u64]`, `[f32]`, and `[f64]` are no + longer value buffers at a call boundary: a parameter is `const T* x_ptr, + size_t x_len` (an element count, aligned for `T`); a return is a C `T*` + plus `size_t* out_len`, released with `{prefix}_free_bytes((uint8_t*)ptr, + len * sizeof(T))`; a callback returns one allocated with + `{prefix}_alloc(len * sizeof(T))`. Inside value buffers both families keep + their old encoding. +- **Runs are 8-aligned.** Every run a producer hands out, and every + `{prefix}_alloc` run, is allocated with alignment 8. A hand-written + producer must allocate (and free) accordingly; `malloc` already does. +- **Contract tables changed.** Parameter and field names are no longer part + of a canonical signature, `throws` renders as ` throws {Domain}` or + ` throws any`, and every error code and every callback method has an entry + of its own, so every hash changed. Regenerate, and have a hand-written + producer return the new header's `{PREFIX}_{MODULE}_CONTRACT`. +- **Thread-affine vtables.** Bit 0 of a vtable's `flags` is + `{PREFIX}_VTABLE_THREAD_AFFINE`: a producer must fail a value-returning + method called off the adopting thread with `-4` instead of calling it. + Only Dart sets it. +- **Callback failures.** A consumer reports a failure of a `throws: any` + method as `-1` with a message, and one of a method that declares no errors + as `-4` (any non-zero code still works); a declared domain code with its + payload stays typed. +- An iterator's `_next` checks every out slot for null before it pulls an + element, and a misaligned or null-with-length typed array is `-3`. +- The C buffer header names composite codecs by their element types alone + (`{p}_opt_Item`, not `{p}_opt_kv_Item`), and its string reader rejects + malformed UTF-8. + +### IDL (schema 0.11 to 0.12) + +- Set `version: "0.12.0"`. +- **`throws` names what a callable throws.** Replace `throws: true` with + `throws: {Domain}` (any domain in the API; names are global) or + `throws: any` for an untyped error. A boolean `throws` fails to parse + with a message pointing at the new syntax. Callback methods use the same + key. +- **`errors:` is a list.** A module may declare several domains, each + `{ name, codes }`: + + ```yaml + errors: + - name: KvError + codes: + - { name: KeyNotFound, code: 1001, message: key not found } + ``` + + There's no "domain in scope" anymore, so `ThrowsWithoutErrorDomain` is + gone; a `throws` naming something that isn't a domain is + `UnknownErrorDomain`, a domain used as a value type is + `ErrorDomainAsType`, and `any` can't name a domain (`ReservedKeyword`). + Code values must be unique within their domain (`DuplicateErrorCode` + names the domain); code names stay unique across the API. +- Two C slots of one function that would share a name (a parameter `name` + next to one called `name_ptr`, or a parameter named `out_err`) are a new + `SlotCollision` error. +- **TOML IDL is gone.** IDL documents are YAML or JSON; convert a `.toml` + IDL with any TOML-to-YAML tool (or `weaveffi extract -f yaml` from a Rust + producer). TOML remains the format of `weaveffi.toml`. + +### Rust producers + +- **Error domains generate `Display`.** `#[weaveffi::error]` now generates + `Display` and `std::error::Error` (so the enum must derive `Debug`); delete + your hand-written `impl Display`. A variant's message is its + `#[weaveffi(message = "...")]` template (with `{field}` interpolation), else + its doc comment's first line. To keep your own `Display` (from thiserror, + say), write `#[weaveffi::error(no_display)]`. +- **Several domains per module, any error type per function.** A module may + declare several `#[weaveffi::error]` enums. A `Result` throws `E` + when `E` is a domain of the module tree, and otherwise throws `any`, + requiring only `E: Display` (`String`, `std::io::Error`, `anyhow::Error`, + `Box`). A `Result` no longer needs a domain in scope, and the + `weaveffi::ErrorReport` trait is gone. +- **Callback methods return `Result` with `E: From`.** + `#[weaveffi::throws]` is gone: a method whose `E` is a domain throws that + domain (the consumer's typed codes arrive as its variants), and any other + `E` (`ForeignError` included) throws `any`. Wrapper enums that combined a + domain error with a `ForeignError` (the old `kvstore` sample's + `StoreError`) are unnecessary: implement `From` for the + domain and use `?`. A typed callback error's message is now rendered by the + domain's `Display` from its fields, not taken from the consumer. +- `ForeignError` as an exported function's error type is `throws: any`, so a + propagated consumer failure reaches the caller as `-1` with its message + (it used to keep `-4`); propagate through a domain to keep it typed. +- `usize` and `isize` are supported (they cross as `u64` and `i64`, checked + on the way in), and so is `char` (a one-scalar `string`). +- `#[weaveffi::custom(repr = R, lift = f, lower = g)] pub type Name = T;` + declares a custom type, and `#[weaveffi::skip]` leaves a `pub fn` out of + an interface. +- Marker attributes must be written `#[weaveffi::...]`, and one outside a + `#[weaveffi::module]` is a compile error. A crate whose modules use the + macro but never call `weaveffi::export_runtime!()` fails to compile. +- **Tokio is the default executor.** The `weaveffi` crate's `tokio` feature + is on by default; the hand-rolled worker pool (and + `weaveffi::abi::default_pool_size`) is gone. With `default-features = + false`, each async call runs on a thread of its own. +- The crate needs no `crate-type`: the CLI builds the `cdylib` itself. +- Runtime API (`weaveffi::abi`), for code that calls it directly: `CEnum` is + replaced by `Scalar`; `ErrorReport`, `lift_enum`, `lift_string_param`, + `callback_ret_enum`, `callback_ret_string`, `str_slots`, and + `FfiError::from_report` are gone; `FfiError` has `message_ptr` and + `message_len` fields instead of `message`; `ErrorDomain` (with a + `Display` supertrait) is the one error trait; `callback_status` takes any + `E: From`. + +### CLI and configuration + +- **`weaveffi diff` is gone.** Use `weaveffi generate --check` (lists what + would change, exits `1` if anything would) or `generate --diff` (prints a + unified diff). +- **`weaveffi schema-version` is gone.** Use `weaveffi schema --version`. +- **`--release` and `--debug` are gone.** Every command that builds a + producer takes `--profile ` (a Cargo profile): `dev` by default for + `generate`, `dev`, `validate`, and `extract`, and `[build] profile`, else + `release`, for `build` and `package`. `[build] release = true` becomes + `[build] profile = "release"` (the old key is an unknown-key error). +- Producer builds run `cargo rustc --lib --crate-type cdylib`, so a crate + needs no `crate-type`, and the `staticlib` fallback is gone. +- `weaveffi init` writes an active `targets = ["c", "python"]` line. +- `build` and `package` warn and skip an artifact whose external tool is + missing, list the skips at the end, and exit `1` only with the new + `--strict` flag (CI should pass it). +- `weaveffi dev` accepts `--library` to reuse an existing build. +- `[generators.wasm] emscripten` is gone with the Emscripten mode it + selected; `[generators.go] package` names the Go package. +- `weaveffi extract -f toml` is gone (`yaml` or `json`). +- The `weaveffi-cli` library's public surface is `project`, `config`, + `targets` (the `Target` trait and its registry), `codegen` (the + `Orchestrator` and `OutputFile`), and `package`; everything else is + private. + +### Generated bindings + +Regenerate every target. Beyond the ABI changes: + +- **Domain type names are no longer doubled.** Every target names a domain + with one shared rule that strips a trailing `Error`, `Errors`, or + `Exception` before adding its suffix: `KitchenErrors` becomes + `KitchenError` (or `KitchenException`), not `KitchenErrorsError`. + `KvError` is unchanged. +- **Domains are open.** A positive code a binding wasn't generated with + surfaces as the domain's base error, with its code and message. +- `throws: any` callables raise the library's root error type (Swift's + `{Module}RuntimeError`) with the producer's message. +- A non-zero code from a callable that declares no errors is a trap; see + [the trap policy](guides/errors-and-memory.md#the-trap-policy) for each + target's error type. +- Optional scalars and numeric lists keep their surface types in every + language (an optional integer, a list or array of doubles); only the + transport changed. +- Every callback method a Rust producer declares now throws (`any` or a + domain), so every consumer implementation may raise. A failure that isn't + a code of the method's own domain is reported as `-1` with its message, or + as `-4` from a method that declares no errors. +- Load-time check failures are catchable in every language (see + [Load-time checks](guides/errors-and-memory.md#load-time-checks)); Swift + and Go gain a check function in place of a trap or an `init()` panic. +- The C header declares enums and error codes as `int32_t` typedefs (see + above). + +Per target, the names to update consumer code against: + +- **C++**: one `Error` root (a `std::runtime_error` with `code()`) under + every domain class, `LoadError`, `InternalError`, and `Cancelled`; + `check_library()` runs the load check; `iter` returns a move-only + `Range` (a C++20 input range). +- **Swift**: `{Module}Library.check()` throws a `{Module}Library.LoadError` + (calls made without a successful check still `fatalError`); domain enums + gain an `unknown(code:message:)` case; `{Module}RuntimeError` carries + untyped and runtime codes; `iter` returns `NativeSequence`. +- **Kotlin**: `NativeLibrary.load()` runs the load check and throws an + `UnsatisfiedLinkError`; module functions are `@JvmStatic`, and throwing + functions and callback methods carry `@Throws`; record fields are + lowerCamelCase; a failed non-throwing call throws `NativeBugException`. +- **Python**: a failed load raises `LibraryLoadError(ImportError)`; each + code's exception is `{Code}Error` (`KeyNotFoundError`, aliased as + `KvError.KeyNotFound`); records are frozen, slotted dataclasses; a failed + non-throwing call raises `InternalError`. +- **.NET**: `{Name}Library.Check()` runs the load check, which throws + `NativeLoadException`; a failed non-throwing call throws + `NativeBugException`. +- **Node.js and WebAssembly**: the leak counters moved to a `./debug` + subpath export (`debugLive`); iterators are typed `NativeIterator`. + WebAssembly poisons the instance after a trap: every later call fails + with code `-2`. +- **Go**: `Check() error` replaces the `init()` panic (a call made after a + failed check panics with the same error); a throwing `iter` is an + `iter.Seq2[T, error]`; an unknown code is `Unknown{Domain}`. +- **Ruby**: records are `Data.define` classes; the `Native` and `Bridge` + modules are private; a failed load raises `{Module}::LoadError`, a + `::LoadError`; a failed non-throwing call raises `NativeBugError`. IDL + modules are flattened into the gem's module. +- **Dart**: `bytes` is `Uint8List`; the package is one library split into + parts under `lib/src/`; a failed load throws `NativeLibraryException`; a + failed non-throwing call throws `NativeError`; value-returning callback + methods are thread-affine (see [Dart threading](generators/dart.md#threading)). + +Each [language page](generators/README.md) documents its full surface and +its [tier](#target-tiers). + +### Samples + +- `calculator` gains a second domain (`ParseError`, thrown by `parse`), a + `throws: any` function (`sqrt`), an optional-scalar return (`mean`), and + numeric lists in and out (`running_total`). +- `kvstore`'s `KvError` gains `CallbackFailed`, its `count` and + `listener_count` return `u64` (Rust `usize`), `Store.put`'s TTL is an + optional scalar, `Policy` gains a first method `ttl_for`, every callback + method throws (`any` or `KvError`), and new methods cover optional and + typed-array returns, iterators, and async results (`expires_at`, + `value_sizes`, `expirations`, `version_of`, `versions`), a `Scorer` + callback (`rank`), and a `throws: any` method (`import_lines`). +- `codec` adds `echo_opt_*`, `echo_f64s`, `echo_i32s`, `echo_u64s`, + `echo_usize`, `echo_char`, `echo_hex` (a custom type), and `chunks` (an + iterator of typed arrays). + +## Migrating from 0.24 to 0.25 (schema 0.11, ABI 4) + +These notes describe the 0.25 release. Where the section above changes +something again (`throws: true`, `#[weaveffi::throws]`, `weaveffi diff`, +`--release`, the worker-pool executor, TOML IDL), the section above wins. This release makes names global, replaces the per-module checksums with contract tables, lets callback interfaces return any value and report typed @@ -236,28 +504,8 @@ value-buffer oracle, rebuilt around shared test vectors), and `kvstore` `kvstore` and removed, and every language's conformance consumers follow suit: three lanes per language. See [Samples](samples.md). -## Migrating to schema 0.10 and ABI 3 - -WeaveFFI 0.24.0 shipped schema 0.10 and ABI revision 3. Coming from an -older release, apply these changes first, then the ones above: - -- Every symbol, type, constant, and macro carries the library's prefix - (`{prefix}_error`, `{prefix}_free_bytes`, `{PREFIX}_API`, - `{prefix}_kv_Store_open`), the header is `{library}.h`, and generated - packages and loaders are named after the library's identity, never after - WeaveFFI. `[global] c_prefix` and every per-target prefix key are gone; an - IDL sets `[package] c_prefix` or `library`, and a Rust producer's prefix is - its crate's library name. -- Strings cross as UTF-8 `(ptr, len)` runs everywhere, and returned strings - are freed with `{prefix}_free_bytes`. -- Async launchers drop the `_async` suffix, cancel tokens are reference - counted, and runtime code `-5` means cancelled. -- Map keys can't be floats, and `cancellable: true` on a synchronous - callable is an error. -- The runtime is exported with `weaveffi::export_runtime!()`, and a - `#[weaveffi::error]` enum must implement `Display`. -- The `[project]` table and `weaveffi init` are new, and generation rewrites - only changed files and deletes stale ones. +## Older releases The [changelog](https://github.com/weavefoundry/weaveffi/blob/main/CHANGELOG.md) -lists every release. +lists every release; migration notes for releases before 0.25 are in this +page's history. diff --git a/samples/calculator/src/lib.rs b/samples/calculator/src/lib.rs index 8f5cce99..8ca94f8e 100644 --- a/samples/calculator/src/lib.rs +++ b/samples/calculator/src/lib.rs @@ -1,8 +1,11 @@ //! Calculator sample cdylib: the smallest useful WeaveFFI producer. //! -//! Three plain, safe Rust functions: `add`, `divide` (which fails with the -//! typed `CalcError` domain on a zero divisor), and `greet` (a string in and -//! a string out). The `#[weaveffi::module]` attribute generates the +//! Plain, safe Rust functions: `add`, `divide` (which fails with the typed +//! `CalcError` domain on a zero divisor), `greet` (a string in and a string +//! out), `parse` (a second domain, `ParseError`, whose message names the +//! input), `sqrt` (an untyped `throws any` error), `mean` (a numeric list in, +//! an optional number out), and `running_total` (a numeric list in and +//! out). The `#[weaveffi::module]` attribute generates the //! `#[no_mangle] extern "C"` thunks that the stable C ABI (and every //! generated binding) calls, so this file contains no `unsafe` glue. //! @@ -19,12 +22,6 @@ pub mod calculator { DivisionByZero = 1, } - impl std::fmt::Display for CalcError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("division by zero") - } - } - /// Add two integers, wrapping on overflow. #[weaveffi::export] pub fn add(a: i32, b: i32) -> i32 { @@ -46,6 +43,61 @@ pub mod calculator { pub fn greet(name: String) -> String { format!("Hello, {name}!") } + + /// The calculator's parsing errors: a second domain in the module. + #[weaveffi::error] + #[derive(Debug)] + #[repr(i32)] + pub enum ParseError { + /// not a number + #[weaveffi(message = "not a number: {text}")] + NotANumber { + /// The text that didn't parse. + text: String, + } = 1, + } + + /// Parse a decimal integer (surrounding whitespace allowed). Fails with + /// `NotANumber` carrying the input. + #[weaveffi::export] + pub fn parse(text: &str) -> Result { + text.trim().parse().map_err(|_| ParseError::NotANumber { + text: text.to_string(), + }) + } + + /// The square root of `x`. Fails with an untyped error (`throws any`) + /// whose message is `"cannot take the square root of {x}"` for a + /// negative `x`. + #[weaveffi::export] + pub fn sqrt(x: f64) -> Result { + if x < 0.0 { + return Err(format!("cannot take the square root of {x}")); + } + Ok(x.sqrt()) + } + + /// The arithmetic mean of `values`, or none for an empty list. + #[weaveffi::export] + pub fn mean(values: &[f64]) -> Option { + if values.is_empty() { + return None; + } + Some(values.iter().sum::() / values.len() as f64) + } + + /// The running totals of `values` (wrapping on overflow): + /// `[1, 2, 3]` gives `[1, 3, 6]`. + #[weaveffi::export] + pub fn running_total(values: Vec) -> Vec { + values + .iter() + .scan(0i32, |total, v| { + *total = total.wrapping_add(*v); + Some(*total) + }) + .collect() + } } weaveffi::export_runtime!(); @@ -111,6 +163,60 @@ mod tests { let mut len = 0usize; let table = unsafe { calculator_calculator_contract(&mut len) }; assert!(!table.is_null()); - assert_eq!(len, 4, "three functions and the error domain"); + assert_eq!( + len, 11, + "seven functions, and two error domains with one code each" + ); + } + + #[test] + fn parse_reports_its_own_domain() { + let mut err = FfiError::default(); + let ok = " 42 "; + assert_eq!( + unsafe { calculator_calculator_parse(ok.as_ptr(), ok.len(), &mut err) }, + 42 + ); + let bad = "4x"; + assert_eq!( + unsafe { calculator_calculator_parse(bad.as_ptr(), bad.len(), &mut err) }, + 0 + ); + assert_eq!(err.code, 1); + assert_eq!(message(&err), "not a number: 4x"); + assert_eq!( + unsafe { abi::decode_value::(err.payload()) }.unwrap(), + "4x" + ); + unsafe { abi::error_clear(&mut err) }; + } + + #[test] + fn sqrt_fails_untyped() { + let mut err = FfiError::default(); + assert_eq!(unsafe { calculator_calculator_sqrt(9.0, &mut err) }, 3.0); + assert_eq!(unsafe { calculator_calculator_sqrt(-4.0, &mut err) }, 0.0); + assert_eq!(err.code, abi::GENERIC_ERROR_CODE); + assert_eq!(message(&err), "cannot take the square root of -4"); + unsafe { abi::error_clear(&mut err) }; + } + + #[test] + fn lists_cross_as_typed_arrays() { + let mut err = FfiError::default(); + let mut mean = 0.0f64; + let xs = [1.0, 2.0, 6.0]; + assert!(unsafe { calculator_calculator_mean(xs.as_ptr(), 3, &mut mean, &mut err) }); + assert_eq!(mean, 3.0); + assert!(!unsafe { calculator_calculator_mean(std::ptr::null(), 0, &mut mean, &mut err) }); + let ints = [1, 2, 3, i32::MAX]; + let mut len = 0usize; + let ptr = + unsafe { calculator_calculator_running_total(ints.as_ptr(), 4, &mut len, &mut err) }; + assert_eq!( + unsafe { std::slice::from_raw_parts(ptr, len) }, + [1, 3, 6, i32::MIN + 5] + ); + unsafe { abi::free_bytes(ptr.cast(), len * 4) }; } } diff --git a/samples/codec/src/lib.rs b/samples/codec/src/lib.rs index 243e254f..ab248d5e 100644 --- a/samples/codec/src/lib.rs +++ b/samples/codec/src/lib.rs @@ -38,14 +38,27 @@ //! Buffers aren't the only way values cross. The `echo_*` functions return //! their argument unchanged through the direct ABI families (scalars by //! value, strings and bytes as `(ptr, len)` runs, the C-style enum as an -//! `int32_t`), so a consumer feeds every primitive vector's value through the -//! matching echo too. The `Token` interface with `primary_of`, -//! `same_primary`, and `sum_holder` checks object identity and reference -//! counting through buffers. +//! `int32_t`, optional scalars as a presence flag plus a value, numeric +//! lists as typed arrays, `usize` as a `u64`, a `char` as a one-scalar +//! string, and the custom `Hex` type as its string repr), so a consumer +//! feeds every primitive vector's value through the matching echo too. +//! `chunks` streams typed arrays through an iterator. The `Token` interface +//! with `primary_of`, `same_primary`, and `sum_holder` checks object +//! identity and reference counting through buffers. //! //! The producer itself is pure safe Rust; the `#[weaveffi::module]` //! expansion supplies the codecs the consumers are checked against. +/// Parse a `Hex`: lowercase or uppercase hex digits, no prefix. +fn parse_hex(text: String) -> Result { + u32::from_str_radix(&text, 16) +} + +/// Format a `Hex`: lowercase hex digits, no prefix. +fn format_hex(value: &u32) -> String { + format!("{value:x}") +} + /// Value-buffer round-trip oracle covering every wire shape. #[weaveffi::module] pub mod codec { @@ -58,6 +71,7 @@ pub mod codec { #[repr(i32)] pub enum CodecError { /// vector index out of range + #[weaveffi(message = "vector {index} is out of range (count {count})")] OutOfRange { /// The requested index. index: u32, @@ -66,15 +80,10 @@ pub mod codec { } = 1, } - impl std::fmt::Display for CodecError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::OutOfRange { index, count } => { - write!(f, "vector {index} is out of range (count {count})") - } - } - } - } + /// A `u32` written in hex, crossing as its string (`"ff"`): a custom + /// type. Bindings see a `string`; the producer sees a `u32`. + #[weaveffi::custom(repr = String, lift = super::parse_hex, lower = super::format_hex)] + pub type Hex = u32; /// A C-style enum with sparse and negative discriminants (crosses by /// value, and as an `i32` inside buffers). @@ -838,6 +847,85 @@ pub mod codec { value } + /// Return the argument unchanged (an optional `i32`: a presence flag and + /// a value). + #[weaveffi::export] + pub fn echo_opt_i32(value: Option) -> Option { + value + } + + /// Return the argument unchanged (an optional `f64`). + #[weaveffi::export] + pub fn echo_opt_f64(value: Option) -> Option { + value + } + + /// Return the argument unchanged (an optional `bool`). + #[weaveffi::export] + pub fn echo_opt_bool(value: Option) -> Option { + value + } + + /// Return the argument unchanged (an optional C-style enum). + #[weaveffi::export] + pub fn echo_opt_color(value: Option) -> Option { + value + } + + /// Return the argument unchanged (a typed array of `f64`, borrowed). + #[weaveffi::export] + pub fn echo_f64s(values: &[f64]) -> Vec { + values.to_vec() + } + + /// Return the argument unchanged (a typed array of `i32`). + #[weaveffi::export] + pub fn echo_i32s(values: Vec) -> Vec { + values + } + + /// Return the argument unchanged (a typed array of `u64`, borrowed). + #[weaveffi::export] + pub fn echo_u64s(values: &[u64]) -> Vec { + values.to_vec() + } + + /// Return the argument unchanged (a `usize`, crossing as a `u64`; a + /// value past `usize::MAX` on a 32-bit producer is a marshalling error). + #[weaveffi::export] + pub fn echo_usize(value: usize) -> usize { + value + } + + /// Return the argument unchanged (a `char`, crossing as a string of + /// exactly one Unicode scalar value; any other string is a marshalling + /// error). + #[weaveffi::export] + pub fn echo_char(value: char) -> char { + value + } + + /// Return the argument normalized (a `Hex`, crossing as a string): the + /// same number, written in lowercase hex without leading zeros. A + /// string that isn't hex for a `u32` is a marshalling error carrying the + /// parse failure's message. + #[weaveffi::export] + pub fn echo_hex(value: Hex) -> Hex { + value + } + + /// `values` split into consecutive arrays of `size` elements (the last + /// may be shorter), pulled lazily; nothing for a `size` of `0`. + #[weaveffi::export] + pub fn chunks(values: &[i32], size: u32) -> weaveffi::Iter> { + let chunks: Vec> = if size == 0 { + Vec::new() + } else { + values.chunks(size as usize).map(<[i32]>::to_vec).collect() + }; + weaveffi::Iter::new(chunks) + } + /// The sum of every token value inside `holder` (wrapping on overflow). #[weaveffi::export] pub fn sum_holder(holder: &Holder) -> i64 { @@ -1004,6 +1092,108 @@ mod tests { assert!(out.is_null() && len == 0 && err.code == 0); } + #[test] + fn optional_scalars_echo() { + let mut err = FfiError::default(); + let mut out = 0i32; + assert!(unsafe { codec_codec_echo_opt_i32(true, i32::MIN, &mut out, &mut err) }); + assert_eq!(out, i32::MIN); + assert!(!unsafe { codec_codec_echo_opt_i32(false, 5, &mut out, &mut err) }); + let mut f = 0.0f64; + assert!(unsafe { codec_codec_echo_opt_f64(true, -0.0, &mut f, &mut err) }); + assert!(f == 0.0 && f.is_sign_negative()); + let mut b = false; + assert!(unsafe { codec_codec_echo_opt_bool(true, true, &mut b, &mut err) }); + assert!(b); + let mut c = 0i32; + assert!(unsafe { codec_codec_echo_opt_color(true, -1, &mut c, &mut err) }); + assert_eq!(c, Color::Infrared as i32); + assert!(!unsafe { codec_codec_echo_opt_color(true, 3, &mut c, &mut err) }); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + unsafe { abi::error_clear(&mut err) }; + } + + fn take(ptr: *mut T, len: usize) -> Vec { + if len == 0 { + return Vec::new(); + } + let out = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec(); + unsafe { abi::free_bytes(ptr.cast(), len * std::mem::size_of::()) }; + out + } + + #[test] + fn typed_arrays_echo() { + let mut err = FfiError::default(); + let mut len = 0usize; + let floats = [f64::NAN, -0.0, 5e-324, f64::INFINITY]; + let out = take( + unsafe { codec_codec_echo_f64s(floats.as_ptr(), 4, &mut len, &mut err) }, + len, + ); + assert_eq!( + out.iter().map(|f| f.to_bits()).collect::>(), + floats.iter().map(|f| f.to_bits()).collect::>() + ); + let ints = [i32::MIN, 0, i32::MAX]; + let out = take( + unsafe { codec_codec_echo_i32s(ints.as_ptr(), 3, &mut len, &mut err) }, + len, + ); + assert_eq!(out, ints); + let words = [u64::MAX, 1 << 63]; + let out = take( + unsafe { codec_codec_echo_u64s(words.as_ptr(), 2, &mut len, &mut err) }, + len, + ); + assert_eq!(out, words); + let empty = unsafe { codec_codec_echo_u64s(std::ptr::null(), 0, &mut len, &mut err) }; + assert!(empty.is_null() && len == 0 && err.code == 0); + + let it = unsafe { codec_codec_chunks(ints.as_ptr(), 3, 2, &mut err) }; + let mut got = Vec::new(); + loop { + let mut item: *mut i32 = std::ptr::null_mut(); + if unsafe { codec_codec_ChunksIterator_next(it, &mut item, &mut len, &mut err) } == 0 { + break; + } + got.push(take(item, len)); + } + unsafe { codec_codec_ChunksIterator_destroy(it) }; + assert_eq!(got, vec![vec![i32::MIN, 0], vec![i32::MAX]]); + } + + #[test] + fn sizes_chars_and_custom_types_echo() { + let mut err = FfiError::default(); + assert_eq!( + unsafe { codec_codec_echo_usize(u64::from(u32::MAX), &mut err) }, + u64::from(u32::MAX) + ); + let mut len = 0usize; + let crab = "\u{1F980}"; + let p = unsafe { codec_codec_echo_char(crab.as_ptr(), crab.len(), &mut len, &mut err) }; + assert_eq!(unsafe { abi::lift_str(p, len) }, Some(crab)); + unsafe { abi::free_bytes(p.cast_mut(), len) }; + let two = "ab"; + let p = unsafe { codec_codec_echo_char(two.as_ptr(), two.len(), &mut len, &mut err) }; + assert!(p.is_null()); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + let hex = "00FF"; + let p = unsafe { codec_codec_echo_hex(hex.as_ptr(), hex.len(), &mut len, &mut err) }; + assert_eq!(unsafe { abi::lift_str(p, len) }, Some("ff")); + unsafe { abi::free_bytes(p.cast_mut(), len) }; + let bad = "xyz"; + let p = unsafe { codec_codec_echo_hex(bad.as_ptr(), bad.len(), &mut len, &mut err) }; + assert!(p.is_null()); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + assert_eq!( + unsafe { err.message_str() }, + Some("value: invalid digit found in string") + ); + unsafe { abi::error_clear(&mut err) }; + } + #[test] fn holders_carry_object_references() { let mut err = FfiError::default(); diff --git a/samples/kvstore/src/lib.rs b/samples/kvstore/src/lib.rs index 8020aa53..e37bbf46 100644 --- a/samples/kvstore/src/lib.rs +++ b/samples/kvstore/src/lib.rs @@ -6,22 +6,30 @@ //! //! * a reference-counted `Store` interface with fallible and infallible //! constructors, methods, statics, and a deprecated method; -//! * the `KvError` domain, whose codes carry payload fields; +//! * the `KvError` domain, whose codes carry payload fields, with its +//! `Display` generated from message templates; //! * records (`Entry`, `StoreInfo`), a C-style enum (`EntryKind`), and a rich //! enum (`Change`), with optionals, lists, and maps (including maps keyed by //! the C-style enum); //! * `Store` objects in every position: parameters, returns, optionals, //! lists, map values, record fields, iterator elements, an async result, //! and both a parameter and the return of a callback method; -//! * three callback interfaces the consumer implements: `Listener` (retained, +//! * four callback interfaces the consumer implements: `Listener` (retained, //! and notified from a producer thread during compaction), `Policy` -//! (rich returns and a throwing method whose typed error, payload -//! included, reaches the original caller), and `Loader` (string, bytes, and -//! optional-object returns, passed as an optional parameter); -//! * lazy iterators of strings, records, and objects; +//! (rich returns, an optional-scalar parameter and return, and methods +//! that throw `KvError`, whose typed errors, payload included, reach the +//! original caller typed), `Loader` (string, bytes, and optional-object +//! returns, passed as an optional parameter), and `Scorer` (a typed array +//! in and out); +//! * optional scalars and numeric lists crossing directly (an optional TTL +//! parameter, an optional expiry return, a `[u64]` return), `usize` +//! counts, and a `throws any` method (`import_lines`, failing with a +//! `String`); +//! * lazy iterators of strings, records, objects, and optional scalars; //! * async methods and functions, including a cancellable one that stops its //! background work cooperatively, with a test hook (`Store::active_jobs`) -//! that shows it did; +//! that shows it did, and ones completing with an optional scalar and a +//! typed array; //! * a nested `kv.stats` module that uses the parent's `Store` and inherits //! the parent's error domain, and a sibling `report` root that shares the //! `Entry` record. @@ -45,20 +53,23 @@ pub mod kv { use std::sync::{Arc, Mutex, PoisonError}; use std::task::{Context, Poll}; - use weaveffi::{CancelToken, ErrorReport, ForeignError}; + use weaveffi::{CancelToken, ForeignError}; /// The store's error domain. Each code's fields travel as the error's - /// payload, so every binding raises a typed error carrying them. + /// payload, so every binding raises a typed error carrying them. The + /// message of each is its template, filled from the fields. #[weaveffi::error] #[derive(Debug, Clone, PartialEq, Eq)] #[repr(i32)] pub enum KvError { /// key not found + #[weaveffi(message = "key not found: {key}")] KeyNotFound { /// The key that was looked up. key: String, } = 1001, /// entry expired + #[weaveffi(message = "entry {key} expired at {expired_at}")] Expired { /// The expired entry's key. key: String, @@ -66,6 +77,7 @@ pub mod kv { expired_at: i64, } = 1002, /// store is full + #[weaveffi(message = "store is full ({capacity} entries)")] StoreFull { /// The store's capacity. capacity: u32, @@ -73,69 +85,27 @@ pub mod kv { /// invalid path InvalidPath = 1004, /// write rejected by policy + #[weaveffi(message = "write to {key} rejected: {reason}")] Rejected { /// The rejected key. key: String, /// Why the policy rejected it. reason: String, } = 1005, + /// a consumer callback failed + #[weaveffi(message = "{message}")] + CallbackFailed { + /// The consumer's message. + message: String, + } = 1006, } - impl std::fmt::Display for KvError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::KeyNotFound { key } => write!(f, "key not found: {key}"), - Self::Expired { key, expired_at } => { - write!(f, "entry {key} expired at {expired_at}") - } - Self::StoreFull { capacity } => write!(f, "store is full ({capacity} entries)"), - Self::InvalidPath => f.write_str("invalid path"), - Self::Rejected { key, reason } => write!(f, "write to {key} rejected: {reason}"), - } - } - } - - /// A store operation's failure: a `KvError`, or a consumer callback's - /// failure passed through unchanged (its code, message, and payload), so - /// a typed error a callback raised reaches the original caller typed. - #[derive(Debug)] - pub enum StoreError { - /// A failure of the store itself. - Kv(KvError), - /// A callback's failure. - Callback(ForeignError), - } - - impl From for StoreError { - fn from(e: KvError) -> Self { - Self::Kv(e) - } - } - - impl From for StoreError { + /// A consumer callback that fails with anything but a `KvError` code + /// (or is called off its thread) fails the store operation with + /// `CallbackFailed`, carrying its message. + impl From for KvError { fn from(e: ForeignError) -> Self { - Self::Callback(e) - } - } - - impl ErrorReport for StoreError { - fn code(&self) -> i32 { - match self { - Self::Kv(e) => e.code(), - Self::Callback(e) => e.code(), - } - } - fn message(&self) -> String { - match self { - Self::Kv(e) => e.message(), - Self::Callback(e) => e.message(), - } - } - fn payload(&self) -> Vec { - match self { - Self::Kv(e) => e.payload(), - Self::Callback(e) => e.payload(), - } + Self::CallbackFailed { message: e.message } } } @@ -238,19 +208,32 @@ pub mod kv { /// with `set_policy`. #[weaveffi::callback_interface] pub trait Policy: Send + Sync { + /// The TTL a write of `key` gets, given the TTL the caller + /// `requested` (absent for none): return it unchanged to keep it, + /// another value to override it, or none to store without one. + /// Consulted before `admit`. + fn ttl_for(&self, key: &str, requested: Option) -> Result, KvError>; + /// Admit an entry about to be stored, returning it as it should be /// stored. The policy may change its value, kind, TTL, tags, and /// metadata (its key and version are kept). Fail with /// `Rejected` to veto the write: `put` then fails with that same /// error. - #[weaveffi::throws] - fn admit(&self, entry: &Entry) -> Result; + fn admit(&self, entry: &Entry) -> Result; /// The store `key` belongs in: return `home` (the store `put` was /// called on) to keep it there, or another store to redirect it. fn route(&self, key: &str, home: Arc) -> Result, ForeignError>; } + /// Scores entries for `Store::rank`. + #[weaveffi::callback_interface] + pub trait Scorer: Send + Sync { + /// One score per entry, given each entry's value size in bytes (in + /// key order). Higher scores rank first. + fn scores(&self, sizes: &[u64]) -> Result, ForeignError>; + } + /// A read-through source that `get_or_load` consults on a miss. #[weaveffi::callback_interface] pub trait Loader: Send + Sync { @@ -263,8 +246,7 @@ pub mod kv { /// The value for `key`. Fail with `KeyNotFound` naming /// `key` when there's none. - #[weaveffi::throws] - fn load(&self, key: &str) -> Result, ForeignError>; + fn load(&self, key: &str) -> Result, KvError>; } /// The default capacity of a new store. @@ -437,7 +419,7 @@ pub mod kv { .iter() .chain(named.values()) .chain(extra.as_ref().map(|i| &i.store)) - .map(|s| s.count()) + .map(|s| s.count() as u32) .sum() } @@ -481,17 +463,23 @@ pub mod kv { } /// Store `value` under `key`, returning the stored entry. The - /// installed policy (if any) admits the entry first and may route it - /// to another store, where it's stored instead. Fails with - /// `StoreFull`, or with the policy's failure unchanged (such as - /// `Rejected`). + /// installed policy (if any) picks the TTL (`ttl_for`), admits the + /// entry, and may route it to another store, where it's stored + /// instead. Fails with `StoreFull`, or with the policy's failure + /// (its own `KvError`, such as `Rejected`, unchanged, and any other + /// as `CallbackFailed`). pub fn put( self: Arc, key: String, value: Vec, kind: EntryKind, ttl_seconds: Option, - ) -> Result { + ) -> Result { + let policy = lock(&self.policy).clone(); + let ttl_seconds = match &policy { + Some(policy) => policy.ttl_for(&key, ttl_seconds)?, + None => ttl_seconds, + }; let mut entry = Entry { key: key.clone(), value, @@ -501,7 +489,6 @@ pub mod kv { tags: Vec::new(), metadata: BTreeMap::new(), }; - let policy = lock(&self.policy).clone(); let target = match policy { Some(policy) => { entry = policy.admit(&entry)?; @@ -509,7 +496,7 @@ pub mod kv { } None => self, }; - Ok(target.insert(entry, key)?) + target.insert(entry, key) } /// The live entry for `key`. Fails with `KeyNotFound`, or with @@ -551,12 +538,13 @@ pub mod kv { /// holding what `load` returns, with metadata `source` set to the /// loader's name (either way it's stored here). Returns no entry /// with no loader, or when `load` fails with `KeyNotFound` for - /// this same key; any other loader failure fails the call unchanged. + /// this same key; any other loader failure fails the call (a + /// `KvError` unchanged, anything else as `CallbackFailed`). pub fn get_or_load( &self, key: String, loader: Option>, - ) -> Result, StoreError> { + ) -> Result, KvError> { if let Some(entry) = self.find(key.clone()) { return Ok(Some(entry)); } @@ -568,12 +556,8 @@ pub mod kv { } let value = match loader.load(&key) { Ok(value) => value, - Err(e) => { - return match e.domain::() { - Some(KvError::KeyNotFound { key: missing }) if missing == key => Ok(None), - _ => Err(e.into()), - } - } + Err(KvError::KeyNotFound { key: missing }) if missing == key => return Ok(None), + Err(e) => return Err(e), }; let entry = Entry { key: key.clone(), @@ -613,14 +597,75 @@ pub mod kv { } /// The number of live (unexpired) entries. - pub fn count(&self) -> u32 { - self.live(None).len() as u32 + pub fn count(&self) -> usize { + self.live(None).len() } /// The number of live entries. #[deprecated(note = "use count()")] pub fn size(&self) -> u32 { - self.count() + self.count() as u32 + } + + /// The logical time at which `key`'s live entry expires: none when + /// there's no live entry or it has no TTL. + pub fn expires_at(&self, key: String) -> Option { + self.find(key).and_then(|e| e.expires_at) + } + + /// The value size in bytes of every live entry, in key order. + pub fn value_sizes(&self) -> Vec { + self.live(None) + .iter() + .map(|e| e.value.len() as u64) + .collect() + } + + /// The live keys ordered by `scorer`'s scores, highest first (ties + /// keep key order). Fails with `CallbackFailed` when the scorer + /// fails or returns a score count that doesn't match the entries. + pub fn rank(&self, scorer: Arc) -> Result, KvError> { + let entries = self.live(None); + let sizes: Vec = entries.iter().map(|e| e.value.len() as u64).collect(); + let scores = scorer.scores(&sizes)?; + if scores.len() != entries.len() { + return Err(KvError::CallbackFailed { + message: format!("expected {} scores, got {}", entries.len(), scores.len()), + }); + } + let mut ranked: Vec<(f64, String)> = scores + .into_iter() + .zip(entries.into_iter().map(|e| e.key)) + .collect(); + ranked.sort_by(|a, b| b.0.total_cmp(&a.0)); + Ok(ranked.into_iter().map(|(_, k)| k).collect()) + } + + /// Store one `Volatile` entry per line of `text`, each written + /// `key=value` (blank lines are skipped), returning how many were + /// stored. Fails, as an untyped error, with `line {n}: expected + /// key=value` (`n` counts from 1) at the first malformed line, after + /// storing the lines before it. + pub fn import_lines(self: Arc, text: &str) -> Result { + let mut stored = 0; + for (i, line) in text.lines().enumerate() { + if line.trim().is_empty() { + continue; + } + let Some((key, value)) = line.split_once('=') else { + return Err(format!("line {}: expected key=value", i + 1)); + }; + Arc::clone(&self) + .put( + key.to_string(), + value.as_bytes().to_vec(), + EntryKind::Volatile, + None, + ) + .map_err(|e| e.to_string())?; + stored += 1; + } + Ok(stored) } /// The live keys in order, optionally only those starting with @@ -645,6 +690,12 @@ pub mod kv { weaveffi::Iter::new(self.live(prefix.as_deref())) } + /// Each live entry's expiry time (none for an entry without a TTL), + /// in key order, pulled lazily from a snapshot. + pub fn expirations(&self) -> weaveffi::Iter> { + weaveffi::Iter::new(self.live(None).into_iter().map(|e| e.expires_at)) + } + /// One new store per prefix, created lazily as the iterator is /// pulled, holding copies of the live entries under that prefix /// (with the prefix as its path). @@ -675,8 +726,8 @@ pub mod kv { } /// The number of subscribed listeners. - pub fn listener_count(&self) -> u32 { - lock(&self.listeners).len() as u32 + pub fn listener_count(&self) -> usize { + lock(&self.listeners).len() } /// Install a write policy, replacing (and releasing) any previous @@ -716,7 +767,7 @@ pub mod kv { /// Snapshot this store into a record that carries the store itself. pub fn describe(self: Arc, label: String, mirror: Option>) -> StoreInfo { - let count = self.count(); + let count = self.count() as u32; StoreInfo { label, store: self, @@ -742,6 +793,20 @@ pub mod kv { pub async fn get_many(&self, keys: Vec) -> Vec> { keys.into_iter().map(|k| self.find(k)).collect() } + + /// The version of `key`'s live entry, if any, resolved on a producer + /// thread. + pub async fn version_of(&self, key: String) -> Option { + self.find(key).map(|e| e.version) + } + + /// The version of each key's live entry, in order (`0` where + /// there's none), resolved on a producer thread. + pub async fn versions(&self, keys: Vec) -> Vec { + keys.into_iter() + .map(|k| self.find(k).map_or(0, |e| e.version)) + .collect() + } } /// Open a store asynchronously, completing with the new object. Fails @@ -890,12 +955,6 @@ pub mod report { NothingToReport = 2001, } - impl std::fmt::Display for ReportError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("nothing to report") - } - } - /// One line per entry, sorted by key: `"{key}: {n} bytes, {kind}"`, with /// `, v{version}` appended when the version is above 1. Fails with /// `NothingToReport` for an empty list. diff --git a/samples/kvstore/src/tests.rs b/samples/kvstore/src/tests.rs index 82b6c193..f15c8c04 100644 --- a/samples/kvstore/src/tests.rs +++ b/samples/kvstore/src/tests.rs @@ -12,7 +12,7 @@ use std::ffi::c_void; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{mpsc, Arc, Mutex}; use std::time::Duration; -use weaveffi::abi::{self, ErrorReport, FfiError}; +use weaveffi::abi::{self, ErrorDomain, FfiError}; use weaveffi::ForeignError; fn store() -> Arc { @@ -22,7 +22,7 @@ fn store() -> Arc { fn put(s: &Arc, key: &str, value: &[u8]) -> Entry { Arc::clone(s) .put(key.to_string(), value.to_vec(), EntryKind::Persistent, None) - .unwrap_or_else(|e| panic!("put {key}: {}", e.message())) + .unwrap_or_else(|e| panic!("put {key}: {e}")) } fn keys(s: &Store, prefix: Option<&str>) -> Result, KvError> { @@ -97,7 +97,7 @@ fn capacity_rejects_new_keys_only() { .put("b".to_string(), vec![], EntryKind::Volatile, None) .unwrap_err(); assert_eq!(err.code(), 1003); - assert_eq!(err.message(), "store is full (1 entries)"); + assert_eq!(err.to_string(), "store is full (1 entries)"); assert_eq!(err.payload(), abi::encode_value(&1u32)); assert_eq!(Store::default_capacity(), 1_000_000); } @@ -197,16 +197,19 @@ struct Gatekeeper { } impl Policy for Gatekeeper { - fn admit(&self, entry: &Entry) -> Result { + fn ttl_for(&self, key: &str, requested: Option) -> Result, KvError> { + Ok(match key { + "short" => Some(1), + "forever" => None, + _ => requested, + }) + } + + fn admit(&self, entry: &Entry) -> Result { if entry.key.starts_with("secret") { - let e = KvError::Rejected { + return Err(KvError::Rejected { key: entry.key.clone(), reason: "no secrets".to_string(), - }; - return Err(ForeignError { - code: e.code(), - message: e.message(), - payload: e.payload(), }); } let mut admitted = entry.clone(); @@ -237,6 +240,13 @@ fn policy_admits_routes_and_rejects() { assert_eq!(e.tags, vec!["admitted"]); put(&s, "b/x", b"2"); assert_eq!((s.count(), other.count()), (1, 1)); + let short = put(&s, "short", b"3"); + assert_eq!(short.expires_at, Some(1), "the policy picked the TTL"); + assert_eq!(s.expires_at("short".to_string()), Some(1)); + let forever = Arc::clone(&s) + .put("forever".to_string(), vec![], EntryKind::Volatile, Some(5)) + .unwrap(); + assert_eq!(forever.expires_at, None, "the policy dropped the TTL"); let err = Arc::clone(&s) .put("secret".to_string(), vec![], EntryKind::Volatile, None) .unwrap_err(); @@ -268,18 +278,14 @@ impl Loader for Shelf { Ok(self.fallback.clone()) } - fn load(&self, key: &str) -> Result, ForeignError> { + fn load(&self, key: &str) -> Result, KvError> { if self.fail { - return Err(ForeignError::new(-4, "shelf is broken")); + // What a consumer failure that isn't a `KvError` arrives as. + return Err(ForeignError::new(-4, "shelf is broken").into()); } if let Some(missing) = &self.missing_key { - let e = KvError::KeyNotFound { + return Err(KvError::KeyNotFound { key: missing.clone(), - }; - return Err(ForeignError { - code: e.code(), - message: e.message(), - payload: e.payload(), }); } Ok(format!("loaded {key}").into_bytes()) @@ -329,8 +335,8 @@ fn loaders_fill_misses() { .get_or_load("y".to_string(), Some(shelf(None, None, true))) .unwrap_err(); assert_eq!( - (err.code(), err.message()), - (-4, "shelf is broken".to_string()) + (err.code(), err.to_string()), + (1006, "shelf is broken".to_string()) ); } @@ -407,6 +413,20 @@ struct RejectAll { freed: Arc, } +unsafe extern "C" fn reject_ttl_for( + _ctx: *mut c_void, + _key_ptr: *const u8, + _key_len: usize, + has_requested: bool, + requested: i64, + out_value: *mut i64, + _out_err: *mut FfiError, +) -> bool { + // An optional scalar crosses as a presence flag and a value, both ways. + unsafe { *out_value = requested * 2 }; + has_requested +} + unsafe extern "C" fn reject_admit( _ctx: *mut c_void, entry_ptr: *const u8, @@ -422,8 +442,9 @@ unsafe extern "C" fn reject_admit( reason: "read-only".to_string(), } .payload(); + let message = "rejected"; unsafe { - crate::kvstore_error_set(out_err, 1005, c"rejected".as_ptr()); + crate::kvstore_error_set(out_err, 1005, message.as_ptr(), message.len()); crate::kvstore_error_set_payload(out_err, payload.as_ptr(), payload.len()); } } @@ -449,6 +470,7 @@ static REJECT_VTABLE: kvstore_kv_Policy_vtable = kvstore_kv_Policy_vtable { flags: 0, free: reject_free, }, + ttl_for: reject_ttl_for, admit: reject_admit, route: reject_route, }; @@ -467,7 +489,6 @@ fn a_typed_callback_error_reaches_the_caller_with_its_payload() { assert_eq!(err.code, 0); let (key, value) = ("k", b"v"); - let ttl = abi::encode_value(&None::); let mut len = 0usize; let out = unsafe { kvstore_kv_Store_put( @@ -477,15 +498,20 @@ fn a_typed_callback_error_reaches_the_caller_with_its_payload() { value.as_ptr(), value.len(), EntryKind::Volatile as i32, - ttl.as_ptr(), - ttl.len(), + false, + 0, &mut len, &mut err, ) }; assert!(out.is_null()); assert_eq!(err.code, 1005); - assert_eq!(unsafe { err.message_str() }, Some("rejected")); + // The consumer's code decoded into the typed `KvError`, so the caller + // sees that error's own message, rendered from its fields. + assert_eq!( + unsafe { err.message_str() }, + Some("write to k rejected: read-only") + ); let mut r = abi::BufferReader::token_free(err.payload()); assert_eq!(r.read_string().unwrap(), "k"); assert_eq!(r.read_string().unwrap(), "read-only"); @@ -570,7 +596,7 @@ fn contract_tables_are_exported() { assert!(!unsafe { crate::kv::kvstore_kv_contract(&mut len) }.is_null()); assert!(len > 40); assert!(!unsafe { crate::report::kvstore_report_contract(&mut len) }.is_null()); - assert_eq!(len, 2); + assert_eq!(len, 3, "the function, the domain, and its code"); } #[test] @@ -593,7 +619,8 @@ fn listener_vtables_are_released_once() { _len: usize, out_err: *mut FfiError, ) { - unsafe { crate::kvstore_error_set(out_err, -4, c"nope".as_ptr()) }; + let message = "nope"; + unsafe { crate::kvstore_error_set(out_err, -4, message.as_ptr(), message.len()) }; } static VTABLE: kvstore_kv_Listener_vtable = kvstore_kv_Listener_vtable { header: abi::VtableHeader { @@ -613,3 +640,99 @@ fn listener_vtables_are_released_once() { assert_eq!(s.listener_count(), 0); assert_eq!(FREED.load(Ordering::SeqCst), 1); } + +#[test] +fn optional_scalars_arrays_and_counts() { + let s = store(); + put(&s, "b", b"12"); + Arc::clone(&s) + .put("a".to_string(), vec![1, 2, 3], EntryKind::Volatile, Some(7)) + .unwrap(); + assert_eq!(s.count(), 2usize); + assert_eq!(s.expires_at("a".to_string()), Some(7)); + assert_eq!(s.expires_at("b".to_string()), None); + assert_eq!(s.expires_at("zzz".to_string()), None); + assert_eq!(s.value_sizes(), vec![3, 2]); + assert_eq!(collect(s.expirations()), vec![Some(7), None]); + assert_eq!( + weaveffi::abi::block_on(s.version_of("a".to_string())), + Some(1) + ); + assert_eq!(weaveffi::abi::block_on(s.version_of("q".to_string())), None); + put(&s, "b", b"x"); + assert_eq!( + weaveffi::abi::block_on(s.versions(vec!["b".into(), "q".into(), "a".into()])), + vec![2, 0, 1] + ); +} + +struct BySize(f64); + +impl Scorer for BySize { + fn scores(&self, sizes: &[u64]) -> Result, ForeignError> { + if self.0 < 0.0 { + return Err(ForeignError::new(-4, "scorer is broken")); + } + Ok(sizes.iter().map(|s| *s as f64 * self.0).collect()) + } +} + +#[test] +fn rank_orders_by_the_consumers_scores() { + let s = store(); + put(&s, "a", b"1"); + put(&s, "b", b"333"); + put(&s, "c", b"22"); + assert_eq!(s.rank(Arc::new(BySize(1.0))).unwrap(), vec!["b", "c", "a"]); + assert_eq!(s.rank(Arc::new(BySize(-1.0))).unwrap_err().code(), 1006); +} + +#[test] +fn import_lines_fails_untyped() { + let s = store(); + assert_eq!(Arc::clone(&s).import_lines("a=1\n\nb=two\n"), Ok(2)); + assert_eq!(s.get("b".to_string()).unwrap().value, b"two"); + assert_eq!( + Arc::clone(&s).import_lines("c=3\nbroken\nd=4"), + Err("line 2: expected key=value".to_string()) + ); + assert_eq!(s.count(), 3, "the lines before the bad one were stored"); + + // `import_lines` throws `any`: the generic code and the `String`. + let mut err = FfiError::default(); + let text = "nope"; + let n = unsafe { + kvstore_kv_Store_import_lines(Arc::as_ptr(&s), text.as_ptr(), text.len(), &mut err) + }; + assert_eq!((n, err.code), (0, abi::GENERIC_ERROR_CODE)); + assert_eq!( + unsafe { err.message_str() }, + Some("line 1: expected key=value") + ); + unsafe { abi::error_clear(&mut err) }; +} + +#[test] +fn a_consumer_policy_returns_optional_scalars() { + let freed = Arc::new(AtomicBool::new(false)); + let ctx = Box::into_raw(Box::new(RejectAll { + freed: Arc::clone(&freed), + })); + // The same wrapper a `set_policy` call builds, driven directly: the + // optional TTL crosses as a presence flag plus a value, both ways. + let policy = unsafe { abi::lift_callback::(ctx.cast(), &REJECT_VTABLE, "p") } + .unwrap_or_else(|_| panic!("a valid vtable")); + assert_eq!(policy.ttl_for("k", Some(4)), Ok(Some(8))); + assert_eq!(policy.ttl_for("k", None), Ok(None)); + let entry = put(&store(), "k", b"v"); + assert_eq!( + policy.admit(&entry), + Err(KvError::Rejected { + key: "k".to_string(), + reason: "read-only".to_string() + }), + "a consumer's KvError arrives typed" + ); + drop(policy); + assert!(freed.load(Ordering::SeqCst)); +} diff --git a/scripts/fixtures/cpp.sh b/scripts/fixtures/cpp.sh index b331e628..b5d9cb06 100644 --- a/scripts/fixtures/cpp.sh +++ b/scripts/fixtures/cpp.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Compile the generated C++ wrapper header, together with the C header it -# ships, as C++17 with warnings as errors. The translation unit includes the -# wrapper and then the C header directly, so including both must compile. +# ships, with warnings as errors: as C++17 (the floor) with -Wpedantic, and +# as C++20. The translation unit includes the wrapper and then the C header +# directly, so including both must compile. set -euo pipefail . "$(dirname "$0")/lib.sh" require "${CXX:-clang++}" @@ -10,3 +11,4 @@ hpp=$(ls "$dir"/cpp/*.hpp | head -1) h=$(ls "$dir"/cpp/*.h | head -1) printf '#include "%s"\n#include "%s"\nint main() { return 0; }\n' "$hpp" "$h" > "$dir/cpp_check.cpp" "${CXX:-clang++}" -std=c++17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only "$dir/cpp_check.cpp" +"${CXX:-clang++}" -std=c++20 -Wall -Wextra -Werror -fsyntax-only "$dir/cpp_check.cpp" diff --git a/scripts/fixtures/js.sh b/scripts/fixtures/js.sh index 1910e2e6..0ccc93f7 100644 --- a/scripts/fixtures/js.sh +++ b/scripts/fixtures/js.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Shared by node.sh and wasm.sh: `js_check ...` runs -# `tsc --noEmit --strict` on /index.d.ts and `node --check` on each -# module. Without a `tsc` on PATH, TypeScript is installed once into +# `tsc --noEmit --strict` on /index.d.ts and /debug.d.ts and +# `node --check` on each module. Without a `tsc` on PATH, TypeScript is installed once into # ${WEAVEFFI_TSC_DIR:-$TMPDIR/weaveffi-typescript} (this needs npm and the # network). Callers source lib.sh first. @@ -24,8 +24,22 @@ js_check() { local tsc command -v tsc >/dev/null 2>&1 || require npm tsc=$(tsc_bin) - "$tsc" --noEmit --strict --target es2022 --module es2022 \ - --lib es2022,dom,esnext.disposable --types "" "$dir/index.d.ts" + # A config file, since `--types ""` (no ambient @types packages) isn't + # accepted on the command line by every TypeScript release. + cat > "$dir/tsconfig.check.json" <<'EOF' +{ + "compilerOptions": { + "noEmit": true, + "strict": true, + "target": "es2022", + "module": "es2022", + "lib": ["es2022", "dom", "esnext.disposable"], + "types": [] + }, + "files": ["index.d.ts", "debug.d.ts"] +} +EOF + "$tsc" -p "$dir/tsconfig.check.json" for f in "$@"; do node --check "$dir/$f" done diff --git a/scripts/fixtures/node.sh b/scripts/fixtures/node.sh index 5c36e8ff..0712ba65 100644 --- a/scripts/fixtures/node.sh +++ b/scripts/fixtures/node.sh @@ -10,7 +10,7 @@ here=$(cd "$(dirname "$0")" && pwd) . "$here/js.sh" require node cc -js_check "$dir" index.js runtime.js +js_check "$dir" index.js runtime.js debug.js # The headers node-gyp caches, or the ones an official Node.js distribution # (nvm, actions/setup-node) ships next to its binary. diff --git a/scripts/fixtures/python.sh b/scripts/fixtures/python.sh index 36c3ccfc..6722caef 100755 --- a/scripts/fixtures/python.sh +++ b/scripts/fixtures/python.sh @@ -7,16 +7,17 @@ # byte-compile has run. set -euo pipefail . "$(dirname "$0")/lib.sh" -require python3 +. "$(dirname "$0")/../toolchains.sh" +python=$(find_python) || missing "Python 3.10 or newer not found" dir=$1 -find "$dir/python" -name '*.py' -print0 | xargs -0 python3 -m py_compile -if command -v mypy >/dev/null 2>&1; then +find "$dir/python" -name '*.py' -print0 | xargs -0 "$python" -m py_compile +if "$python" -c "import mypy" >/dev/null 2>&1; then + mypy=("$python" -m mypy) +elif command -v mypy >/dev/null 2>&1; then mypy=(mypy) -elif python3 -c "import mypy" >/dev/null 2>&1; then - mypy=(python3 -m mypy) else missing "mypy not found; type checks not run (pip install 'mypy<2')" fi for pkg in "$dir"/python/*/; do - "${mypy[@]}" --strict --python-version 3.9 --no-incremental "$pkg" + "${mypy[@]}" --strict --python-version 3.10 --no-incremental "$pkg" done diff --git a/scripts/fixtures/ruby.sh b/scripts/fixtures/ruby.sh index 25ed9de8..13e17d31 100644 --- a/scripts/fixtures/ruby.sh +++ b/scripts/fixtures/ruby.sh @@ -1,9 +1,16 @@ #!/usr/bin/env bash # Syntax-check every generated Ruby file with warnings enabled (any warning -# fails), then build the gem from its generated gemspec. +# fails), build the gem from its generated gemspec, and then load the +# bindings for real: a stub library built from the C header generated +# alongside (every function returns zero; the contract tables are the +# header's own) lets `require` run the ABI and contract checks, attach every +# function, and evaluate every class, layout, and vtable, again with +# warnings enabled. set -euo pipefail . "$(dirname "$0")/lib.sh" -require ruby gem +. "$(dirname "$0")/../toolchains.sh" +use_ruby || missing "Ruby 3.2 or newer not found" +require gem cc dir=$(cd "$1" && pwd) status=0 while IFS= read -r file; do @@ -15,4 +22,37 @@ while IFS= read -r file; do done < <(find "$dir/ruby" -name '*.rb' -o -name '*.gemspec') spec=$(cd "$dir/ruby" && ls ./*.gemspec) (cd "$dir/ruby" && gem build --silent "$spec" -o "$dir/ruby-fixture.gem") || status=1 + +lib=$(find "$dir/ruby/lib" -maxdepth 1 -name '*.rb' | head -1) +feature=$(basename "$lib" .rb) +header=$(find "$dir/c" -name '*.h' ! -name '*_buffer.h' | head -1) +env_var=$(sed -n "s/.*ENV.fetch('\([A-Z0-9_]*\)'.*/\1/p" "$dir/ruby/lib/$feature/runtime.rb") +case "$(uname -s)" in + Darwin) stub="$dir/libruby_stub.dylib" ;; + *) stub="$dir/libruby_stub.so" ;; +esac +ruby - "$header" > "$dir/ruby_stub.c" <<'RUBY' +header = ARGV.fetch(0) +src = File.read(header) +api = src[/#ifndef (\w+_API)/, 1] +puts %(#include "#{File.basename(header)}") +src.scan(/^#{api} (.+?)\b(\w+)\((.*)\);$/) do |ret, name, args| + ret = ret.strip + body = + if name.end_with?('_contract') + entry = ret.sub('const ', '').delete('*').strip + "static const #{entry} e[] = #{name.upcase}; *out_len = sizeof e / sizeof e[0]; return e;" + elsif name.end_with?('_abi_version') then "return #{src[/#define \w+_ABI_VERSION (\d+)u/, 1]};" + elsif ret == 'void' then '' + else 'return 0;' + end + puts "#{ret} #{name}(#{args}) { #{body} }" +end +RUBY +cc -shared -fPIC -w -I "$dir/c" -o "$stub" "$dir/ruby_stub.c" || status=1 +if ! out=$(env "$env_var=$stub" ruby -w -I "$dir/ruby/lib" -e "require '$feature'" 2>&1) || [ -n "$out" ]; then + echo "loading $feature against the stub library:" >&2 + echo "$out" >&2 + status=1 +fi exit $status diff --git a/scripts/fixtures/wasm.sh b/scripts/fixtures/wasm.sh index 2b7f469d..84f15dac 100644 --- a/scripts/fixtures/wasm.sh +++ b/scripts/fixtures/wasm.sh @@ -8,4 +8,4 @@ here=$(cd "$(dirname "$0")" && pwd) . "$here/js.sh" require node -js_check "$dir" index.js runtime.js linear.js +js_check "$dir" index.js runtime.js debug.js linear.js diff --git a/scripts/package-smoke.sh b/scripts/package-smoke.sh index c72a0a4f..15822067 100755 --- a/scripts/package-smoke.sh +++ b/scripts/package-smoke.sh @@ -20,6 +20,7 @@ set -euo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) cd "$ROOT" +. "$ROOT/scripts/toolchains.sh" if [ $# -gt 0 ]; then TARGETS="$*" @@ -74,13 +75,14 @@ TARGET_LIST=$(echo "$TARGETS" | tr ' ' ',') echo "==> generate ($TARGET_LIST)" "$WEAVEFFI" generate --config "$CONFIG" --target "$TARGET_LIST" echo "==> package ($TARGET_LIST)" -"$WEAVEFFI" package --config "$CONFIG" --target "$TARGET_LIST" +# Under CI every tool is installed, so a skipped artifact is a failure. +"$WEAVEFFI" package --config "$CONFIG" --target "$TARGET_LIST" ${CI:+--strict} smoke_python() { - have python3 || return 77 - local wheel + local python wheel + python=$(find_python) || have python3.10 || return 77 wheel=$(ls "$DIST"/python/calculator-1.0.0-py3-none-*.whl) - python3 -m venv "$SCRATCH/venv" + "$python" -m venv "$SCRATCH/venv" "$SCRATCH/venv/bin/python" -m pip install --quiet --disable-pip-version-check "$wheel" (cd "$SCRATCH" && venv/bin/python -c ' import calculator diff --git a/scripts/toolchains.sh b/scripts/toolchains.sh new file mode 100644 index 00000000..1b688c51 --- /dev/null +++ b/scripts/toolchains.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Interpreter discovery shared by the fixture checks (scripts/fixtures), the +# conformance lanes (conformance/), and scripts/package-smoke.sh. +# +# The generated Python package requires Python 3.10 or newer and the +# generated gem Ruby 3.2 or newer, while a system `python3` or `ruby` can be +# older (macOS ships Python 3.9 and Ruby 2.6). These helpers respect the +# interpreter on PATH when it's new enough and otherwise look for a newer +# one under a versioned name (Python) or in a Homebrew keg (Ruby). + +# Print the command of the first Python 3.10+ interpreter found: `python3` +# on PATH first, then `python3.N` names from newest to oldest. Fail when +# there's none. +find_python() { + local candidate + for candidate in python3 python3.14 python3.13 python3.12 python3.11 python3.10 python; do + command -v "$candidate" >/dev/null 2>&1 || continue + if "$candidate" -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then + echo "$candidate" + return 0 + fi + done + return 1 +} + +# Put a Ruby 3.2+ first on PATH, with its own `gem`: the `ruby` on PATH when +# it's new enough, else Homebrew's keg-only Ruby. Fail when there's none. +use_ruby() { + local candidate dir + for candidate in "$(command -v ruby 2>/dev/null)" \ + "$(brew --prefix ruby 2>/dev/null)/bin/ruby"; do + [ -n "$candidate" ] && [ -x "$candidate" ] || continue + if "$candidate" -e 'exit(Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2"))' 2>/dev/null; then + dir=$(dirname "$candidate") + case ":$PATH:" in + ":$dir:"*) ;; + *) PATH="$dir:$PATH"; export PATH ;; + esac + return 0 + fi + done + return 1 +} diff --git a/weaveffi.schema.json b/weaveffi.schema.json index 65a6287b..9cc3d91a 100644 --- a/weaveffi.schema.json +++ b/weaveffi.schema.json @@ -9,7 +9,7 @@ ], "properties": { "version": { - "description": "IR schema version this document targets (for example `0.11.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", + "description": "IR schema version this document targets (for example `0.12.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", "type": "string" }, "modules": { @@ -76,15 +76,11 @@ } }, "errors": { - "description": "Optional error domain: the named codes this module's fallible functions report. `None` when the module declares no errors.", - "anyOf": [ - { - "$ref": "#/definitions/ErrorDomain" - }, - { - "type": "null" - } - ] + "description": "The error domains this module declares: named sets of codes that fallible callables anywhere in the API report by naming the domain in their `throws`. Domain names share the global type namespace.", + "type": "array", + "items": { + "$ref": "#/definitions/ErrorDomain" + } }, "modules": { "description": "Nested submodules, forming a tree that mirrors a package hierarchy.", @@ -133,8 +129,15 @@ ] }, "throws": { - "description": "Whether the function can fail with a domain error. A throwing function surfaces as `throws`/`raises` in the idiomatic bindings, reporting the owning module's error domain; a non-throwing function has a plain signature and treats any error as a producer bug (a trap, not a typed error). Defaults to `false`.", - "type": "boolean" + "description": "How the function can fail. Absent for a function that can't, which treats any reported error as a producer bug (a trap, not a typed error); an error domain's name (`throws: KvError`) for one that reports that domain's codes; or `any` (`throws: any`) for one that fails with an untyped error, a message under the generic runtime code `-1`.", + "anyOf": [ + { + "$ref": "#/definitions/Throws" + }, + { + "type": "null" + } + ] }, "async": { "description": "Whether the function is asynchronous, lowering to a completion-callback form rather than a blocking call. Serialized under the IDL key `async`.", @@ -189,6 +192,11 @@ "description": "Reference to a type. Encoded as a string with custom syntax: primitives (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `f32`, `f64`, `bool`, `string`, `bytes`), iterators (`iter<{T}>`), lists (`[{T}]`), maps (`{{K:V}}`), optionals (`{T}?`), or the bare (unqualified) name of a user-defined struct, enum, interface, or callback interface.", "type": "string" }, + "Throws": { + "title": "Throws", + "description": "What a fallible callable reports: the name of an error domain declared in any module, or `any` for an untyped error (code -1 and a message).", + "type": "string" + }, "InterfaceDef": { "description": "An interface: an opaque object type with constructors, methods, and statics.", "type": "object", @@ -411,7 +419,7 @@ "additionalProperties": false }, "ErrorDomain": { - "description": "A module's error domain: the named set of error codes its fallible functions can report.", + "description": "An error domain: a named, open set of error codes that fallible callables report by naming the domain in their `throws`.\n\nA module may declare several. The domain's name is a type name (unique across the API); its code names are unique across the API too, and its code values are positive and unique within the domain. Domains are open: adding a code never breaks a deployed binding, which maps a positive code it doesn't know to the domain's base error type.", "type": "object", "required": [ "codes", @@ -419,7 +427,7 @@ ], "properties": { "name": { - "description": "Error domain name, used to name the generated error type (for example `ContactErrors`).", + "description": "Error domain name, used to name the generated error type (for example `ContactError`). Callables name it in their `throws`.", "type": "string" }, "codes": {